1# Orca
2#
3# Copyright 2010 Joanmarie Diggs.
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
18# Boston MA  02110-1301 USA.
19
20"""Commonly-required utility methods needed by -- and potentially
21   customized by -- application and toolkit scripts. They have
22   been pulled out from the scripts because certain scripts had
23   gotten way too large as a result of including these methods."""
24
25__id__ = "$Id$"
26__version__   = "$Revision$"
27__date__      = "$Date$"
28__copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
29__license__   = "LGPL"
30
31import functools
32import gi
33import locale
34import math
35import pyatspi
36import re
37import subprocess
38import time
39from gi.repository import Gdk
40from gi.repository import Gtk
41
42from . import chnames
43from . import colornames
44from . import debug
45from . import keynames
46from . import keybindings
47from . import input_event
48from . import mathsymbols
49from . import messages
50from . import orca
51from . import orca_state
52from . import object_properties
53from . import pronunciation_dict
54from . import settings
55from . import settings_manager
56from . import text_attribute_names
57
58_settingsManager = settings_manager.getManager()
59
60#############################################################################
61#                                                                           #
62# Utilities                                                                 #
63#                                                                           #
64#############################################################################
65
66class Utilities:
67
68    _desktop = pyatspi.Registry.getDesktop(0)
69    _last_clipboard_update = time.time()
70
71    EMBEDDED_OBJECT_CHARACTER = '\ufffc'
72    ZERO_WIDTH_NO_BREAK_SPACE = '\ufeff'
73    SUPERSCRIPT_DIGITS = \
74        ['\u2070', '\u00b9', '\u00b2', '\u00b3', '\u2074',
75         '\u2075', '\u2076', '\u2077', '\u2078', '\u2079']
76    SUBSCRIPT_DIGITS = \
77        ['\u2080', '\u2081', '\u2082', '\u2083', '\u2084',
78         '\u2085', '\u2086', '\u2087', '\u2088', '\u2089']
79
80    flags = re.UNICODE
81    WORDS_RE = re.compile(r"(\W+)", flags)
82    SUPERSCRIPTS_RE = re.compile("[%s]+" % "".join(SUPERSCRIPT_DIGITS), flags)
83    SUBSCRIPTS_RE = re.compile("[%s]+" % "".join(SUBSCRIPT_DIGITS), flags)
84    PUNCTUATION = re.compile(r"[^\w\s]", flags)
85
86    # generatorCache
87    #
88    DISPLAYED_DESCRIPTION = 'displayedDescription'
89    DISPLAYED_LABEL = 'displayedLabel'
90    DISPLAYED_TEXT = 'displayedText'
91    KEY_BINDING = 'keyBinding'
92    NESTING_LEVEL = 'nestingLevel'
93    NODE_LEVEL = 'nodeLevel'
94
95    def __init__(self, script):
96        """Creates an instance of the Utilities class.
97
98        Arguments:
99        - script: the script with which this instance is associated.
100        """
101
102        self._script = script
103        self._clipboardHandlerId = None
104        self._selectedMenuBarMenu = {}
105
106    #########################################################################
107    #                                                                       #
108    # Utilities for finding, identifying, and comparing accessibles         #
109    #                                                                       #
110    #########################################################################
111
112    def _isActiveAndShowingAndNotIconified(self, obj):
113        try:
114            state = obj.getState()
115        except:
116            msg = "ERROR: Exception getting state of %s" % obj
117            debug.println(debug.LEVEL_INFO, msg, True)
118            return False
119
120        if not state.contains(pyatspi.STATE_ACTIVE):
121            msg = "INFO: %s lacks state active" % obj
122            debug.println(debug.LEVEL_INFO, msg, True)
123            return False
124
125        if state.contains(pyatspi.STATE_ICONIFIED):
126            msg = "INFO: %s has state iconified" % obj
127            debug.println(debug.LEVEL_INFO, msg, True)
128            return False
129
130        if not state.contains(pyatspi.STATE_SHOWING):
131            msg = "INFO: %s lacks state showing" % obj
132            debug.println(debug.LEVEL_INFO, msg, True)
133            return False
134
135        return True
136
137    @staticmethod
138    def _getAppCommandLine(app):
139        if not app:
140            return ""
141
142        try:
143            pid = app.get_process_id()
144        except:
145            msg = "ERROR: Exception getting process id of %s. May be defunct." % app
146            debug.println(debug.LEVEL_INFO, msg, True)
147            return ""
148
149        try:
150            cmdline = subprocess.getoutput("cat /proc/%s/cmdline" % pid)
151        except:
152            return ""
153
154        return cmdline.replace("\x00", " ")
155
156    def canBeActiveWindow(self, window, clearCache=False):
157        if not window:
158            return False
159
160        try:
161            app = window.getApplication()
162        except:
163            app = None
164
165        msg = "INFO: Looking at %s from %s %s" % (window, app, self._getAppCommandLine(app))
166        debug.println(debug.LEVEL_INFO, msg, True)
167
168        if clearCache:
169            window.clearCache()
170
171        if not self._isActiveAndShowingAndNotIconified(window):
172            msg = "INFO: %s is not active and showing, or is iconified" % window
173            debug.println(debug.LEVEL_INFO, msg, True)
174            return False
175
176        msg = "INFO: %s can be active window" % window
177        debug.println(debug.LEVEL_INFO, msg, True)
178        return True
179
180    def activeWindow(self, *apps):
181        """Tries to locate the active window; may or may not succeed."""
182
183        candidates = []
184        apps = apps or self.knownApplications()
185        for app in apps:
186            try:
187                candidates.extend([child for child in app if self.canBeActiveWindow(child)])
188            except:
189                msg = "ERROR: Exception examining children of %s" % app
190                debug.println(debug.LEVEL_INFO, msg, True)
191
192        if not candidates:
193            msg = "ERROR: Unable to find active window from %s" % list(map(str, apps))
194            debug.println(debug.LEVEL_INFO, msg, True)
195            return None
196
197        if len(candidates) == 1:
198            msg = "INFO: Active window is %s" % candidates[0]
199            debug.println(debug.LEVEL_INFO, msg, True)
200            return candidates[0]
201
202        # Sorting by size in a lame attempt to filter out the "desktop" frame of various
203        # desktop environments. App name won't work because we don't know it. Getting the
204        # screen/desktop size via Gdk risks a segfault depending on the user environment.
205        # Asking AT-SPI2 for the size seems to give us 1024x768 regardless of reality....
206        # This is why we can't have nice things.
207        candidates = sorted(candidates, key=functools.cmp_to_key(self.sizeComparison))
208        msg = "WARNING: These windows all claim to be active: %s" % list(map(str, candidates))
209        debug.println(debug.LEVEL_INFO, msg, True)
210
211        msg = "INFO: Active window is (hopefully) %s" % candidates[0]
212        debug.println(debug.LEVEL_INFO, msg, True)
213        return candidates[0]
214
215    @staticmethod
216    def ancestorWithRole(obj, ancestorRoles, stopRoles):
217        """Returns the object of the specified roles which contains the
218        given object, or None if the given object is not contained within
219        an object the specified roles.
220
221        Arguments:
222        - obj: the Accessible object
223        - ancestorRoles: the list of roles to look for
224        - stopRoles: the list of roles to stop the search at
225        """
226
227        if not obj:
228            return None
229
230        if not isinstance(ancestorRoles, [].__class__):
231            ancestorRoles = [ancestorRoles]
232
233        if not isinstance(stopRoles, [].__class__):
234            stopRoles = [stopRoles]
235
236        ancestor = None
237
238        obj = obj.parent
239        while obj and (obj != obj.parent):
240            try:
241                role = obj.getRole()
242            except:
243                break
244            if role in ancestorRoles:
245                ancestor = obj
246                break
247            elif role in stopRoles:
248                break
249            else:
250                obj = obj.parent
251
252        return ancestor
253
254    def objectAttributes(self, obj, useCache=True):
255        try:
256            rv = dict([attr.split(':', 1) for attr in obj.getAttributes()])
257        except:
258            rv = {}
259
260        return rv
261
262    def cellIndex(self, obj):
263        """Returns the index of the cell which should be used with the
264        table interface.  This is necessary because in some apps we
265        cannot count on getIndexInParent() returning the index we need.
266
267        Arguments:
268        -obj: the table cell whose index we need.
269        """
270
271        attrs = self.objectAttributes(obj)
272        index = attrs.get('table-cell-index')
273        if index:
274            return int(index)
275
276        isCell = lambda x: x and x.getRole() in self.getCellRoles()
277        obj = pyatspi.findAncestor(obj, isCell) or obj
278        return obj.getIndexInParent()
279
280    def childNodes(self, obj):
281        """Gets all of the children that have RELATION_NODE_CHILD_OF pointing
282        to this expanded table cell.
283
284        Arguments:
285        -obj: the Accessible Object
286
287        Returns: a list of all the child nodes
288        """
289
290        try:
291            table = obj.parent.queryTable()
292        except:
293            return []
294        else:
295            if not obj.getState().contains(pyatspi.STATE_EXPANDED):
296                return []
297
298        nodes = []
299
300        # First see if this accessible implements RELATION_NODE_PARENT_OF.
301        # If it does, the full target list are the nodes. If it doesn't
302        # we'll do an old-school, row-by-row search for child nodes.
303        #
304        relations = obj.getRelationSet()
305        try:
306            for relation in relations:
307                if relation.getRelationType() == \
308                        pyatspi.RELATION_NODE_PARENT_OF:
309                    for target in range(relation.getNTargets()):
310                        node = relation.getTarget(target)
311                        if node and node.getIndexInParent() != -1:
312                            nodes.append(node)
313                    return nodes
314        except:
315            pass
316
317        index = self.cellIndex(obj)
318        row = table.getRowAtIndex(index)
319        col = table.getColumnAtIndex(index)
320        nodeLevel = self.nodeLevel(obj)
321        done = False
322
323        # Candidates will be in the rows beneath the current row.
324        # Only check in the current column and stop checking as
325        # soon as the node level of a candidate is equal or less
326        # than our current level.
327        #
328        for i in range(row+1, table.nRows):
329            cell = table.getAccessibleAt(i, col)
330            if not cell:
331                continue
332            relations = cell.getRelationSet()
333            for relation in relations:
334                if relation.getRelationType() \
335                       == pyatspi.RELATION_NODE_CHILD_OF:
336                    nodeOf = relation.getTarget(0)
337                    if self.isSameObject(obj, nodeOf):
338                        nodes.append(cell)
339                    else:
340                        currentLevel = self.nodeLevel(nodeOf)
341                        if currentLevel <= nodeLevel:
342                            done = True
343                    break
344            if done:
345                break
346
347        return nodes
348
349    def commonAncestor(self, a, b):
350        """Finds the common ancestor between Accessible a and Accessible b.
351
352        Arguments:
353        - a: Accessible
354        - b: Accessible
355        """
356
357        msg = 'INFO: Looking for common ancestor of %s and %s' % (a, b)
358        debug.println(debug.LEVEL_INFO, msg, True)
359
360        # Don't do any Zombie checks here, as tempting and logical as it
361        # may seem as it can lead to chattiness.
362        if not (a and b):
363            return None
364
365        if a == b:
366            return a
367
368        aParents = [a]
369        try:
370            parent = a.parent
371            while parent and (parent.parent != parent):
372                aParents.append(parent)
373                parent = parent.parent
374            aParents.reverse()
375        except:
376            debug.printException(debug.LEVEL_FINEST)
377
378        bParents = [b]
379        try:
380            parent = b.parent
381            while parent and (parent.parent != parent):
382                bParents.append(parent)
383                parent = parent.parent
384            bParents.reverse()
385        except:
386            debug.printException(debug.LEVEL_FINEST)
387
388        commonAncestor = None
389
390        maxSearch = min(len(aParents), len(bParents))
391        i = 0
392        while i < maxSearch:
393            if self.isSameObject(aParents[i], bParents[i]):
394                commonAncestor = aParents[i]
395                i += 1
396            else:
397                break
398
399        msg = 'INFO: Common ancestor of %s and %s is %s' % (a, b, commonAncestor)
400        debug.println(debug.LEVEL_INFO, msg, True)
401        return commonAncestor
402
403    def defaultButton(self, obj):
404        """Returns the default button in the dialog which contains obj.
405
406        Arguments:
407        - obj: the top-level object (e.g. window, frame, dialog) for
408          which the status bar is sought.
409        """
410
411        # There are some objects which are not worth descending.
412        #
413        skipRoles = [pyatspi.ROLE_TREE,
414                     pyatspi.ROLE_TREE_TABLE,
415                     pyatspi.ROLE_TABLE]
416
417        if obj.getState().contains(pyatspi.STATE_MANAGES_DESCENDANTS) \
418           or obj.getRole() in skipRoles:
419            return
420
421        defaultButton = None
422        # The default button is likely near the bottom of the window.
423        #
424        for i in range(obj.childCount - 1, -1, -1):
425            if obj[i].getRole() == pyatspi.ROLE_PUSH_BUTTON \
426                and obj[i].getState().contains(pyatspi.STATE_IS_DEFAULT):
427                defaultButton = obj[i]
428            elif not obj[i].getRole() in skipRoles:
429                defaultButton = self.defaultButton(obj[i])
430
431            if defaultButton:
432                break
433
434        return defaultButton
435
436    def displayedLabel(self, obj):
437        """If there is an object labelling the given object, return the
438        text being displayed for the object labelling this object.
439        Otherwise, return None.
440
441        Argument:
442        - obj: the object in question
443
444        Returns the string of the object labelling this object, or None
445        if there is nothing of interest here.
446        """
447
448        try:
449            return self._script.generatorCache[self.DISPLAYED_LABEL][obj]
450        except:
451            if self.DISPLAYED_LABEL not in self._script.generatorCache:
452                self._script.generatorCache[self.DISPLAYED_LABEL] = {}
453            labelString = None
454
455        labels = self.labelsForObject(obj)
456        for label in labels:
457            labelString = \
458                self.appendString(labelString, self.displayedText(label))
459
460        self._script.generatorCache[self.DISPLAYED_LABEL][obj] = labelString
461        return self._script.generatorCache[self.DISPLAYED_LABEL][obj]
462
463    def preferDescriptionOverName(self, obj):
464        return False
465
466    def descriptionsForObject(self, obj):
467        """Return a list of objects describing obj."""
468
469        try:
470            relations = obj.getRelationSet()
471        except (LookupError, RuntimeError):
472            msg = 'ERROR: Exception getting relationset for %s' % obj
473            debug.println(debug.LEVEL_INFO, msg, True)
474            return []
475
476        describedBy = lambda x: x.getRelationType() == pyatspi.RELATION_DESCRIBED_BY
477        relation = filter(describedBy, relations)
478        return [r.getTarget(i) for r in relation for i in range(r.getNTargets())]
479
480    def detailsContentForObject(self, obj):
481        details = self.detailsForObject(obj)
482        return list(map(self.displayedText, details))
483
484    def detailsForObject(self, obj, textOnly=True):
485        """Return a list of objects containing details for obj."""
486
487        try:
488            relations = obj.getRelationSet()
489            role = obj.getRole()
490            state = obj.getState()
491        except (LookupError, RuntimeError):
492            msg = 'ERROR: Exception getting relationset, role, and state for %s' % obj
493            debug.println(debug.LEVEL_INFO, msg, True)
494            return []
495
496        hasDetails = lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS
497        relation = filter(hasDetails, relations)
498        details = [r.getTarget(i) for r in relation for i in range(r.getNTargets())]
499        if not details and role == pyatspi.ROLE_TOGGLE_BUTTON and state.contains(pyatspi.STATE_EXPANDED):
500            details = [child for child in obj]
501
502        if not textOnly:
503            return details
504
505        textObjects = []
506        for detail in details:
507            textObjects.extend(self.findAllDescendants(detail, self.queryNonEmptyText))
508
509        return textObjects
510
511    def displayedDescription(self, obj):
512        """Returns the text being displayed for the object describing obj."""
513
514        try:
515            return self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj]
516        except:
517            if self.DISPLAYED_DESCRIPTION not in self._script.generatorCache:
518                self._script.generatorCache[self.DISPLAYED_DESCRIPTION] = {}
519
520        string = " ".join(map(self.displayedText, self.descriptionsForObject(obj)))
521        self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj] = string
522        return self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj]
523
524    def displayedText(self, obj):
525        """Returns the text being displayed for an object.
526
527        Arguments:
528        - obj: the object
529
530        Returns the text being displayed for an object or None if there isn't
531        any text being shown.
532        """
533
534        try:
535            return self._script.generatorCache[self.DISPLAYED_TEXT][obj]
536        except:
537            displayedText = None
538
539        try:
540            role = obj.getRole()
541            name = obj.name
542        except:
543            msg = 'ERROR: Exception getting role and name of %s' % obj
544            debug.println(debug.LEVEL_INFO, msg, True)
545            role = None
546            name = ''
547
548        if role in [pyatspi.ROLE_PUSH_BUTTON, pyatspi.ROLE_LABEL] and name:
549            return name
550
551        if 'Text' in pyatspi.listInterfaces(obj):
552            # We should be able to use -1 for the final offset, but that crashes Nautilus.
553            text = obj.queryText()
554            displayedText = text.getText(0, text.characterCount)
555            if self.EMBEDDED_OBJECT_CHARACTER in displayedText:
556                displayedText = None
557
558        if not displayedText and role not in [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_SPIN_BUTTON]:
559            # TODO - JD: This should probably get nuked. But all sorts of
560            # existing code might be relying upon this bogus hack. So it
561            # will need thorough testing when removed.
562            try:
563                displayedText = obj.name
564            except (LookupError, RuntimeError):
565                pass
566
567        if not displayedText and role in [pyatspi.ROLE_PUSH_BUTTON, pyatspi.ROLE_LIST_ITEM]:
568            labels = self.unrelatedLabels(obj, minimumWords=1)
569            if not labels:
570                labels = self.unrelatedLabels(obj, onlyShowing=False, minimumWords=1)
571            displayedText = " ".join(map(self.displayedText, labels))
572
573        if self.DISPLAYED_TEXT not in self._script.generatorCache:
574            self._script.generatorCache[self.DISPLAYED_TEXT] = {}
575
576        self._script.generatorCache[self.DISPLAYED_TEXT][obj] = displayedText
577        return self._script.generatorCache[self.DISPLAYED_TEXT][obj]
578
579    def documentFrame(self, obj=None):
580        """Returns the document frame which is displaying the content.
581        Note that this is intended primarily for web content."""
582
583        if not obj:
584            obj, offset = self.getCaretContext()
585        docRoles = [pyatspi.ROLE_DOCUMENT_EMAIL,
586                    pyatspi.ROLE_DOCUMENT_FRAME,
587                    pyatspi.ROLE_DOCUMENT_PRESENTATION,
588                    pyatspi.ROLE_DOCUMENT_SPREADSHEET,
589                    pyatspi.ROLE_DOCUMENT_TEXT,
590                    pyatspi.ROLE_DOCUMENT_WEB]
591        stopRoles = [pyatspi.ROLE_FRAME, pyatspi.ROLE_SCROLL_PANE]
592        document = self.ancestorWithRole(obj, docRoles, stopRoles)
593        if not document and orca_state.locusOfFocus:
594            if orca_state.locusOfFocus.getRole() in docRoles:
595                return orca_state.locusOfFocus
596
597        return document
598
599    def documentFrameURI(self):
600        """Returns the URI of the document frame that is active."""
601
602        return None
603
604    @staticmethod
605    def focusedObject(root):
606        """Returns the accessible that has focus under or including the
607        given root.
608
609        TODO: This will currently traverse all children, whether they are
610        visible or not and/or whether they are children of parents that
611        manage their descendants.  At some point, this method should be
612        optimized to take such things into account.
613
614        Arguments:
615        - root: the root object where to start searching
616
617        Returns the object with the FOCUSED state or None if no object with
618        the FOCUSED state can be found.
619        """
620
621        if not root:
622            return None
623
624        if root.getState().contains(pyatspi.STATE_FOCUSED):
625            return root
626
627        for child in root:
628            try:
629                candidate = Utilities.focusedObject(child)
630                if candidate:
631                    return candidate
632            except:
633                pass
634
635        return None
636
637    def frameAndDialog(self, obj):
638        """Returns the frame and (possibly) the dialog containing obj."""
639
640        results = [None, None]
641
642        obj = obj or orca_state.locusOfFocus
643        if not obj:
644            msg = "ERROR: frameAndDialog() called without valid object"
645            debug.println(debug.LEVEL_INFO, msg, True)
646            return results
647
648        if obj.getRole() == pyatspi.ROLE_FRAME:
649            results[0] = obj
650
651        parent = obj.parent
652        while parent and (parent.parent != parent):
653            if parent.getRole() == pyatspi.ROLE_FRAME:
654                results[0] = parent
655            if parent.getRole() in [pyatspi.ROLE_DIALOG,
656                                    pyatspi.ROLE_FILE_CHOOSER]:
657                results[1] = parent
658            parent = parent.parent
659
660        return results
661
662    def presentEventFromNonShowingObject(self, event):
663        if event.source == orca_state.locusOfFocus:
664            return True
665
666        return False
667
668    def grabFocusBeforeRouting(self, obj, offset):
669        """Whether or not we should perform a grabFocus before routing
670        the cursor via the braille cursor routing keys.
671
672        Arguments:
673        - obj: the accessible object where the cursor should be routed
674        - offset: the offset to which it should be routed
675
676        Returns True if we should do an explicit grabFocus on obj prior
677        to routing the cursor.
678        """
679
680        if obj and obj.getRole() == pyatspi.ROLE_COMBO_BOX \
681           and not self.isSameObject(obj, orca_state.locusOfFocus):
682            return True
683
684        return False
685
686    def hasMatchingHierarchy(self, obj, rolesList):
687        """Called to determine if the given object and it's hierarchy of
688        parent objects, each have the desired roles. Please note: You
689        should strongly consider an alternative means for determining
690        that a given object is the desired item. Failing that, you should
691        include only enough of the hierarchy to make the determination.
692        If the developer of the application you are providing access to
693        does so much as add an Adjustment to reposition a widget, this
694        method can fail. You have been warned.
695
696        Arguments:
697        - obj: the accessible object to check.
698        - rolesList: the list of desired roles for the components and the
699          hierarchy of its parents.
700
701        Returns True if all roles match.
702        """
703
704        current = obj
705        for role in rolesList:
706            if current is None:
707                return False
708
709            if not isinstance(role, list):
710                role = [role]
711
712            try:
713                if isinstance(role[0], str):
714                    current_role = current.getRoleName()
715                else:
716                    current_role = current.getRole()
717            except:
718                current_role = None
719
720            if not current_role in role:
721                return False
722
723            current = self.validParent(current)
724
725        return True
726
727    def inFindContainer(self, obj=None):
728        if not obj:
729            obj = orca_state.locusOfFocus
730
731        try:
732            role = obj.getRole()
733        except:
734            return False
735
736        if role != pyatspi.ROLE_ENTRY:
737            return False
738
739        isToolbar = lambda x: x and x.getRole() == pyatspi.ROLE_TOOL_BAR
740        toolbar = pyatspi.findAncestor(obj, isToolbar)
741
742        return toolbar is not None
743
744    def getFindResultsCount(self, root=None):
745        return ""
746
747    def isAnchor(self, obj):
748        return False
749
750    def isCode(self, obj):
751        return False
752
753    def isCodeDescendant(self, obj):
754        return False
755
756    def isDesktop(self, obj):
757        try:
758            role = obj.getRole()
759        except:
760            msg = 'ERROR: Exception getting role of %s' % obj
761            debug.println(debug.LEVEL_INFO, msg, True)
762            return False
763
764        if role != pyatspi.ROLE_FRAME:
765            return False
766
767        attrs = self.objectAttributes(obj)
768        return attrs.get('is-desktop') == 'true'
769
770    def isComboBoxWithToggleDescendant(self, obj):
771        return False
772
773    def isToggleDescendantOfComboBox(self, obj):
774        return False
775
776    def isTypeahead(self, obj):
777        return False
778
779    def isOrDescendsFrom(self, obj, ancestor):
780        if obj == ancestor:
781            return True
782
783        return pyatspi.findAncestor(obj, lambda x: x and x == ancestor)
784
785    def isFunctionalDialog(self, obj):
786        """Returns True if the window is a functioning as a dialog.
787        This method should be subclassed by application scripts as
788        needed.
789        """
790
791        return False
792
793    def isComment(self, obj):
794        return False
795
796    def isContentDeletion(self, obj):
797        return False
798
799    def isContentError(self, obj):
800        return False
801
802    def isContentInsertion(self, obj):
803        return False
804
805    def isContentMarked(self, obj):
806        return False
807
808    def isContentSuggestion(self, obj):
809        return False
810
811    def isInlineSuggestion(self, obj):
812        return False
813
814    def isFirstItemInInlineContentSuggestion(self, obj):
815        return False
816
817    def isLastItemInInlineContentSuggestion(self, obj):
818        return False
819
820    def isEmpty(self, obj):
821        return False
822
823    def isHidden(self, obj):
824        return False
825
826    def isDPub(self, obj):
827        return False
828
829    def isDPubAbstract(self, obj):
830        return False
831
832    def isDPubAcknowledgments(self, obj):
833        return False
834
835    def isDPubAfterword(self, obj):
836        return False
837
838    def isDPubAppendix(self, obj):
839        return False
840
841    def isDPubBibliography(self, obj):
842        return False
843
844    def isDPubBacklink(self, obj):
845        return False
846
847    def isDPubBiblioref(self, obj):
848        return False
849
850    def isDPubChapter(self, obj):
851        return False
852
853    def isDPubColophon(self, obj):
854        return False
855
856    def isDPubConclusion(self, obj):
857        return False
858
859    def isDPubCover(self, obj):
860        return False
861
862    def isDPubCredit(self, obj):
863        return False
864
865    def isDPubCredits(self, obj):
866        return False
867
868    def isDPubDedication(self, obj):
869        return False
870
871    def isDPubEndnote(self, obj):
872        return False
873
874    def isDPubEndnotes(self, obj):
875        return False
876
877    def isDPubEpigraph(self, obj):
878        return False
879
880    def isDPubEpilogue(self, obj):
881        return False
882
883    def isDPubErrata(self, obj):
884        return False
885
886    def isDPubExample(self, obj):
887        return False
888
889    def isDPubFootnote(self, obj):
890        return False
891
892    def isDPubForeword(self, obj):
893        return False
894
895    def isDPubGlossary(self, obj):
896        return False
897
898    def isDPubGlossref(self, obj):
899        return False
900
901    def isDPubIndex(self, obj):
902        return False
903
904    def isDPubIntroduction(self, obj):
905        return False
906
907    def isDPubPagelist(self, obj):
908        return False
909
910    def isDPubPagebreak(self, obj):
911        return False
912
913    def isDPubPart(self, obj):
914        return False
915
916    def isDPubPreface(self, obj):
917        return False
918
919    def isDPubPrologue(self, obj):
920        return False
921
922    def isDPubPullquote(self, obj):
923        return False
924
925    def isDPubQna(self, obj):
926        return False
927
928    def isDPubSubtitle(self, obj):
929        return False
930
931    def isDPubToc(self, obj):
932        return False
933
934    def isFeed(self, obj):
935        return False
936
937    def isFigure(self, obj):
938        return False
939
940    def isGrid(self, obj):
941        return False
942
943    def isGridCell(self, obj):
944        return False
945
946    def supportsLandmarkRole(self):
947        return False
948
949    def isLandmark(self, obj):
950        return False
951
952    def isLandmarkWithoutType(self, obj):
953        return False
954
955    def isLandmarkBanner(self, obj):
956        return False
957
958    def isLandmarkComplementary(self, obj):
959        return False
960
961    def isLandmarkContentInfo(self, obj):
962        return False
963
964    def isLandmarkForm(self, obj):
965        return False
966
967    def isLandmarkMain(self, obj):
968        return False
969
970    def isLandmarkNavigation(self, obj):
971        return False
972
973    def isDPubNoteref(self, obj):
974        return False
975
976    def isLandmarkRegion(self, obj):
977        return False
978
979    def isLandmarkSearch(self, obj):
980        return False
981
982    def speakMathSymbolNames(self, obj=None):
983        return False
984
985    def isInMath(self):
986        return False
987
988    def isMath(self, obj):
989        return False
990
991    def isMathLayoutOnly(self, obj):
992        return False
993
994    def isMathMultiline(self, obj):
995        return False
996
997    def isMathEnclosed(self, obj):
998        return False
999
1000    def isMathFenced(self, obj):
1001        return False
1002
1003    def isMathFractionWithoutBar(self, obj):
1004        return False
1005
1006    def isMathPhantom(self, obj):
1007        return False
1008
1009    def isMathMultiScript(self, obj):
1010        return False
1011
1012    def isMathSubOrSuperScript(self, obj):
1013        return False
1014
1015    def isMathUnderOrOverScript(self, obj):
1016        return False
1017
1018    def isMathSquareRoot(self, obj):
1019        return False
1020
1021    def isMathTable(self, obj):
1022        return False
1023
1024    def isMathTableRow(self, obj):
1025        return False
1026
1027    def isMathTableCell(self, obj):
1028        return False
1029
1030    def isMathToken(self, obj):
1031        return False
1032
1033    def isMathTopLevel(self, obj):
1034        return False
1035
1036    def getMathDenominator(self, obj):
1037        return None
1038
1039    def getMathNumerator(self, obj):
1040        return None
1041
1042    def getMathRootBase(self, obj):
1043        return None
1044
1045    def getMathRootIndex(self, obj):
1046        return None
1047
1048    def getMathScriptBase(self, obj):
1049        return None
1050
1051    def getMathScriptSubscript(self, obj):
1052        return None
1053
1054    def getMathScriptSuperscript(self, obj):
1055        return None
1056
1057    def getMathScriptUnderscript(self, obj):
1058        return None
1059
1060    def getMathScriptOverscript(self, obj):
1061        return None
1062
1063    def getMathPrescripts(self, obj):
1064        return []
1065
1066    def getMathPostscripts(self, obj):
1067        return []
1068
1069    def getMathEnclosures(self, obj):
1070        return []
1071
1072    def getMathFencedSeparators(self, obj):
1073        return ['']
1074
1075    def getMathFences(self, obj):
1076        return ['', '']
1077
1078    def getMathNestingLevel(self, obj, test=None):
1079        return 0
1080
1081    def getLandmarkTypes(self):
1082        return ["banner",
1083                "complementary",
1084                "contentinfo",
1085                "doc-acknowledgments",
1086                "doc-afterword",
1087                "doc-appendix",
1088                "doc-bibliography",
1089                "doc-chapter",
1090                "doc-conclusion",
1091                "doc-credits",
1092                "doc-endnotes",
1093                "doc-epilogue",
1094                "doc-errata",
1095                "doc-foreword",
1096                "doc-glossary",
1097                "doc-index",
1098                "doc-introduction",
1099                "doc-pagelist",
1100                "doc-part",
1101                "doc-preface",
1102                "doc-prologue",
1103                "doc-toc",
1104                "form",
1105                "main",
1106                "navigation",
1107                "region",
1108                "search"]
1109
1110    def isProgressBar(self, obj):
1111        if not (obj and obj.getRole() == pyatspi.ROLE_PROGRESS_BAR):
1112            return False
1113
1114        try:
1115            value = obj.queryValue()
1116        except NotImplementedError:
1117            msg = "ERROR: %s doesn't implement AtspiValue" % obj
1118            debug.println(debug.LEVEL_INFO, msg, True)
1119            return False
1120        except:
1121            msg = "ERROR: Exception getting value for %s" % obj
1122            debug.println(debug.LEVEL_INFO, msg, True)
1123            return False
1124        else:
1125            try:
1126                if value.maximumValue == value.minimumValue:
1127                    msg = "INFO: %s is busy indicator" % obj
1128                    debug.println(debug.LEVEL_INFO, msg, True)
1129                    return False
1130            except:
1131                msg = "INFO: %s is either busy indicator or broken" % obj
1132                debug.println(debug.LEVEL_INFO, msg, True)
1133                return False
1134
1135        return True
1136
1137    def isProgressBarUpdate(self, obj, event):
1138        if not _settingsManager.getSetting('speakProgressBarUpdates') \
1139           and not _settingsManager.getSetting('brailleProgressBarUpdates') \
1140           and not _settingsManager.getSetting('beepProgressBarUpdates'):
1141            return False, "Updates not enabled"
1142
1143        if not self.isProgressBar(obj):
1144            return False, "Is not progress bar"
1145
1146        if self.hasNoSize(obj):
1147            return False, "Has no size"
1148
1149        if _settingsManager.getSetting('ignoreStatusBarProgressBars'):
1150            isStatusBar = lambda x: x and x.getRole() == pyatspi.ROLE_STATUS_BAR
1151            if pyatspi.findAncestor(obj, isStatusBar):
1152                return False, "Is status bar descendant"
1153
1154        verbosity = _settingsManager.getSetting('progressBarVerbosity')
1155        if verbosity == settings.PROGRESS_BAR_ALL:
1156            return True, "Verbosity is all"
1157
1158        if verbosity == settings.PROGRESS_BAR_WINDOW:
1159            topLevel = self.topLevelObject(obj)
1160            if topLevel == orca_state.activeWindow:
1161                return True, "Verbosity is window"
1162            return False, "Window %s is not %s" % (topLevel, orca_state.activeWindow)
1163
1164        if verbosity == settings.PROGRESS_BAR_APPLICATION:
1165            if event:
1166                app = event.host_application
1167            else:
1168                app = obj.getApplication()
1169            if app == orca_state.activeScript.app:
1170                return True, "Verbosity is app"
1171            return False, "App %s is not %s" % (app, orca_state.activeScript.app)
1172
1173        return True, "Not handled by any other case"
1174
1175    def getValueAsPercent(self, obj):
1176        try:
1177            value = obj.queryValue()
1178            minval, val, maxval =  value.minimumValue, value.currentValue, value.maximumValue
1179        except NotImplementedError:
1180            msg = "ERROR: %s doesn't implement AtspiValue" % obj
1181            debug.println(debug.LEVEL_INFO, msg, True)
1182            return None
1183        except:
1184            msg = "ERROR: Exception getting value for %s" % obj
1185            debug.println(debug.LEVEL_INFO, msg, True)
1186            return None
1187
1188        if obj.getState().contains(pyatspi.STATE_INDETERMINATE):
1189            msg = "INFO: %s has state indeterminate and value of %s" % (obj, val)
1190            debug.println(debug.LEVEL_INFO, msg, True)
1191            if val <= 0:
1192                return None
1193
1194        if maxval == minval == val:
1195            if 1 <= val <= 100:
1196                return int(val)
1197            return None
1198
1199        return int((val / (maxval - minval)) * 100)
1200
1201    def isBlockquote(self, obj):
1202        return obj and obj.getRole() == pyatspi.ROLE_BLOCK_QUOTE
1203
1204    def isDocumentList(self, obj):
1205        if not (obj and obj.getRole() == pyatspi.ROLE_LIST):
1206            return False
1207
1208        try:
1209            document = pyatspi.findAncestor(obj, self.isDocument)
1210        except:
1211            msg = "ERROR: Exception finding ancestor of %s" % obj
1212            debug.println(debug.LEVEL_INFO, msg)
1213            return False
1214
1215        return document is not None
1216
1217    def isDocumentPanel(self, obj):
1218        if not (obj and obj.getRole() == pyatspi.ROLE_PANEL):
1219            return False
1220
1221        try:
1222            document = pyatspi.findAncestor(obj, self.isDocument)
1223        except:
1224            msg = "ERROR: Exception finding ancestor of %s" % obj
1225            debug.println(debug.LEVEL_INFO, msg)
1226            return False
1227
1228        return document is not None
1229
1230    def isDocument(self, obj):
1231        documentRoles = [pyatspi.ROLE_DOCUMENT_EMAIL,
1232                         pyatspi.ROLE_DOCUMENT_FRAME,
1233                         pyatspi.ROLE_DOCUMENT_PRESENTATION,
1234                         pyatspi.ROLE_DOCUMENT_SPREADSHEET,
1235                         pyatspi.ROLE_DOCUMENT_TEXT,
1236                         pyatspi.ROLE_DOCUMENT_WEB]
1237        return obj and obj.getRole() in documentRoles
1238
1239    def inDocumentContent(self, obj=None):
1240        obj = obj or orca_state.locusOfFocus
1241        return self.getDocumentForObject(obj) is not None
1242
1243    def activeDocument(self, window=None):
1244        return self.getTopLevelDocumentForObject(orca_state.locusOfFocus)
1245
1246    def isTopLevelDocument(self, obj):
1247        return self.isDocument(obj) and not pyatspi.findAncestor(obj, self.isDocument)
1248
1249    def getTopLevelDocumentForObject(self, obj):
1250        if self.isTopLevelDocument(obj):
1251            return obj
1252
1253        return pyatspi.findAncestor(obj, self.isTopLevelDocument)
1254
1255    def getDocumentForObject(self, obj):
1256        if not obj:
1257            return None
1258
1259        if self.isDocument(obj):
1260            return obj
1261
1262        try:
1263            doc = pyatspi.findAncestor(obj, self.isDocument)
1264        except:
1265            msg = "ERROR: Exception finding ancestor of %s" % obj
1266            debug.println(debug.LEVEL_INFO, msg, True)
1267            return None
1268
1269        return doc
1270
1271    def getTable(self, obj):
1272        if not obj:
1273            return None
1274
1275        tableRoles = [pyatspi.ROLE_TABLE, pyatspi.ROLE_TREE_TABLE, pyatspi.ROLE_TREE]
1276        isTable = lambda x: x and x.getRole() in tableRoles and "Table" in pyatspi.listInterfaces(x)
1277        if isTable(obj):
1278            return obj
1279
1280        try:
1281            table = pyatspi.findAncestor(obj, isTable)
1282        except:
1283            msg = "ERROR: Exception finding ancestor of %s" % obj
1284            debug.println(debug.LEVEL_INFO, msg, True)
1285            return None
1286
1287        return table
1288
1289    def isTextDocumentTable(self, obj):
1290        if not (obj and obj.getRole() == pyatspi.ROLE_TABLE):
1291            return False
1292
1293        doc = self.getDocumentForObject(obj)
1294        if not doc:
1295            return False
1296
1297        return doc.getRole() != pyatspi.ROLE_DOCUMENT_SPREADSHEET
1298
1299    def isGUITable(self, obj):
1300        if not (obj and obj.getRole() == pyatspi.ROLE_TABLE):
1301            return False
1302
1303        return self.getDocumentForObject(obj) is None
1304
1305    def isSpreadSheetTable(self, obj):
1306        if not obj:
1307            return False
1308
1309        try:
1310            role = obj.getRole()
1311        except:
1312            msg = 'ERROR: Exception getting role of %s' % obj
1313            debug.println(debug.LEVEL_INFO, msg, True)
1314            return False
1315
1316        if not role == pyatspi.ROLE_TABLE:
1317            return False
1318
1319        doc = self.getDocumentForObject(obj)
1320        if not doc:
1321            return False
1322
1323        if doc.getRole() == pyatspi.ROLE_DOCUMENT_SPREADSHEET:
1324            return True
1325
1326        try:
1327            table = obj.queryTable()
1328        except NotImplementedError:
1329            msg = 'ERROR: Table %s does not implement table interface' % obj
1330            debug.println(debug.LEVEL_INFO, msg, True)
1331        except:
1332            msg = 'ERROR: Exception querying table interface of %s' % obj
1333            debug.println(debug.LEVEL_INFO, msg, True)
1334        else:
1335            return table.nRows > 65536
1336
1337        return False
1338
1339    def getCellRoles(self):
1340        return [pyatspi.ROLE_TABLE_CELL,
1341                pyatspi.ROLE_TABLE_COLUMN_HEADER,
1342                pyatspi.ROLE_TABLE_ROW_HEADER,
1343                pyatspi.ROLE_COLUMN_HEADER,
1344                pyatspi.ROLE_ROW_HEADER]
1345
1346    def isTextDocumentCell(self, obj):
1347        if not obj:
1348            return False
1349
1350        try:
1351            role = obj.getRole()
1352        except:
1353            msg = 'ERROR: Exception getting role of %s' % obj
1354            debug.println(debug.LEVEL_INFO, msg, True)
1355            return False
1356
1357        if not role in self.getCellRoles():
1358            return False
1359
1360        return pyatspi.findAncestor(obj, self.isTextDocumentTable)
1361
1362    def isSpreadSheetCell(self, obj):
1363        if not obj:
1364            return False
1365
1366        try:
1367            role = obj.getRole()
1368        except:
1369            msg = 'ERROR: Exception getting role of %s' % obj
1370            debug.println(debug.LEVEL_INFO, msg, True)
1371            return False
1372
1373        if not role in self.getCellRoles():
1374            return False
1375
1376        return pyatspi.findAncestor(obj, self.isSpreadSheetTable)
1377
1378    def cellColumnChanged(self, cell):
1379        row, column = self.coordinatesForCell(cell)
1380        if column == -1:
1381            return False
1382
1383        lastColumn = self._script.pointOfReference.get("lastColumn")
1384        return column != lastColumn
1385
1386    def cellRowChanged(self, cell):
1387        row, column = self.coordinatesForCell(cell)
1388        if row == -1:
1389            return False
1390
1391        lastRow = self._script.pointOfReference.get("lastRow")
1392        return row != lastRow
1393
1394    def shouldReadFullRow(self, obj):
1395        if self._script.inSayAll():
1396            return False
1397
1398        if not self.cellRowChanged(obj):
1399            return False
1400
1401        table = self.getTable(obj)
1402        if not table:
1403            return False
1404
1405        if not self.getDocumentForObject(table):
1406            return _settingsManager.getSetting('readFullRowInGUITable')
1407
1408        if self.isSpreadSheetTable(table):
1409            return _settingsManager.getSetting('readFullRowInSpreadSheet')
1410
1411        return _settingsManager.getSetting('readFullRowInDocumentTable')
1412
1413    def isSorted(self, obj):
1414        return False
1415
1416    def isAscending(self, obj):
1417        return False
1418
1419    def isDescending(self, obj):
1420        return False
1421
1422    def getSortOrderDescription(self, obj, includeName=False):
1423        if not (obj and self.isSorted(obj)):
1424            return ""
1425
1426        if self.isAscending(obj):
1427            result = object_properties.SORT_ORDER_ASCENDING
1428        elif self.isDescending(obj):
1429            result = object_properties.SORT_ORDER_DESCENDING
1430        else:
1431            result = object_properties.SORT_ORDER_OTHER
1432
1433        if includeName and obj.name:
1434            result = "%s. %s" % (obj.name, result)
1435
1436        return result
1437
1438    def isFocusableLabel(self, obj):
1439        try:
1440            role = obj.getRole()
1441            state = obj.getState()
1442        except:
1443            return False
1444
1445        if role != pyatspi.ROLE_LABEL:
1446            return False
1447
1448        if state.contains(pyatspi.STATE_FOCUSABLE):
1449            return True
1450
1451        if state.contains(pyatspi.STATE_FOCUSED):
1452            msg = 'INFO: %s is focused but lacks state focusable' % obj
1453            debug.println(debug.LEVEL_INFO, msg, True)
1454            return True
1455
1456        return False
1457
1458    def isNonFocusableList(self, obj):
1459        try:
1460            role = obj.getRole()
1461            state = obj.getState()
1462        except:
1463            return False
1464
1465        if role != pyatspi.ROLE_LIST:
1466            return False
1467
1468        if state.contains(pyatspi.STATE_FOCUSABLE):
1469            return False
1470
1471        if state.contains(pyatspi.STATE_FOCUSED):
1472            msg = 'INFO: %s is focused but lacks state focusable' % obj
1473            debug.println(debug.LEVEL_INFO, msg, True)
1474            return False
1475
1476        return True
1477
1478    def isStatusBarNotification(self, obj):
1479        if not (obj and obj.getRole() == pyatspi.ROLE_NOTIFICATION):
1480            return False
1481
1482        isStatusBar = lambda x: x and x.getRole() == pyatspi.ROLE_STATUS_BAR
1483        if pyatspi.findAncestor(obj, isStatusBar):
1484            return True
1485
1486        return False
1487
1488    def isTreeDescendant(self, obj):
1489        if not obj:
1490            return False
1491
1492        try:
1493            role = obj.getRole()
1494        except:
1495            return False
1496
1497        if role == pyatspi.ROLE_TREE_ITEM:
1498            return True
1499
1500        isTree = lambda x: x and x.getRole() in [pyatspi.ROLE_TREE, pyatspi.ROLE_TREE_TABLE]
1501        if pyatspi.findAncestor(obj, isTree):
1502            return True
1503
1504        return False
1505
1506    def isLayoutOnly(self, obj):
1507        """Returns True if the given object is a container which has
1508        no presentable information (label, name, displayed text, etc.)."""
1509
1510        layoutOnly = False
1511
1512        if self.isDead(obj) or self.isZombie(obj):
1513            return True
1514
1515        attrs = self.objectAttributes(obj)
1516
1517        try:
1518            role = obj.getRole()
1519        except:
1520            role = None
1521
1522        try:
1523            parentRole = obj.parent.getRole()
1524        except:
1525            parentRole = None
1526
1527        try:
1528            firstChild = obj[0]
1529        except:
1530            firstChild = None
1531
1532        topLevelRoles = self._topLevelRoles()
1533        ignorePanelParent = [pyatspi.ROLE_MENU,
1534                             pyatspi.ROLE_MENU_ITEM,
1535                             pyatspi.ROLE_LIST_ITEM,
1536                             pyatspi.ROLE_TREE_ITEM]
1537
1538        if role == pyatspi.ROLE_TABLE and attrs.get('layout-guess') != 'true':
1539            try:
1540                table = obj.queryTable()
1541            except NotImplementedError:
1542                msg = 'ERROR: Table %s does not implement table interface' % obj
1543                debug.println(debug.LEVEL_INFO, msg, True)
1544                layoutOnly = True
1545            except:
1546                msg = 'ERROR: Exception querying table interface of %s' % obj
1547                debug.println(debug.LEVEL_INFO, msg, True)
1548                layoutOnly = True
1549            else:
1550                if not (table.nRows and table.nColumns):
1551                    layoutOnly = not obj.getState().contains(pyatspi.STATE_FOCUSED)
1552                elif attrs.get('xml-roles') == 'table' or attrs.get('tag') == 'table':
1553                    layoutOnly = False
1554                elif not (obj.name or self.displayedLabel(obj)):
1555                    layoutOnly = not (table.getColumnHeader(0) or table.getRowHeader(0))
1556        elif role == pyatspi.ROLE_TABLE_CELL and obj.childCount:
1557            if parentRole == pyatspi.ROLE_TREE_TABLE:
1558                layoutOnly = False
1559            elif firstChild.getRole() == pyatspi.ROLE_TABLE_CELL:
1560                layoutOnly = True
1561            elif parentRole == pyatspi.ROLE_TABLE:
1562                layoutOnly = self.isLayoutOnly(obj.parent)
1563        elif role == pyatspi.ROLE_SECTION:
1564            layoutOnly = not self.isBlockquote(obj)
1565        elif role == pyatspi.ROLE_BLOCK_QUOTE:
1566            layoutOnly = False
1567        elif role == pyatspi.ROLE_FILLER:
1568            layoutOnly = True
1569        elif role == pyatspi.ROLE_SCROLL_PANE:
1570            layoutOnly = True
1571        elif role == pyatspi.ROLE_LAYERED_PANE:
1572            layoutOnly = self.isDesktop(self.topLevelObject(obj))
1573        elif role == pyatspi.ROLE_AUTOCOMPLETE:
1574            layoutOnly = True
1575        elif role in [pyatspi.ROLE_TEAROFF_MENU_ITEM, pyatspi.ROLE_SEPARATOR]:
1576            layoutOnly = True
1577        elif role in [pyatspi.ROLE_LIST_BOX, pyatspi.ROLE_TREE_TABLE]:
1578            layoutOnly = False
1579        elif role in topLevelRoles:
1580            layoutOnly = False
1581        elif role == pyatspi.ROLE_MENU:
1582            layoutOnly = parentRole == pyatspi.ROLE_COMBO_BOX
1583        elif role == pyatspi.ROLE_COMBO_BOX:
1584            layoutOnly = False
1585        elif role == pyatspi.ROLE_LIST:
1586            layoutOnly = False
1587        elif role == pyatspi.ROLE_FORM:
1588            layoutOnly = False
1589        elif role in [pyatspi.ROLE_PUSH_BUTTON, pyatspi.ROLE_TOGGLE_BUTTON]:
1590            layoutOnly = False
1591        elif role in [pyatspi.ROLE_TEXT, pyatspi.ROLE_PASSWORD_TEXT, pyatspi.ROLE_ENTRY]:
1592            layoutOnly = False
1593        elif role == pyatspi.ROLE_LIST_ITEM and parentRole == pyatspi.ROLE_LIST_BOX:
1594            layoutOnly = False
1595        elif role in [pyatspi.ROLE_REDUNDANT_OBJECT, pyatspi.ROLE_UNKNOWN]:
1596            layoutOnly = True
1597        elif self.isTableRow(obj):
1598            state = obj.getState()
1599            layoutOnly = not (state.contains(pyatspi.STATE_FOCUSABLE) \
1600                              or state.contains(pyatspi.STATE_SELECTABLE))
1601        elif role == pyatspi.ROLE_PANEL and obj.childCount and firstChild \
1602             and firstChild.getRole() in ignorePanelParent:
1603            layoutOnly = True
1604        elif role == pyatspi.ROLE_PANEL and obj.name == obj.getApplication().name:
1605            layoutOnly = True
1606        elif obj.childCount == 1 and obj.name and obj.name == firstChild.name:
1607            layoutOnly = True
1608        elif self.isHidden(obj):
1609            layoutOnly = True
1610        else:
1611            if not (self.displayedText(obj) or self.displayedLabel(obj)):
1612                layoutOnly = True
1613
1614        if layoutOnly:
1615            msg = 'INFO: %s is deemed to be layout only' % obj
1616            debug.println(debug.LEVEL_INFO, msg, True)
1617
1618        return layoutOnly
1619
1620    @staticmethod
1621    def isInActiveApp(obj):
1622        """Returns True if the given object is from the same application that
1623        currently has keyboard focus.
1624
1625        Arguments:
1626        - obj: an Accessible object
1627        """
1628
1629        if not obj or not orca_state.locusOfFocus:
1630            return False
1631
1632        return orca_state.locusOfFocus.getApplication() == obj.getApplication()
1633
1634    def isLink(self, obj):
1635        """Returns True if obj is a link."""
1636
1637        if not obj:
1638            return False
1639
1640        try:
1641            role = obj.getRole()
1642        except (LookupError, RuntimeError):
1643            msg = 'ERROR: Exception getting role for %s' % obj
1644            debug.println(debug.LEVEL_INFO, msg, True)
1645            return False
1646
1647        return role == pyatspi.ROLE_LINK
1648
1649    def isReadOnlyTextArea(self, obj):
1650        """Returns True if obj is a text entry area that is read only."""
1651
1652        if not self.isTextArea(obj):
1653            return False
1654
1655        state = obj.getState()
1656        readOnly = state.contains(pyatspi.STATE_FOCUSABLE) \
1657                   and not state.contains(pyatspi.STATE_EDITABLE)
1658        return readOnly
1659
1660    def isSwitch(self, obj):
1661        return False
1662
1663    def _hasSamePath(self, obj1, obj2):
1664        path1 = pyatspi.utils.getPath(obj1)
1665        path2 = pyatspi.utils.getPath(obj2)
1666        if len(path1) != len(path2):
1667            return False
1668
1669        # The first item in all paths, even valid ones, is -1.
1670        path1 = path1[1:]
1671        path2 = path2[1:]
1672
1673        # If both have invalid child indices, all bets are off.
1674        if path1.count(-1) and path2.count(-1):
1675            return False
1676
1677        try:
1678            index = path1.index(-1)
1679        except ValueError:
1680            try:
1681                index = path2.index(-1)
1682            except ValueError:
1683                index = len(path2)
1684
1685        return path1[0:index] == path2[0:index]
1686
1687    def isSameObject(self, obj1, obj2, comparePaths=False, ignoreNames=False):
1688        if (obj1 == obj2):
1689            return True
1690        elif (not obj1) or (not obj2):
1691            return False
1692
1693        try:
1694            if obj1.getRole() != obj2.getRole():
1695                return False
1696            if obj1.name != obj2.name and not ignoreNames:
1697                return False
1698            if comparePaths and self._hasSamePath(obj1, obj2):
1699                return True
1700            else:
1701                # Comparing the extents of objects which claim to be different
1702                # addresses both managed descendants and implementations which
1703                # recreate accessibles for the same widget.
1704                extents1 = \
1705                    obj1.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
1706                extents2 = \
1707                    obj2.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
1708
1709                # Objects which claim to be different and which are in different
1710                # locations are almost certainly not recreated objects.
1711                if extents1 != extents2:
1712                    return False
1713
1714                # Objects which claim to have the same role, the same name, and
1715                # the same size and position are highly likely to be the same
1716                # functional object -- if they have valid, on-screen extents.
1717                if extents1.x >= 0 and extents1.y >= 0 and extents1.width > 0 \
1718                   and extents1.height > 0:
1719                    return True
1720        except:
1721            pass
1722
1723        return False
1724
1725    def isTextArea(self, obj):
1726        """Returns True if obj is a GUI component that is for entering text.
1727
1728        Arguments:
1729        - obj: an accessible
1730        """
1731
1732        if self.isLink(obj):
1733            return False
1734
1735        return obj and obj.getRole() in (pyatspi.ROLE_TEXT,
1736                                         pyatspi.ROLE_ENTRY,
1737                                         pyatspi.ROLE_PARAGRAPH)
1738
1739    @staticmethod
1740    def knownApplications():
1741        """Retrieves the list of currently running apps for the desktop
1742        as a list of Accessible objects.
1743        """
1744
1745        return [x for x in Utilities._desktop if x is not None]
1746
1747    def labelsForObject(self, obj):
1748        """Return a list of the labels for this object."""
1749
1750        try:
1751            relations = obj.getRelationSet()
1752        except (LookupError, RuntimeError):
1753            msg = 'ERROR: Exception getting relationset for %s' % obj
1754            debug.println(debug.LEVEL_INFO, msg, True)
1755            return []
1756
1757        pred = lambda r: r.getRelationType() == pyatspi.RELATION_LABELLED_BY
1758        relations = list(filter(pred, obj.getRelationSet()))
1759        if not relations:
1760            return []
1761
1762        r = relations[0]
1763        result = set([r.getTarget(i) for i in range(r.getNTargets())])
1764        if obj in result:
1765            msg = 'WARNING: %s claims to be labelled by itself' % obj
1766            debug.println(debug.LEVEL_INFO, msg, True)
1767            result.remove(obj)
1768
1769        def isNotAncestor(acc):
1770            return not pyatspi.findAncestor(obj, lambda x: x == acc)
1771
1772        return list(filter(isNotAncestor, result))
1773
1774    def linkBasenameToName(self, obj):
1775        basename = self.linkBasename(obj)
1776        if not basename:
1777            return ""
1778
1779        basename = re.sub(r"[-_]", " ", basename)
1780        tokens = basename.split()
1781        for token in tokens:
1782            if not token.isalpha():
1783                return ""
1784
1785        return basename
1786
1787    @staticmethod
1788    def linkBasename(obj):
1789        """Returns the relevant information from the URI.  The idea is
1790        to attempt to strip off all prefix and suffix, much like the
1791        basename command in a shell."""
1792
1793        basename = None
1794
1795        try:
1796            hyperlink = obj.queryHyperlink()
1797        except:
1798            pass
1799        else:
1800            uri = hyperlink.getURI(0)
1801            if uri and len(uri):
1802                # Sometimes the URI is an expression that includes a URL.
1803                # Currently that can be found at the bottom of safeway.com.
1804                # It can also be seen in the backwards.html test file.
1805                #
1806                expression = uri.split(',')
1807                if len(expression) > 1:
1808                    for item in expression:
1809                        if item.find('://') >=0:
1810                            if not item[0].isalnum():
1811                                item = item[1:-1]
1812                            if not item[-1].isalnum():
1813                                item = item[0:-2]
1814                            uri = item
1815                            break
1816
1817                # We're assuming that there IS a base name to be had.
1818                # What if there's not? See backwards.html.
1819                #
1820                uri = uri.split('://')[-1]
1821                if not uri:
1822                    return basename
1823
1824                # Get the last thing after all the /'s, unless it ends
1825                # in a /.  If it ends in a /, we'll look to the stuff
1826                # before the ending /.
1827                #
1828                if uri[-1] == "/":
1829                    basename = uri[0:-1]
1830                    basename = basename.split('/')[-1]
1831                elif not uri.count("/"):
1832                    basename = uri
1833                else:
1834                    basename = uri.split('/')[-1]
1835                    if basename.startswith("index"):
1836                        basename = uri.split('/')[-2]
1837
1838                    # Now, try to strip off the suffixes.
1839                    #
1840                    basename = basename.split('.')[0]
1841                    basename = basename.split('?')[0]
1842                    basename = basename.split('#')[0]
1843                    basename = basename.split('%')[0]
1844
1845        return basename
1846
1847    @staticmethod
1848    def linkIndex(obj, characterIndex):
1849        """A brute force method to see if an offset is a link.  This
1850        is provided because not all Accessible Hypertext implementations
1851        properly support the getLinkIndex method.  Returns an index of
1852        0 or greater of the characterIndex is on a hyperlink.
1853
1854        Arguments:
1855        -obj: the object with the Accessible Hypertext specialization
1856        -characterIndex: the text position to check
1857        """
1858
1859        if not obj:
1860            return -1
1861
1862        try:
1863            obj.queryText()
1864        except NotImplementedError:
1865            return -1
1866
1867        try:
1868            hypertext = obj.queryHypertext()
1869        except NotImplementedError:
1870            return -1
1871
1872        for i in range(hypertext.getNLinks()):
1873            link = hypertext.getLink(i)
1874            if (characterIndex >= link.startIndex) \
1875               and (characterIndex <= link.endIndex):
1876                return i
1877
1878        return -1
1879
1880    def nestingLevel(self, obj):
1881        """Determines the nesting level of this object.
1882
1883        Arguments:
1884        -obj: the Accessible object
1885        """
1886
1887        if not obj:
1888            return 0
1889
1890        try:
1891            return self._script.generatorCache[self.NESTING_LEVEL][obj]
1892        except:
1893            if self.NESTING_LEVEL not in self._script.generatorCache:
1894                self._script.generatorCache[self.NESTING_LEVEL] = {}
1895
1896        if self.isBlockquote(obj):
1897            pred = lambda x: self.isBlockquote(x)
1898        elif obj.getRole() == pyatspi.ROLE_LIST_ITEM:
1899            pred = lambda x: x and x.parent and x.parent.getRole() == pyatspi.ROLE_LIST
1900        else:
1901            role = obj.getRole()
1902            pred = lambda x: x and x.getRole() == role
1903
1904        ancestors = []
1905        ancestor = pyatspi.findAncestor(obj, pred)
1906        while ancestor:
1907            ancestors.append(ancestor)
1908            ancestor = pyatspi.findAncestor(ancestor, pred)
1909
1910        nestingLevel = len(ancestors)
1911        self._script.generatorCache[self.NESTING_LEVEL][obj] = nestingLevel
1912        return self._script.generatorCache[self.NESTING_LEVEL][obj]
1913
1914    def nodeLevel(self, obj):
1915        """Determines the node level of this object if it is in a tree
1916        relation, with 0 being the top level node.  If this object is
1917        not in a tree relation, then -1 will be returned.
1918
1919        Arguments:
1920        -obj: the Accessible object
1921        """
1922
1923        if not self.isTreeDescendant(obj):
1924            return -1
1925
1926        try:
1927            return self._script.generatorCache[self.NODE_LEVEL][obj]
1928        except:
1929            if self.NODE_LEVEL not in self._script.generatorCache:
1930                self._script.generatorCache[self.NODE_LEVEL] = {}
1931
1932        nodes = []
1933        node = obj
1934        done = False
1935        while not done:
1936            try:
1937                relations = node.getRelationSet()
1938            except (LookupError, RuntimeError):
1939                msg = 'ERROR: Exception getting relationset for %s' % node
1940                debug.println(debug.LEVEL_INFO, msg, True)
1941                return -1
1942            node = None
1943            for relation in relations:
1944                if relation.getRelationType() \
1945                       == pyatspi.RELATION_NODE_CHILD_OF:
1946                    node = relation.getTarget(0)
1947                    break
1948
1949            # We want to avoid situations where something gives us an
1950            # infinite cycle of nodes.  Bon Echo has been seen to do
1951            # this (see bug 351847).
1952            if nodes.count(node):
1953                msg = 'ERROR: %s is already in the list of nodes for %s' % (node, obj)
1954                debug.println(debug.LEVEL_INFO, msg, True)
1955                done = True
1956            if len(nodes) > 100:
1957                msg = 'INFO: More than 100 nodes found for %s' % obj
1958                debug.println(debug.LEVEL_INFO, msg, True)
1959                done = True
1960            elif node:
1961                nodes.append(node)
1962            else:
1963                done = True
1964
1965        self._script.generatorCache[self.NODE_LEVEL][obj] = len(nodes) - 1
1966        return self._script.generatorCache[self.NODE_LEVEL][obj]
1967
1968    def isOnScreen(self, obj, boundingbox=None):
1969        if self.isDead(obj):
1970            return False
1971
1972        if self.isHidden(obj):
1973            return False
1974
1975        if not self.isShowingAndVisible(obj):
1976            msg = "INFO: %s is not showing and visible" % obj
1977            debug.println(debug.LEVEL_INFO, msg, True)
1978            return False
1979
1980        try:
1981            box = obj.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
1982        except:
1983            msg = "ERROR: Exception getting extents for %s" % obj
1984            debug.println(debug.LEVEL_INFO, msg, True)
1985            return False
1986
1987        msg = "INFO: Extents for %s are: %s" % (obj, box)
1988        debug.println(debug.LEVEL_INFO, msg, True)
1989
1990        if box.x > 10000 or box.y > 10000:
1991            msg = "INFO: %s seems to have bogus coordinates" % obj
1992            debug.println(debug.LEVEL_INFO, msg, True)
1993            return False
1994
1995        if box.x < 0 and box.y < 0 and tuple(box) != (-1, -1, -1, -1):
1996            msg = "INFO: %s has negative coordinates" % obj
1997            debug.println(debug.LEVEL_INFO, msg, True)
1998            return False
1999
2000        if not (box.width or box.height):
2001            if not obj.childCount:
2002                msg = "INFO: %s has no size and no children" % obj
2003                debug.println(debug.LEVEL_INFO, msg, True)
2004                return False
2005            if obj.getRole() == pyatspi.ROLE_MENU:
2006                msg = "INFO: %s has no size" % obj
2007                debug.println(debug.LEVEL_INFO, msg, True)
2008                return False
2009
2010            return True
2011
2012        if boundingbox is None or not self._boundsIncludeChildren(obj.parent):
2013            return True
2014
2015        if not self.containsRegion(box, boundingbox) and tuple(box) != (-1, -1, -1, -1):
2016            msg = "INFO: %s %s not in %s" % (obj, box, boundingbox)
2017            debug.println(debug.LEVEL_INFO, msg, True)
2018            return False
2019
2020        return True
2021
2022    def selectedMenuBarMenu(self, menubar):
2023        try:
2024            role = menubar.getRole()
2025        except:
2026            msg = "ERROR: Exception getting role of %s" % menubar
2027            debug.println(debug.LEVEL_INFO, msg, True)
2028            return None
2029
2030        if role != pyatspi.ROLE_MENU_BAR:
2031            return None
2032
2033        if "Selection" in pyatspi.listInterfaces(menubar):
2034            selected = self.selectedChildren(menubar)
2035            if selected:
2036                return selected[0]
2037            return None
2038
2039        for menu in menubar:
2040            try:
2041                menu.clearCache()
2042                state = menu.getState()
2043            except:
2044                msg = "ERROR: Exception getting state of %s" % menu
2045                debug.println(debug.LEVEL_INFO, msg, True)
2046                continue
2047
2048            if state.contains(pyatspi.STATE_EXPANDED) \
2049               or state.contains(pyatspi.STATE_SELECTED):
2050                return menu
2051
2052        return None
2053
2054    def isInOpenMenuBarMenu(self, obj):
2055        if not obj:
2056            return False
2057
2058        isMenuBar = lambda x: x and x.getRole() == pyatspi.ROLE_MENU_BAR
2059        menubar = pyatspi.findAncestor(obj, isMenuBar)
2060        if menubar is None:
2061            return False
2062
2063        selectedMenu = self._selectedMenuBarMenu.get(hash(menubar))
2064        if selectedMenu is None:
2065            selectedMenu = self.selectedMenuBarMenu(menubar)
2066
2067        if not selectedMenu:
2068            return False
2069
2070        inSelectedMenu = lambda x: x == selectedMenu
2071        if inSelectedMenu(obj):
2072            return True
2073
2074        return pyatspi.findAncestor(obj, inSelectedMenu) is not None
2075
2076    def isStaticTextLeaf(self, obj):
2077        return False
2078
2079    def isListItemMarker(self, obj):
2080        return False
2081
2082    def hasPresentableText(self, obj):
2083        if self.isStaticTextLeaf(obj):
2084            return False
2085
2086        text = self.queryNonEmptyText(obj)
2087        if not text:
2088            return False
2089
2090        return bool(re.search(r"\w+", text.getText(0, -1)))
2091
2092    def getOnScreenObjects(self, root, extents=None):
2093        if not self.isOnScreen(root, extents):
2094            return []
2095
2096        try:
2097            role = root.getRole()
2098        except:
2099            msg = "ERROR: Exception getting role of %s" % root
2100            debug.println(debug.LEVEL_INFO, msg, True)
2101            return []
2102
2103        if role == pyatspi.ROLE_INVALID:
2104            return []
2105
2106        if role == pyatspi.ROLE_COMBO_BOX:
2107            return [root]
2108
2109        if role == pyatspi.ROLE_PUSH_BUTTON:
2110            return [root]
2111
2112        if role == pyatspi.ROLE_TOGGLE_BUTTON:
2113            return [root]
2114
2115        if role == pyatspi.ROLE_MENU_BAR:
2116            self._selectedMenuBarMenu[hash(root)] = self.selectedMenuBarMenu(root)
2117
2118        if root.parent and root.parent.getRole() == pyatspi.ROLE_MENU_BAR \
2119           and not self.isInOpenMenuBarMenu(root):
2120            return [root]
2121
2122        if role == pyatspi.ROLE_FILLER and not root.childCount:
2123            msg = "INFO: %s is empty filler. Clearing cache." % root
2124            debug.println(debug.LEVEL_INFO, msg, True)
2125            root.clearCache()
2126            msg = "INFO: %s reports %i children" % (root, root.childCount)
2127            debug.println(debug.LEVEL_INFO, msg, True)
2128
2129        if extents is None:
2130            try:
2131                component = root.queryComponent()
2132                extents = component.getExtents(pyatspi.DESKTOP_COORDS)
2133            except:
2134                msg = "ERROR: Exception getting extents of %s" % root
2135                debug.println(debug.LEVEL_INFO, msg, True)
2136                extents = 0, 0, 0, 0
2137
2138        interfaces = pyatspi.listInterfaces(root)
2139        if 'Table' in interfaces and 'Selection' in interfaces:
2140            visibleCells = self.getVisibleTableCells(root)
2141            if visibleCells:
2142                return visibleCells
2143
2144        objects = []
2145        hasNameOrDescription = (root.name or root.description)
2146        if role in [pyatspi.ROLE_PAGE_TAB, pyatspi.ROLE_IMAGE] and hasNameOrDescription:
2147            objects.append(root)
2148        elif self.hasPresentableText(root):
2149            objects.append(root)
2150
2151        for child in root:
2152            if not self.isStaticTextLeaf(child):
2153                objects.extend(self.getOnScreenObjects(child, extents))
2154
2155        if role == pyatspi.ROLE_MENU_BAR:
2156            self._selectedMenuBarMenu[hash(root)] = None
2157
2158        if objects:
2159            return objects
2160
2161        if role == pyatspi.ROLE_LABEL and not (root.name or self.queryNonEmptyText(root)):
2162            return []
2163
2164        containers = [pyatspi.ROLE_CANVAS,
2165                      pyatspi.ROLE_FILLER,
2166                      pyatspi.ROLE_IMAGE,
2167                      pyatspi.ROLE_LINK,
2168                      pyatspi.ROLE_LIST_BOX,
2169                      pyatspi.ROLE_PANEL,
2170                      pyatspi.ROLE_SECTION,
2171                      pyatspi.ROLE_SCROLL_PANE,
2172                      pyatspi.ROLE_VIEWPORT]
2173        if role in containers and not hasNameOrDescription:
2174            return []
2175
2176        return [root]
2177
2178    @staticmethod
2179    def isTableRow(obj):
2180        """Determines if obj is a table row -- real or functionally."""
2181
2182        try:
2183            if not (obj and obj.parent and obj.childCount):
2184                return False
2185        except:
2186            msg = "ERROR: Exception getting parent and childCount for %s" % obj
2187            debug.println(debug.LEVEL_INFO, msg, True)
2188            return False
2189
2190        role = obj.getRole()
2191        if role == pyatspi.ROLE_TABLE_ROW:
2192            return True
2193
2194        if role == pyatspi.ROLE_TABLE_CELL:
2195            return False
2196
2197        if not obj.parent.getRole() == pyatspi.ROLE_TABLE:
2198            return False
2199
2200        isCell = lambda x: x and x.getRole() in [pyatspi.ROLE_TABLE_CELL,
2201                                                 pyatspi.ROLE_TABLE_COLUMN_HEADER,
2202                                                 pyatspi.ROLE_TABLE_ROW_HEADER,
2203                                                 pyatspi.ROLE_ROW_HEADER,
2204                                                 pyatspi.ROLE_COLUMN_HEADER]
2205        cellChildren = list(filter(isCell, [x for x in obj]))
2206        if len(cellChildren) == obj.childCount:
2207            return True
2208
2209        return False
2210
2211    def realActiveAncestor(self, obj):
2212        if obj.getState().contains(pyatspi.STATE_FOCUSED):
2213            return obj
2214
2215        roles = [pyatspi.ROLE_TABLE_CELL,
2216                 pyatspi.ROLE_TABLE_COLUMN_HEADER,
2217                 pyatspi.ROLE_TABLE_ROW_HEADER,
2218                 pyatspi.ROLE_COLUMN_HEADER,
2219                 pyatspi.ROLE_ROW_HEADER,
2220                 pyatspi.ROLE_LIST_ITEM]
2221
2222        ancestor = pyatspi.findAncestor(obj, lambda x: x and x.getRole() in roles)
2223        if ancestor and not self._script.utilities.isLayoutOnly(ancestor.parent):
2224            obj = ancestor
2225
2226        return obj
2227
2228    def realActiveDescendant(self, obj):
2229        """Given an object that should be a child of an object that
2230        manages its descendants, return the child that is the real
2231        active descendant carrying useful information.
2232
2233        Arguments:
2234        - obj: an object that should be a child of an object that
2235        manages its descendants.
2236        """
2237
2238        if self.isDead(obj):
2239            return None
2240
2241        if obj.getRole() != pyatspi.ROLE_TABLE_CELL:
2242            return obj
2243
2244        children = [x for x in obj if not self.isStaticTextLeaf(x)]
2245        hasContent = [x for x in children if self.displayedText(x).strip()]
2246        if len(hasContent) == 1:
2247            return hasContent[0]
2248
2249        return obj
2250
2251    def isStatusBarDescendant(self, obj):
2252        if not obj:
2253            return False
2254
2255        isStatusBar = lambda x: x and x.getRole() == pyatspi.ROLE_STATUS_BAR
2256        return pyatspi.findAncestor(obj, isStatusBar) is not None
2257
2258    def statusBarItems(self, obj):
2259        if not (obj and obj.getRole() == pyatspi.ROLE_STATUS_BAR):
2260            return []
2261
2262        start = time.time()
2263        items = self._script.pointOfReference.get('statusBarItems')
2264        if not items:
2265            include = lambda x: x and x.getRole() != pyatspi.ROLE_STATUS_BAR
2266            items = list(filter(include, self.getOnScreenObjects(obj)))
2267            self._script.pointOfReference['statusBarItems'] = items
2268
2269        end = time.time()
2270        msg = "INFO: Time getting status bar items: %.4f" % (end - start)
2271        debug.println(debug.LEVEL_INFO, msg, True)
2272
2273        return items
2274
2275    def statusBar(self, obj):
2276        """Returns the status bar in the window which contains obj.
2277
2278        Arguments:
2279        - obj: the top-level object (e.g. window, frame, dialog) for which
2280          the status bar is sought.
2281        """
2282
2283        if obj.getRole() == pyatspi.ROLE_STATUS_BAR:
2284            return obj
2285
2286        # There are some objects which are not worth descending.
2287        #
2288        skipRoles = [pyatspi.ROLE_TREE,
2289                     pyatspi.ROLE_TREE_TABLE,
2290                     pyatspi.ROLE_TABLE]
2291
2292        if obj.getState().contains(pyatspi.STATE_MANAGES_DESCENDANTS) \
2293           or obj.getRole() in skipRoles:
2294            return
2295
2296        statusBar = None
2297        # The status bar is likely near the bottom of the window.
2298        #
2299        for i in range(obj.childCount - 1, -1, -1):
2300            if obj[i].getRole() == pyatspi.ROLE_STATUS_BAR:
2301                statusBar = obj[i]
2302            elif not obj[i].getRole() in skipRoles:
2303                statusBar = self.statusBar(obj[i])
2304
2305            if statusBar and self.isShowingAndVisible(statusBar):
2306                break
2307
2308        return statusBar
2309
2310    def infoBar(self, root):
2311        return None
2312
2313    def _topLevelRoles(self):
2314        return [pyatspi.ROLE_ALERT,
2315                pyatspi.ROLE_DIALOG,
2316                pyatspi.ROLE_FRAME,
2317                pyatspi.ROLE_WINDOW]
2318
2319    def _locusOfFocusIsTopLevelObject(self):
2320        if not orca_state.locusOfFocus:
2321            return False
2322
2323        try:
2324            role = orca_state.locusOfFocus.getRole()
2325        except:
2326            msg = "ERROR: Exception getting role for %s" % orca_state.locusOfFocus
2327            debug.println(debug.LEVEL_INFO, msg, True)
2328            return False
2329
2330        rv = role in self._topLevelRoles()
2331        msg = "INFO: %s is top-level object: %s" % (orca_state.locusOfFocus, rv)
2332        debug.println(debug.LEVEL_INFO, msg, True)
2333
2334        return rv
2335
2336    def topLevelObject(self, obj):
2337        """Returns the top-level object (frame, dialog ...) containing obj,
2338        or None if obj is not inside a top-level object.
2339
2340        Arguments:
2341        - obj: the Accessible object
2342        """
2343
2344        if not obj:
2345            return None
2346
2347        stopAtRoles = self._topLevelRoles()
2348
2349        while obj and obj.parent and obj != obj.parent \
2350              and not obj.getRole() in stopAtRoles \
2351              and not obj.parent.getRole() == pyatspi.ROLE_APPLICATION:
2352            obj = obj.parent
2353
2354        return obj
2355
2356    def topLevelObjectIsActiveAndCurrent(self, obj=None):
2357        obj = obj or orca_state.locusOfFocus
2358
2359        topLevel = self.topLevelObject(obj)
2360        if not topLevel:
2361            return False
2362
2363        topLevel.clearCache()
2364        try:
2365            state = topLevel.getState()
2366        except:
2367            msg = "ERROR: Exception getting state of topLevel %s" % topLevel
2368            debug.println(debug.LEVEL_INFO, msg, True)
2369            return False
2370
2371        if not state.contains(pyatspi.STATE_ACTIVE) \
2372           or state.contains(pyatspi.STATE_DEFUNCT):
2373            return False
2374
2375        if not self.isSameObject(topLevel, orca_state.activeWindow):
2376            return False
2377
2378        return True
2379
2380    @staticmethod
2381    def onSameLine(obj1, obj2, delta=0):
2382        """Determines if obj1 and obj2 are on the same line."""
2383
2384        try:
2385            bbox1 = obj1.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2386            bbox2 = obj2.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2387        except:
2388            return False
2389
2390        center1 = bbox1.y + bbox1.height / 2
2391        center2 = bbox2.y + bbox2.height / 2
2392
2393        return abs(center1 - center2) <= delta
2394
2395    @staticmethod
2396    def pathComparison(path1, path2):
2397        """Compares the two paths and returns -1, 0, or 1 to indicate if path1
2398        is before, the same, or after path2."""
2399
2400        if path1 == path2:
2401            return 0
2402
2403        size = max(len(path1), len(path2))
2404        path1 = (path1 + [-1] * size)[:size]
2405        path2 = (path2 + [-1] * size)[:size]
2406
2407        for x in range(min(len(path1), len(path2))):
2408            if path1[x] < path2[x]:
2409                return -1
2410            if path1[x] > path2[x]:
2411                return 1
2412
2413        return 0
2414
2415    @staticmethod
2416    def sizeComparison(obj1, obj2):
2417        try:
2418            bbox = obj1.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2419            width1, height1 = bbox.width, bbox.height
2420        except:
2421            width1, height1 = 0, 0
2422
2423        try:
2424            bbox = obj2.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2425            width2, height2 = bbox.width, bbox.height
2426        except:
2427            width2, height2 = 0, 0
2428
2429        return (width1 * height1) - (width2 * height2)
2430
2431    @staticmethod
2432    def spatialComparison(obj1, obj2):
2433        """Compares the physical locations of obj1 and obj2 and returns -1,
2434        0, or 1 to indicate if obj1 physically is before, is in the same
2435        place as, or is after obj2."""
2436
2437        try:
2438            bbox = obj1.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2439            x1, y1 = bbox.x, bbox.y
2440        except:
2441            x1, y1 = 0, 0
2442
2443        try:
2444            bbox = obj2.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2445            x2, y2 = bbox.x, bbox.y
2446        except:
2447            x2, y2 = 0, 0
2448
2449        rv = y1 - y2 or x1 - x2
2450
2451        # If the objects claim to have the same coordinates, there is either
2452        # a horrible design crime or we've been given bogus extents. Fall back
2453        # on the index in the parent. This is seen with GtkListBox items which
2454        # had been scrolled off-screen.
2455        if not rv and obj1.parent == obj2.parent:
2456            rv = obj1.getIndexInParent() - obj2.getIndexInParent()
2457
2458        rv = max(rv, -1)
2459        rv = min(rv, 1)
2460
2461        return rv
2462
2463    def getTextBoundingBox(self, obj, start, end):
2464        try:
2465            extents = obj.queryText().getRangeExtents(start, end, pyatspi.DESKTOP_COORDS)
2466        except:
2467            msg = "ERROR: Exception getting range extents of %s" % obj
2468            debug.println(debug.LEVEL_INFO, msg, True)
2469            return -1, -1, 0, 0
2470
2471        return extents
2472
2473    def getBoundingBox(self, obj):
2474        try:
2475            extents = obj.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2476        except:
2477            msg = "ERROR: Exception getting extents of %s" % obj
2478            debug.println(debug.LEVEL_INFO, msg, True)
2479            return -1, -1, 0, 0
2480
2481        return extents.x, extents.y, extents.width, extents.height
2482
2483    def hasNoSize(self, obj):
2484        if not obj:
2485            return False
2486
2487        if obj.getRole() == pyatspi.ROLE_APPLICATION:
2488            return False
2489
2490        try:
2491            extents = obj.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
2492        except:
2493            msg = "ERROR: Exception getting extents for %s" % obj
2494            debug.println(debug.LEVEL_INFO, msg, True)
2495            return True
2496
2497        return not (extents.width and extents.height)
2498
2499    def _findAllDescendants(self, root, includeIf, excludeIf, matches):
2500        if not root:
2501            return
2502
2503        try:
2504            childCount = root.childCount
2505        except:
2506            msg = "ERROR: Exception getting childCount for %s" % root
2507            debug.println(debug.LEVEL_INFO, msg, True)
2508            return
2509
2510        for i in range(childCount):
2511            try:
2512                child = root[i]
2513            except:
2514                msg = "ERROR: Exception getting %i child for %s" % (i, root)
2515                debug.println(debug.LEVEL_INFO, msg, True)
2516                return
2517
2518            if excludeIf and excludeIf(child):
2519                continue
2520            if includeIf and includeIf(child):
2521                matches.append(child)
2522            self._findAllDescendants(child, includeIf, excludeIf, matches)
2523
2524    def findAllDescendants(self, root, includeIf=None, excludeIf=None):
2525        matches = []
2526        self._findAllDescendants(root, includeIf, excludeIf, matches)
2527        return matches
2528
2529    def unrelatedLabels(self, root, onlyShowing=True, minimumWords=3):
2530        """Returns a list containing all the unrelated (i.e., have no
2531        relations to anything and are not a fundamental element of a
2532        more atomic component like a combo box) labels under the given
2533        root.  Note that the labels must also be showing on the display.
2534
2535        Arguments:
2536        - root: the Accessible object to traverse
2537        - onlyShowing: if True, only return labels with STATE_SHOWING
2538
2539        Returns a list of unrelated labels under the given root.
2540        """
2541
2542        if self._script.spellcheck and self._script.spellcheck.isCheckWindow(root):
2543            return []
2544
2545        labelRoles = [pyatspi.ROLE_LABEL, pyatspi.ROLE_STATIC]
2546        skipRoles = [pyatspi.ROLE_COMBO_BOX,
2547                     pyatspi.ROLE_LIST_BOX,
2548                     pyatspi.ROLE_MENU,
2549                     pyatspi.ROLE_MENU_BAR,
2550                     pyatspi.ROLE_SCROLL_PANE,
2551                     pyatspi.ROLE_SPLIT_PANE,
2552                     pyatspi.ROLE_TABLE,
2553                     pyatspi.ROLE_TREE,
2554                     pyatspi.ROLE_TREE_TABLE]
2555
2556        def _include(x):
2557            if not (x and x.getRole() in labelRoles):
2558                return False
2559            if x.getRelationSet():
2560                return False
2561            if onlyShowing and not x.getState().contains(pyatspi.STATE_SHOWING):
2562                return False
2563            return True
2564
2565        def _exclude(x):
2566            if not x or x.getRole() in skipRoles:
2567                return True
2568            if onlyShowing and not x.getState().contains(pyatspi.STATE_SHOWING):
2569                return True
2570            return False
2571
2572        excludeIf = lambda x: x and x.getRole() in skipRoles
2573        labels = self.findAllDescendants(root, _include, _exclude)
2574
2575        rootName = root.name
2576
2577        # Eliminate duplicates and things suspected to be labels for widgets
2578        d = {}
2579        for label in labels:
2580            name = label.name or self.displayedText(label)
2581            if name and name in [rootName, label.parent.name]:
2582                continue
2583            if len(name.split()) < minimumWords:
2584                continue
2585            if rootName.find(name) >= 0:
2586                continue
2587            d[name] = label
2588        labels = list(d.values())
2589
2590        return sorted(labels, key=functools.cmp_to_key(self.spatialComparison))
2591
2592    def _treatAlertsAsDialogs(self):
2593        return True
2594
2595    def unfocusedAlertAndDialogCount(self, obj):
2596        """If the current application has one or more alert or dialog
2597        windows and the currently focused window is not an alert or a dialog,
2598        return a count of the number of alert and dialog windows, otherwise
2599        return a count of zero.
2600
2601        Arguments:
2602        - obj: the Accessible object
2603
2604        Returns the alert and dialog count.
2605        """
2606
2607        roles = [pyatspi.ROLE_DIALOG]
2608        if self._treatAlertsAsDialogs():
2609            roles.append(pyatspi.ROLE_ALERT)
2610
2611        isDialog = lambda x: x and x.getRole() in roles or self.isFunctionalDialog(x)
2612        dialogs = [x for x in obj.getApplication() if isDialog(x)]
2613        dialogs.extend([x for x in self.topLevelObject(obj) if isDialog(x)])
2614
2615        isPresentable = lambda x: self.isShowingAndVisible(x) and (x.name or x.childCount)
2616        presentable = list(filter(isPresentable, set(dialogs)))
2617
2618        unfocused = list(filter(lambda x: not self.canBeActiveWindow(x), presentable))
2619        return len(unfocused)
2620
2621    def uri(self, obj):
2622        """Return the URI for a given link object.
2623
2624        Arguments:
2625        - obj: the Accessible object.
2626        """
2627
2628        try:
2629            return obj.queryHyperlink().getURI(0)
2630        except:
2631            return None
2632
2633    def validParent(self, obj):
2634        """Returns the first valid parent/ancestor of obj. We need to do
2635        this in some applications and toolkits due to bogus hierarchies.
2636
2637        Arguments:
2638        - obj: the Accessible object
2639        """
2640
2641        if not obj:
2642            return None
2643
2644        return obj.parent
2645
2646    #########################################################################
2647    #                                                                       #
2648    # Utilities for working with the accessible text interface              #
2649    #                                                                       #
2650    #########################################################################
2651
2652    @staticmethod
2653    def adjustTextSelection(obj, offset):
2654        """Adjusts the end point of a text selection
2655
2656        Arguments:
2657        - obj: the Accessible object.
2658        - offset: the new end point - can be to the left or to the right
2659          depending on the direction of selection
2660        """
2661
2662        try:
2663            text = obj.queryText()
2664        except:
2665            return
2666
2667        if text.getNSelections() <= 0:
2668            caretOffset = text.caretOffset
2669            startOffset = min(offset, caretOffset)
2670            endOffset = max(offset, caretOffset)
2671            text.addSelection(startOffset, endOffset)
2672        else:
2673            startOffset, endOffset = text.getSelection(0)
2674            if offset < startOffset:
2675                startOffset = offset
2676            else:
2677                endOffset = offset
2678            text.setSelection(0, startOffset, endOffset)
2679
2680    def findPreviousObject(self, obj):
2681        """Finds the object before this one."""
2682
2683        if not obj or self.isZombie(obj):
2684            return None
2685
2686        for relation in obj.getRelationSet():
2687            if relation.getRelationType() == pyatspi.RELATION_FLOWS_FROM:
2688                return relation.getTarget(0)
2689
2690        index = obj.getIndexInParent() - 1
2691        while obj.parent and not (0 <= index < obj.parent.childCount - 1):
2692            obj = obj.parent
2693            index = obj.getIndexInParent() - 1
2694
2695        try:
2696            prevObj = obj.parent[index]
2697        except:
2698            prevObj = None
2699
2700        if prevObj == obj:
2701            prevObj = None
2702
2703        return prevObj
2704
2705    def findNextObject(self, obj):
2706        """Finds the object after this one."""
2707
2708        if not obj or self.isZombie(obj):
2709            return None
2710
2711        for relation in obj.getRelationSet():
2712            if relation.getRelationType() == pyatspi.RELATION_FLOWS_TO:
2713                return relation.getTarget(0)
2714
2715        index = obj.getIndexInParent() + 1
2716        while obj.parent and not (0 < index < obj.parent.childCount):
2717            obj = obj.parent
2718            index = obj.getIndexInParent() + 1
2719
2720        try:
2721            nextObj = obj.parent[index]
2722        except:
2723            nextObj = None
2724
2725        if nextObj == obj:
2726            nextObj = None
2727
2728        return nextObj
2729
2730    def allSelectedText(self, obj):
2731        """Get all the text applicable text selections for the given object.
2732        including any previous or next text objects that also have
2733        selected text and add in their text contents.
2734
2735        Arguments:
2736        - obj: the text object to start extracting the selected text from.
2737
2738        Returns: all the selected text contents plus the start and end
2739        offsets within the text for the given object.
2740        """
2741
2742        textContents, startOffset, endOffset = self.selectedText(obj)
2743        if textContents and self._script.pointOfReference.get('entireDocumentSelected'):
2744            return textContents, startOffset, endOffset
2745
2746        if self.isSpreadSheetCell(obj):
2747            return textContents, startOffset, endOffset
2748
2749        prevObj = self.findPreviousObject(obj)
2750        while prevObj:
2751            if self.queryNonEmptyText(prevObj):
2752                selection, start, end = self.selectedText(prevObj)
2753                if not selection:
2754                    break
2755                textContents = "%s %s" % (selection, textContents)
2756            prevObj = self.findPreviousObject(prevObj)
2757
2758        nextObj = self.findNextObject(obj)
2759        while nextObj:
2760            if self.queryNonEmptyText(nextObj):
2761                selection, start, end = self.selectedText(nextObj)
2762                if not selection:
2763                    break
2764                textContents = "%s %s" % (textContents, selection)
2765            nextObj = self.findNextObject(nextObj)
2766
2767        return textContents, startOffset, endOffset
2768
2769    @staticmethod
2770    def allTextSelections(obj):
2771        """Get a list of text selections in the given accessible object,
2772        equivalent to getNSelections()*texti.getSelection()
2773
2774        Arguments:
2775        - obj: An accessible.
2776
2777        Returns list of start and end offsets for multiple selections, or an
2778        empty list if nothing is selected or if the accessible does not support
2779        the text interface.
2780        """
2781
2782        try:
2783            text = obj.queryText()
2784        except:
2785            return []
2786
2787        rv = []
2788        try:
2789            nSelections = text.getNSelections()
2790        except:
2791            nSelections = 0
2792        for i in range(nSelections):
2793            rv.append(text.getSelection(i))
2794
2795        return rv
2796
2797    def getChildAtOffset(self, obj, offset):
2798        try:
2799            hypertext = obj.queryHypertext()
2800        except NotImplementedError:
2801            msg = "INFO: %s does not implement the hypertext interface" % obj
2802            debug.println(debug.LEVEL_INFO, msg, True)
2803            return None
2804        except:
2805            msg = "INFO: Exception querying hypertext interface for %s" % obj
2806            debug.println(debug.LEVEL_INFO, msg, True)
2807            return None
2808
2809        index = hypertext.getLinkIndex(offset)
2810        if index == -1:
2811            return None
2812
2813        hyperlink = hypertext.getLink(index)
2814        if not hyperlink:
2815            msg = "INFO: No hyperlink object at index %i for %s" % (index, obj)
2816            debug.println(debug.LEVEL_INFO, msg, True)
2817            return None
2818
2819        child = hyperlink.getObject(0)
2820        msg = "INFO: Hyperlink object at index %i for %s is %s" % (index, obj, child)
2821        debug.println(debug.LEVEL_INFO, msg, True)
2822
2823        if offset != hyperlink.startIndex:
2824            msg = "ERROR: The hyperlink start index (%i) should match the offset (%i)" \
2825                % (hyperlink.startIndex, offset)
2826            debug.println(debug.LEVEL_INFO, msg, True)
2827
2828        return child
2829
2830    def characterOffsetInParent(self, obj):
2831        """Returns the character offset of the embedded object
2832        character for this object in its parent's accessible text.
2833
2834        Arguments:
2835        - obj: an Accessible that should implement the accessible
2836          hyperlink specialization.
2837
2838        Returns an integer representing the character offset of the
2839        embedded object character for this hyperlink in its parent's
2840        accessible text, or -1 something was amuck.
2841        """
2842
2843        offset = -1
2844        try:
2845            hyperlink = obj.queryHyperlink()
2846        except NotImplementedError:
2847            msg = "INFO: %s does not implement the hyperlink interface" % obj
2848            debug.println(debug.LEVEL_INFO, msg, True)
2849        else:
2850            # We need to make sure that this is an embedded object in
2851            # some accessible text (as opposed to an imagemap link).
2852            #
2853            try:
2854                obj.parent.queryText()
2855                offset = hyperlink.startIndex
2856            except:
2857                msg = "ERROR: Exception getting startIndex for %s in parent %s" % (obj, obj.parent)
2858                debug.println(debug.LEVEL_INFO, msg, True)
2859            else:
2860                msg = "INFO: startIndex of %s is %i" % (obj, offset)
2861                debug.println(debug.LEVEL_INFO, msg, True)
2862
2863        return offset
2864
2865    def clearTextSelection(self, obj):
2866        """Clears the text selection if the object supports it.
2867
2868        Arguments:
2869        - obj: the Accessible object.
2870        """
2871
2872        try:
2873            text = obj.queryText()
2874        except:
2875            return
2876
2877        for i in range(text.getNSelections()):
2878            text.removeSelection(i)
2879
2880    def containsOnlyEOCs(self, obj):
2881        try:
2882            string = obj.queryText().getText(0, -1)
2883        except:
2884            return False
2885
2886        return string and not re.search(r"[^\ufffc]", string)
2887
2888    def expandEOCs(self, obj, startOffset=0, endOffset=-1):
2889        """Expands the current object replacing EMBEDDED_OBJECT_CHARACTERS
2890        with their text.
2891
2892        Arguments
2893        - obj: the object whose text should be expanded
2894        - startOffset: the offset of the first character to be included
2895        - endOffset: the offset of the last character to be included
2896
2897        Returns the fully expanded text for the object.
2898        """
2899
2900        try:
2901            string = self.substring(obj, startOffset, endOffset)
2902        except:
2903            return ""
2904
2905        if not self.EMBEDDED_OBJECT_CHARACTER in string:
2906            return string
2907
2908        blockRoles = [pyatspi.ROLE_HEADING,
2909                      pyatspi.ROLE_LIST,
2910                      pyatspi.ROLE_LIST_ITEM,
2911                      pyatspi.ROLE_PARAGRAPH,
2912                      pyatspi.ROLE_SECTION,
2913                      pyatspi.ROLE_TABLE,
2914                      pyatspi.ROLE_TABLE_CELL,
2915                      pyatspi.ROLE_TABLE_ROW]
2916
2917        toBuild = list(string)
2918        for i, char in enumerate(toBuild):
2919            if char == self.EMBEDDED_OBJECT_CHARACTER:
2920                child = self.getChildAtOffset(obj, i + startOffset)
2921                result = self.expandEOCs(child)
2922                if child and child.getRole() in blockRoles:
2923                    result += " "
2924                toBuild[i] = result
2925
2926        return "".join(toBuild)
2927
2928    def isWordMisspelled(self, obj, offset):
2929        """Identifies if the current word is flagged as misspelled by the
2930        application. Different applications and toolkits flag misspelled
2931        words differently. Thus each script will likely need to implement
2932        its own version of this method.
2933
2934        Arguments:
2935        - obj: An accessible which implements the accessible text interface.
2936        - offset: Offset in the accessible's text for which to retrieve the
2937          attributes.
2938
2939        Returns True if the word is flagged as misspelled.
2940        """
2941
2942        attributes, start, end  = self.textAttributes(obj, offset, True)
2943        if attributes.get("invalid") == "spelling":
2944            return True
2945        if attributes.get("text-spelling") == "misspelled":
2946            return True
2947        if attributes.get("underline") in ["error", "spelling"]:
2948            return True
2949
2950        return False
2951
2952    def getError(self, obj):
2953        return obj.getState().contains(pyatspi.STATE_INVALID_ENTRY)
2954
2955    def getErrorMessage(self, obj):
2956        return ""
2957
2958    def isErrorMessage(self, obj):
2959        return False
2960
2961    def getCharacterAtOffset(self, obj, offset=None):
2962        text = self.queryNonEmptyText(obj)
2963        if text:
2964            if offset is None:
2965                offset = text.caretOffset
2966            return text.getText(offset, offset + 1)
2967
2968        return ""
2969
2970    def queryNonEmptyText(self, obj):
2971        """Get the text interface associated with an object, if it is
2972        non-empty.
2973
2974        Arguments:
2975        - obj: an accessible object
2976        """
2977
2978        try:
2979            text = obj.queryText()
2980            charCount = text.characterCount
2981        except NotImplementedError:
2982            pass
2983        except:
2984            msg = "ERROR: Exception getting character count of %s" % obj
2985            debug.println(debug.LEVEL_INFO, msg, True)
2986        else:
2987            if charCount:
2988                return text
2989
2990        return None
2991
2992    def deletedText(self, event):
2993        return event.any_data
2994
2995    def insertedText(self, event):
2996        if event.any_data:
2997            return event.any_data
2998
2999        try:
3000            role = event.source.getRole()
3001        except:
3002            msg = "ERROR: Exception getting role of %s" % event.source
3003            debug.println(debug.LEVEL_INFO, msg, True)
3004            role = None
3005
3006        msg = "ERROR: Broken text insertion event"
3007        debug.println(debug.LEVEL_INFO, msg, True)
3008
3009        if role == pyatspi.ROLE_PASSWORD_TEXT:
3010            text = self.queryNonEmptyText(event.source)
3011            if text:
3012                string = text.getText(0, -1)
3013                if string:
3014                    msg = "HACK: Returning last char in '%s'" % string
3015                    debug.println(debug.LEVEL_INFO, msg, True)
3016                    return string[-1]
3017
3018        msg = "FAIL: Unable to correct broken text insertion event"
3019        debug.println(debug.LEVEL_INFO, msg, True)
3020        return ""
3021
3022    def selectedText(self, obj):
3023        """Get the text selection for the given object.
3024
3025        Arguments:
3026        - obj: the text object to extract the selected text from.
3027
3028        Returns: the selected text contents plus the start and end
3029        offsets within the text.
3030        """
3031
3032        textContents = ""
3033        startOffset = endOffset = 0
3034        try:
3035            textObj = obj.queryText()
3036        except:
3037            nSelections = 0
3038        else:
3039            nSelections = textObj.getNSelections()
3040
3041        for i in range(0, nSelections):
3042            [startOffset, endOffset] = textObj.getSelection(i)
3043            if startOffset == endOffset:
3044                continue
3045            selectedText = self.expandEOCs(obj, startOffset, endOffset)
3046            if i > 0:
3047                textContents += " "
3048            textContents += selectedText
3049
3050        return [textContents, startOffset, endOffset]
3051
3052    def getCaretContext(self):
3053        obj = orca_state.locusOfFocus
3054        try:
3055            offset = obj.queryText().caretOffset
3056        except NotImplementedError:
3057            offset = 0
3058        except:
3059            offset = -1
3060
3061        return obj, offset
3062
3063    def getFirstCaretPosition(self, obj):
3064        return obj, 0
3065
3066    def setCaretPosition(self, obj, offset, documentFrame=None):
3067        orca.setLocusOfFocus(None, obj, False)
3068        self.setCaretOffset(obj, offset)
3069
3070    def setCaretOffset(self, obj, offset):
3071        """Set the caret offset on a given accessible. Similar to
3072        Accessible.setCaretOffset()
3073
3074        Arguments:
3075        - obj: Given accessible object.
3076        - offset: Offset to hich to set the caret.
3077        """
3078        try:
3079            texti = obj.queryText()
3080        except:
3081            return None
3082
3083        texti.setCaretOffset(offset)
3084
3085    def substring(self, obj, startOffset, endOffset):
3086        """Returns the substring of the given object's text specialization.
3087
3088        Arguments:
3089        - obj: an accessible supporting the accessible text specialization
3090        - startOffset: the starting character position
3091        - endOffset: the ending character position. Note that an end offset
3092          of -1 means the last character
3093        """
3094
3095        try:
3096            text = obj.queryText()
3097        except:
3098            return ""
3099
3100        return text.getText(startOffset, endOffset)
3101
3102    def getAppNameForAttribute(self, attribName):
3103        """Converts the given Atk attribute name into the application's
3104        equivalent. This is necessary because an application or toolkit
3105        (e.g. Gecko) might invent entirely new names for the same text
3106        attributes.
3107
3108        Arguments:
3109        - attribName: The name of the text attribute
3110
3111        Returns the application's equivalent name if found or attribName
3112        otherwise.
3113        """
3114
3115        for key, value in self._script.attributeNamesDict.items():
3116            if value == attribName:
3117                return key
3118
3119        return attribName
3120
3121    def getAtkNameForAttribute(self, attribName):
3122        """Converts the given attribute name into the Atk equivalent. This
3123        is necessary because an application or toolkit (e.g. Gecko) might
3124        invent entirely new names for the same attributes.
3125
3126        Arguments:
3127        - attribName: The name of the text attribute
3128
3129        Returns the Atk equivalent name if found or attribName otherwise.
3130        """
3131
3132        return self._script.attributeNamesDict.get(attribName, attribName)
3133
3134    def textAttributes(self, acc, offset=None, get_defaults=False):
3135        """Get the text attributes run for a given offset in a given accessible
3136
3137        Arguments:
3138        - acc: An accessible.
3139        - offset: Offset in the accessible's text for which to retrieve the
3140        attributes.
3141        - get_defaults: Get the default attributes as well as the unique ones.
3142        Default is True
3143
3144        Returns a dictionary of attributes, a start offset where the attributes
3145        begin, and an end offset. Returns ({}, 0, 0) if the accessible does not
3146        supprt the text attribute.
3147        """
3148
3149        rv = {}
3150        try:
3151            text = acc.queryText()
3152        except:
3153            return rv, 0, 0
3154
3155        if get_defaults:
3156            stringAndDict = self.stringToKeysAndDict(text.getDefaultAttributes())
3157            rv.update(stringAndDict[1])
3158
3159        if offset is None:
3160            offset = text.caretOffset
3161
3162        attrString, start, end = text.getAttributes(offset)
3163        stringAndDict = self.stringToKeysAndDict(attrString)
3164        rv.update(stringAndDict[1])
3165
3166        start = min(start, offset)
3167        end = max(end, offset + 1)
3168
3169        return rv, start, end
3170
3171    def localizeTextAttribute(self, key, value):
3172        if key == "weight" and (value == "bold" or int(value) > 400):
3173            return messages.BOLD
3174
3175        if key.endswith("spelling") or value == "spelling":
3176            return messages.MISSPELLED
3177
3178        localizedKey = text_attribute_names.getTextAttributeName(key, self._script)
3179
3180        if key == "family-name":
3181            localizedValue = value.split(",")[0].strip().strip('"')
3182        elif value and value.endswith("px"):
3183            value = value.split("px")[0]
3184            if locale.localeconv()["decimal_point"] in value:
3185                localizedValue = messages.pixelCount(float(value))
3186            else:
3187                localizedValue = messages.pixelCount(int(value))
3188        elif key.endswith("color"):
3189            r, g, b = self.rgbFromString(value)
3190            if settings.useColorNames:
3191                localizedValue = colornames.rgbToName(r, g, b)
3192            else:
3193                localizedValue = "%i %i %i" % (r, g, b)
3194        else:
3195            localizedValue = text_attribute_names.getTextAttributeName(value, self._script)
3196
3197        return "%s: %s" % (localizedKey, localizedValue)
3198
3199    def willEchoCharacter(self, event):
3200        """Given a keyboard event containing an alphanumeric key,
3201        determine if the script is likely to echo it as a character.
3202        """
3203
3204        if not orca_state.locusOfFocus or not settings.enableEchoByCharacter:
3205            return False
3206
3207        if len(event.event_string) != 1 \
3208           or event.modifiers & keybindings.ORCA_CTRL_MODIFIER_MASK:
3209            return False
3210
3211        obj = orca_state.locusOfFocus
3212        role = obj.getRole()
3213        if role == pyatspi.ROLE_PASSWORD_TEXT:
3214            return False
3215
3216        if obj.getState().contains(pyatspi.STATE_EDITABLE):
3217            return True
3218
3219        return False
3220
3221    #########################################################################
3222    #                                                                       #
3223    # Miscellaneous Utilities                                               #
3224    #                                                                       #
3225    #########################################################################
3226
3227    def _addRepeatSegment(self, segment, line, respectPunctuation=True):
3228        """Add in the latest line segment, adjusting for repeat characters
3229        and punctuation.
3230
3231        Arguments:
3232        - segment: the segment of repeated characters.
3233        - line: the current built-up line to characters to speak.
3234        - respectPunctuation: if False, ignore punctuation level.
3235
3236        Returns: the current built-up line plus the new segment, after
3237        adjusting for repeat character counts and punctuation.
3238        """
3239
3240        from . import punctuation_settings
3241
3242        style = settings.verbalizePunctuationStyle
3243        isPunctChar = True
3244        try:
3245            level, action = punctuation_settings.getPunctuationInfo(segment[0])
3246        except:
3247            isPunctChar = False
3248        count = len(segment)
3249        if (count >= settings.repeatCharacterLimit) \
3250           and (not segment[0] in self._script.whitespace):
3251            if (not respectPunctuation) \
3252               or (isPunctChar and (style <= level)):
3253                repeatChar = chnames.getCharacterName(segment[0])
3254                repeatSegment = messages.repeatedCharCount(repeatChar, count)
3255                line = "%s %s" % (line, repeatSegment)
3256            else:
3257                line += segment
3258        else:
3259            line += segment
3260
3261        return line
3262
3263    def shouldVerbalizeAllPunctuation(self, obj):
3264        if not (self.isCode(obj) or self.isCodeDescendant(obj)):
3265            return False
3266
3267        # If the user has set their punctuation level to All, then the synthesizer will
3268        # do the work for us. If the user has set their punctuation level to None, then
3269        # they really don't want punctuation and we mustn't override that.
3270        style = _settingsManager.getSetting("verbalizePunctuationStyle")
3271        if style in [settings.PUNCTUATION_STYLE_ALL, settings.PUNCTUATION_STYLE_NONE]:
3272            return False
3273
3274        return True
3275
3276    def verbalizeAllPunctuation(self, string):
3277        result = string
3278        for symbol in set(re.findall(self.PUNCTUATION, result)):
3279            charName = " %s " % chnames.getCharacterName(symbol)
3280            result = re.sub("\%s" % symbol, charName, result)
3281
3282        return result
3283
3284    def adjustForLinks(self, obj, line, startOffset):
3285        """Adjust line to include the word "link" after any hypertext links.
3286
3287        Arguments:
3288        - obj: the accessible object that this line came from.
3289        - line: the string to adjust for links.
3290        - startOffset: the caret offset at the start of the line.
3291
3292        Returns: a new line adjusted to add the speaking of "link" after
3293        text which is also a link.
3294        """
3295
3296        from . import punctuation_settings
3297
3298        endOffset = startOffset + len(line)
3299        try:
3300            hyperText = obj.queryHypertext()
3301            nLinks = hyperText.getNLinks()
3302        except:
3303            nLinks = 0
3304
3305        adjustedLine = list(line)
3306        for n in range(nLinks, 0, -1):
3307            link = hyperText.getLink(n - 1)
3308            if not link:
3309                continue
3310
3311            # We only care about links in the string, line:
3312            #
3313            if startOffset < link.endIndex <= endOffset:
3314                index = link.endIndex - startOffset
3315            elif startOffset <= link.startIndex < endOffset:
3316                index = len(line)
3317                if link.endIndex < endOffset:
3318                    index -= 1
3319            else:
3320                continue
3321
3322            linkString = " " + messages.LINK
3323
3324            # If the link was not followed by a whitespace or punctuation
3325            # character, then add in a space to make it more presentable.
3326            #
3327            nextChar = ""
3328            if index < len(line):
3329                nextChar = adjustedLine[index]
3330            if not (nextChar in self._script.whitespace \
3331                    or punctuation_settings.getPunctuationInfo(nextChar)):
3332                linkString += " "
3333            adjustedLine[index:index] = linkString
3334
3335        return "".join(adjustedLine)
3336
3337    @staticmethod
3338    def _processMultiCaseString(string):
3339        return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string)
3340
3341    @staticmethod
3342    def _convertWordToDigits(word):
3343        if not word.isnumeric():
3344            return word
3345
3346        return ' '.join(list(word))
3347
3348    def adjustForPronunciation(self, line):
3349        """Adjust the line to replace words in the pronunciation dictionary,
3350        with what those words actually sound like.
3351
3352        Arguments:
3353        - line: the string to adjust for words in the pronunciation dictionary.
3354
3355        Returns: a new line adjusted for words found in the pronunciation
3356        dictionary.
3357        """
3358
3359        if settings.speakMultiCaseStringsAsWords:
3360            line = self._processMultiCaseString(line)
3361
3362        if self.speakMathSymbolNames():
3363            line = mathsymbols.adjustForSpeech(line)
3364
3365        if settings.speakNumbersAsDigits:
3366            words = self.WORDS_RE.split(line)
3367            line = ''.join(map(self._convertWordToDigits, words))
3368
3369        if len(line) == 1:
3370            charname = chnames.getCharacterName(line)
3371            if charname != line:
3372                return charname
3373
3374        if not settings.usePronunciationDictionary:
3375            return line
3376
3377        newLine = ""
3378        words = self.WORDS_RE.split(line)
3379        newLine = ''.join(map(pronunciation_dict.getPronunciation, words))
3380
3381        if settings.speakMultiCaseStringsAsWords:
3382            newLine = self._processMultiCaseString(newLine)
3383
3384        return newLine
3385
3386    def adjustForRepeats(self, line):
3387        """Adjust line to include repeat character counts. As some people
3388        will want this and others might not, there is a setting in
3389        settings.py that determines whether this functionality is enabled.
3390
3391        repeatCharacterLimit = <n>
3392
3393        If <n> is 0, then there would be no repeat characters.
3394        Otherwise <n> would be the number of same characters (or more)
3395        in a row that cause the repeat character count output.
3396        If the value is set to 1, 2 or 3 then it's treated as if it was
3397        zero. In other words, no repeat character count is given.
3398
3399        Arguments:
3400        - line: the string to adjust for repeat character counts.
3401
3402        Returns: a new line adjusted for repeat character counts (if enabled).
3403        """
3404
3405        if (len(line) < 4) or (settings.repeatCharacterLimit < 4):
3406            return line
3407
3408        newLine = ''
3409        segment = lastChar = line[0]
3410
3411        multipleChars = False
3412        for i in range(1, len(line)):
3413            if line[i] == lastChar:
3414                segment += line[i]
3415            else:
3416                multipleChars = True
3417                newLine = self._addRepeatSegment(segment, newLine)
3418                segment = line[i]
3419
3420            lastChar = line[i]
3421
3422        return self._addRepeatSegment(segment, newLine, multipleChars)
3423
3424    def adjustForDigits(self, string):
3425        """Adjusts the string to convert digit-like text, such as subscript
3426        and superscript numbers, into actual digits.
3427
3428        Arguments:
3429        - string: the string to be adjusted
3430
3431        Returns: a new string which contains actual digits.
3432        """
3433
3434        subscripted = set(re.findall(self.SUBSCRIPTS_RE, string))
3435        superscripted = set(re.findall(self.SUPERSCRIPTS_RE, string))
3436
3437        for number in superscripted:
3438            new = [str(self.SUPERSCRIPT_DIGITS.index(d)) for d in number]
3439            newString = messages.DIGITS_SUPERSCRIPT % "".join(new)
3440            string = re.sub(number, newString, string)
3441
3442        for number in subscripted:
3443            new = [str(self.SUBSCRIPT_DIGITS.index(d)) for d in number]
3444            newString = messages.DIGITS_SUBSCRIPT % "".join(new)
3445            string = re.sub(number, newString, string)
3446
3447        return string
3448
3449    def indentationDescription(self, line):
3450        if _settingsManager.getSetting('onlySpeakDisplayedText') \
3451           or not _settingsManager.getSetting('enableSpeechIndentation'):
3452            return ""
3453
3454        line = line.replace("\u00a0", " ")
3455        end = re.search("[^ \t]", line)
3456        if end:
3457            line = line[:end.start()]
3458
3459        result = ""
3460        spaces = [m.span() for m in re.finditer(" +", line)]
3461        tabs = [m.span() for m in re.finditer("\t+", line)]
3462        spans = sorted(spaces + tabs)
3463        for (start, end) in spans:
3464            if (start, end) in spaces:
3465                result += "%s " % messages.spacesCount(end-start)
3466            else:
3467                result += "%s " % messages.tabsCount(end-start)
3468
3469        return result
3470
3471    @staticmethod
3472    def absoluteMouseCoordinates():
3473        """Gets the absolute position of the mouse pointer."""
3474
3475        from gi.repository import Gtk
3476        rootWindow = Gtk.Window().get_screen().get_root_window()
3477        window, x, y, modifiers = rootWindow.get_pointer()
3478
3479        return x, y
3480
3481    @staticmethod
3482    def appendString(text, newText, delimiter=" "):
3483        """Appends the newText to the given text with the delimiter in between
3484        and returns the new string.  Edge cases, such as no initial text or
3485        no newText, are handled gracefully."""
3486
3487        if not newText:
3488            return text
3489        if not text:
3490            return newText
3491
3492        return text + delimiter + newText
3493
3494    def treatAsDuplicateEvent(self, event1, event2):
3495        if not (event1 and event2):
3496            return False
3497
3498        # The goal is to find event spam so we can ignore the event.
3499        if event1 == event2:
3500            return False
3501
3502        return event1.source == event2.source \
3503            and event1.type == event2.type \
3504            and event1.detail1 == event2.detail1 \
3505            and event1.detail2 == event2.detail2 \
3506            and event1.any_data == event2.any_data
3507
3508    def isAutoTextEvent(self, event):
3509        """Returns True if event is associated with text being autocompleted
3510        or autoinserted or autocorrected or autosomethingelsed.
3511
3512        Arguments:
3513        - event: the accessible event being examined
3514        """
3515
3516        if event.type.startswith("object:text-changed:insert"):
3517            if not event.any_data or not event.source:
3518                return False
3519
3520            state = event.source.getState()
3521            if not state.contains(pyatspi.STATE_EDITABLE):
3522                return False
3523            if not state.contains(pyatspi.STATE_SHOWING):
3524                return False
3525            if state.contains(pyatspi.STATE_FOCUSABLE):
3526                event.source.clearCache()
3527                state = event.source.getState()
3528                if not state.contains(pyatspi.STATE_FOCUSED):
3529                    return False
3530
3531            lastKey, mods = self.lastKeyAndModifiers()
3532            if lastKey == "Tab" and event.any_data != "\t":
3533                return True
3534            if lastKey == "Return" and event.any_data != "\n":
3535                return True
3536            if lastKey in ["Up", "Down", "Page_Up", "Page_Down"]:
3537                return self.isEditableDescendantOfComboBox(event.source)
3538            if not self.lastInputEventWasPrintableKey():
3539                return False
3540
3541            string = event.source.queryText().getText(0, -1)
3542            if string.endswith(event.any_data):
3543                selection, start, end = self.selectedText(event.source)
3544                if selection == event.any_data:
3545                    return True
3546                if string == event.any_data and string.endswith(selection):
3547                    beginning = string[:string.find(selection)]
3548                    return beginning.lower().endswith(lastKey.lower())
3549
3550        return False
3551
3552    def isSentenceDelimiter(self, currentChar, previousChar):
3553        """Returns True if we are positioned at the end of a sentence.
3554        This is determined by checking if the current character is a
3555        white space character and the previous character is one of the
3556        normal end-of-sentence punctuation characters.
3557
3558        Arguments:
3559        - currentChar:  the current character
3560        - previousChar: the previous character
3561
3562        Returns True if the given character is a sentence delimiter.
3563        """
3564
3565        if currentChar == '\r' or currentChar == '\n':
3566            return True
3567
3568        return currentChar in self._script.whitespace \
3569               and previousChar in '!.?:;'
3570
3571    def isWordDelimiter(self, character):
3572        """Returns True if the given character is a word delimiter.
3573
3574        Arguments:
3575        - character: the character in question
3576
3577        Returns True if the given character is a word delimiter.
3578        """
3579
3580        return character in self._script.whitespace \
3581               or character in r'!*+,-./:;<=>?@[\]^_{|}' \
3582               or character == self._script.NO_BREAK_SPACE_CHARACTER
3583
3584    def intersectingRegion(self, obj1, obj2, coordType=None):
3585        """Returns the extents of the intersection of obj1 and obj2."""
3586
3587        if coordType is None:
3588            coordType = pyatspi.DESKTOP_COORDS
3589
3590        try:
3591            extents1 = obj1.queryComponent().getExtents(coordType)
3592            extents2 = obj2.queryComponent().getExtents(coordType)
3593        except:
3594            return 0, 0, 0, 0
3595
3596        return self.intersection(extents1, extents2)
3597
3598    def intersection(self, extents1, extents2):
3599        x1, y1, width1, height1 = extents1
3600        x2, y2, width2, height2 = extents2
3601
3602        xPoints1 = range(x1, x1 + width1 + 1)
3603        xPoints2 = range(x2, x2 + width2 + 1)
3604        xIntersection = sorted(set(xPoints1).intersection(set(xPoints2)))
3605
3606        yPoints1 = range(y1, y1 + height1 + 1)
3607        yPoints2 = range(y2, y2 + height2 + 1)
3608        yIntersection = sorted(set(yPoints1).intersection(set(yPoints2)))
3609
3610        if not (xIntersection and yIntersection):
3611            return 0, 0, 0, 0
3612
3613        x = xIntersection[0]
3614        y = yIntersection[0]
3615        width = xIntersection[-1] - x
3616        height = yIntersection[-1] - y
3617
3618        return x, y, width, height
3619
3620    def containsRegion(self, extents1, extents2):
3621        return self.intersection(extents1, extents2) != (0, 0, 0, 0)
3622
3623    @staticmethod
3624    def _allNamesForKeyCode(keycode):
3625        keymap = Gdk.Keymap.get_default()
3626        entries = keymap.get_entries_for_keycode(keycode)[-1]
3627        return list(map(Gdk.keyval_name, set(entries)))
3628
3629    @staticmethod
3630    def _lastKeyCodeAndModifiers():
3631        if not isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
3632            return 0, 0
3633
3634        event = orca_state.lastNonModifierKeyEvent
3635        if event:
3636            return event.hw_code, event.modifiers
3637
3638        return 0, 0
3639
3640    @staticmethod
3641    def lastKeyAndModifiers():
3642        """Convenience method which returns a tuple containing the event
3643        string and modifiers of the last non-modifier key event or ("", 0)
3644        if there is no such event."""
3645
3646        if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent) \
3647           and orca_state.lastNonModifierKeyEvent:
3648            event = orca_state.lastNonModifierKeyEvent
3649            if event.keyval_name in ["BackSpace", "Delete"]:
3650                eventStr = event.keyval_name
3651            else:
3652                eventStr = event.event_string
3653            mods = orca_state.lastInputEvent.modifiers
3654        else:
3655            eventStr = ""
3656            mods = 0
3657
3658        return (eventStr, mods)
3659
3660    @staticmethod
3661    def labelFromKeySequence(sequence):
3662        """Turns a key sequence into a user-presentable label."""
3663
3664        try:
3665            from gi.repository import Gtk
3666            key, mods = Gtk.accelerator_parse(sequence)
3667            newSequence = Gtk.accelerator_get_label(key, mods)
3668            if newSequence and \
3669               (not newSequence.endswith('+') or newSequence.endswith('++')):
3670                sequence = newSequence
3671        except:
3672            if sequence.endswith(" "):
3673                sequence += chnames.getCharacterName(" ")
3674            sequence = sequence.replace("<", "")
3675            sequence = sequence.replace(">", " ").strip()
3676
3677        return keynames.localizeKeySequence(sequence)
3678
3679    def mnemonicShortcutAccelerator(self, obj):
3680        """Gets the mnemonic, accelerator string and possibly shortcut
3681        for the given object.  These are based upon the first accessible
3682        action for the object.
3683
3684        Arguments:
3685        - obj: the Accessible object
3686
3687        Returns: list containing strings: [mnemonic, shortcut, accelerator]
3688        """
3689
3690        try:
3691            return self._script.generatorCache[self.KEY_BINDING][obj]
3692        except:
3693            if self.KEY_BINDING not in self._script.generatorCache:
3694                self._script.generatorCache[self.KEY_BINDING] = {}
3695
3696        try:
3697            action = obj.queryAction()
3698        except NotImplementedError:
3699            self._script.generatorCache[self.KEY_BINDING][obj] = ["", "", ""]
3700            return self._script.generatorCache[self.KEY_BINDING][obj]
3701
3702        # Action is a string in the format, where the mnemonic and/or
3703        # accelerator can be missing.
3704        #
3705        # <mnemonic>;<full-path>;<accelerator>
3706        #
3707        # The keybindings in <full-path> should be separated by ":"
3708        #
3709        try:
3710            bindingStrings = action.getKeyBinding(0).split(';')
3711        except:
3712            self._script.generatorCache[self.KEY_BINDING][obj] = ["", "", ""]
3713            return self._script.generatorCache[self.KEY_BINDING][obj]
3714
3715        if len(bindingStrings) == 3:
3716            mnemonic       = bindingStrings[0]
3717            fullShortcut   = bindingStrings[1]
3718            accelerator    = bindingStrings[2]
3719        elif len(bindingStrings) > 0:
3720            mnemonic       = ""
3721            fullShortcut   = bindingStrings[0]
3722            try:
3723                accelerator = bindingStrings[1]
3724            except:
3725                accelerator = ""
3726        else:
3727            mnemonic       = ""
3728            fullShortcut   = ""
3729            accelerator    = ""
3730
3731        fullShortcut = fullShortcut.replace(":", " ").strip()
3732        fullShortcut = self.labelFromKeySequence(fullShortcut)
3733        mnemonic = self.labelFromKeySequence(mnemonic)
3734        accelerator = self.labelFromKeySequence(accelerator)
3735
3736        if self.KEY_BINDING not in self._script.generatorCache:
3737            self._script.generatorCache[self.KEY_BINDING] = {}
3738
3739        self._script.generatorCache[self.KEY_BINDING][obj] = \
3740            [mnemonic, fullShortcut, accelerator]
3741        return self._script.generatorCache[self.KEY_BINDING][obj]
3742
3743    @staticmethod
3744    def stringToKeysAndDict(string):
3745        """Converts a string made up of a series of <key>:<value>; pairs
3746        into a dictionary of keys and values. Text before the colon is the
3747        key and text afterwards is the value. The final semi-colon, if
3748        found, is ignored.
3749
3750        Arguments:
3751        - string: the string of tokens containing <key>:<value>; pairs.
3752
3753        Returns a list containing two items:
3754        A list of the keys in the order they were extracted from the
3755        string and a dictionary of key/value items.
3756        """
3757
3758        try:
3759            items = [s.strip() for s in string.split(";")]
3760            items = [item for item in items if len(item.split(':')) == 2]
3761            keys = [item.split(':')[0].strip() for item in items]
3762            dictionary = dict([item.split(':') for item in items])
3763        except:
3764            return [], {}
3765
3766        return [keys, dictionary]
3767
3768    def textForValue(self, obj):
3769        """Returns the text to be displayed for the object's current value.
3770
3771        Arguments:
3772        - obj: the Accessible object that may or may not have a value.
3773
3774        Returns a string representing the value.
3775        """
3776
3777        attrs = self.objectAttributes(obj, False)
3778        valuetext = attrs.get("valuetext")
3779        if valuetext:
3780            return valuetext
3781
3782        try:
3783            value = obj.queryValue()
3784        except NotImplementedError:
3785            return ""
3786        else:
3787            currentValue = value.currentValue
3788
3789        # "The reports of my implementation are greatly exaggerated."
3790        try:
3791            maxValue = value.maximumValue
3792        except (LookupError, RuntimeError):
3793            maxValue = 0.0
3794            msg = 'ERROR: Exception getting maximumValue for %s' % obj
3795            debug.println(debug.LEVEL_INFO, msg, True)
3796        try:
3797            minValue = value.minimumValue
3798        except (LookupError, RuntimeError):
3799            minValue = 0.0
3800            msg = 'ERROR: Exception getting minimumValue for %s' % obj
3801            debug.println(debug.LEVEL_INFO, msg, True)
3802        try:
3803            minIncrement = value.minimumIncrement
3804        except (LookupError, RuntimeError):
3805            minIncrement = (maxValue - minValue) / 100.0
3806            msg = 'ERROR: Exception getting minimumIncrement for %s' % obj
3807            debug.println(debug.LEVEL_INFO, msg, True)
3808        if minIncrement != 0.0:
3809            try:
3810                decimalPlaces = math.ceil(max(0, -math.log10(minIncrement)))
3811            except ValueError:
3812                msg = 'ERROR: Exception calculating decimal places for %s' % obj
3813                debug.println(debug.LEVEL_INFO, msg, True)
3814                return ""
3815        elif abs(currentValue) < 1:
3816            decimalPlaces = 1
3817        else:
3818            decimalPlaces = 0
3819
3820        formatter = "%%.%df" % decimalPlaces
3821        return formatter % currentValue
3822
3823    @staticmethod
3824    def unicodeValueString(character):
3825        """ Returns a four hex digit representation of the given character
3826
3827        Arguments:
3828        - The character to return representation
3829
3830        Returns a string representaition of the given character unicode vlue
3831        """
3832
3833        try:
3834            return "%04x" % ord(character)
3835        except:
3836            debug.printException(debug.LEVEL_WARNING)
3837            return ""
3838
3839    def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True):
3840        return []
3841
3842    def getObjectContentsAtOffset(self, obj, offset=0, useCache=True):
3843        return []
3844
3845    def previousContext(self, obj=None, offset=-1, skipSpace=False):
3846        if not obj:
3847            obj, offset = self.getCaretContext()
3848
3849        return obj, offset - 1
3850
3851    def nextContext(self, obj=None, offset=-1, skipSpace=False):
3852        if not obj:
3853            obj, offset = self.getCaretContext()
3854
3855        return obj, offset + 1
3856
3857    def lastContext(self, root):
3858        offset = 0
3859        text = self.queryNonEmptyText(root)
3860        if text:
3861            offset = text.characterCount - 1
3862
3863        return root, offset
3864
3865    def getHyperlinkRange(self, obj):
3866        """Returns the text range in parent associated with obj."""
3867
3868        try:
3869            hyperlink = obj.queryHyperlink()
3870            start, end = hyperlink.startIndex, hyperlink.endIndex
3871        except NotImplementedError:
3872            msg = "INFO: %s does not implement the hyperlink interface" % obj
3873            debug.println(debug.LEVEL_INFO, msg, True)
3874            return -1, -1
3875        except:
3876            msg = "INFO: Exception getting hyperlink indices for %s" % obj
3877            debug.println(debug.LEVEL_INFO, msg, True)
3878            return -1, -1
3879
3880        return start, end
3881
3882    def selectedChildren(self, obj):
3883        try:
3884            selection = obj.querySelection()
3885            count = selection.nSelectedChildren
3886        except NotImplementedError:
3887            msg = "INFO: %s does not implement the selection interface" % obj
3888            debug.println(debug.LEVEL_INFO, msg, True)
3889            return []
3890        except:
3891            msg = "ERROR: Exception querying selection interface for %s" % obj
3892            debug.println(debug.LEVEL_INFO, msg, True)
3893            return []
3894
3895        msg = "INFO: %s reports %i selected child(ren)" % (obj, count)
3896        debug.println(debug.LEVEL_INFO, msg, True)
3897
3898        children = []
3899        for x in range(count):
3900            child = selection.getSelectedChild(x)
3901            msg = "INFO: Child %i: %s" % (x, child)
3902            debug.println(debug.LEVEL_INFO, msg, True)
3903            if not self.isZombie(child):
3904                children.append(child)
3905
3906        if count and not children:
3907            msg = "INFO: Selected children not retrieved via selection interface."
3908            debug.println(debug.LEVEL_INFO, msg, True)
3909
3910        role = obj.getRole()
3911        if role == pyatspi.ROLE_MENU and not children:
3912            pred = lambda x: x and x.getState().contains(pyatspi.STATE_SELECTED)
3913            children = self.findAllDescendants(obj, pred)
3914
3915        if role == pyatspi.ROLE_COMBO_BOX \
3916           and children and children[0].getRole() == pyatspi.ROLE_MENU:
3917            children = self.selectedChildren(children[0])
3918            if not children and obj.name:
3919                pred = lambda x: x and x.name == obj.name
3920                children = self.findAllDescendants(obj, pred)
3921
3922        return children
3923
3924    def getSelectionContainer(self, obj):
3925        if not obj:
3926            return None
3927
3928        isSelection = lambda x: x and "Selection" in pyatspi.listInterfaces(x)
3929        if isSelection(obj):
3930            return obj
3931
3932        rolemap = {
3933            pyatspi.ROLE_CANVAS: [pyatspi.ROLE_LAYERED_PANE],
3934            pyatspi.ROLE_ICON: [pyatspi.ROLE_LAYERED_PANE],
3935            pyatspi.ROLE_LIST_ITEM: [pyatspi.ROLE_LIST_BOX],
3936            pyatspi.ROLE_TREE_ITEM: [pyatspi.ROLE_TREE, pyatspi.ROLE_TREE_TABLE],
3937            pyatspi.ROLE_TABLE_CELL: [pyatspi.ROLE_TABLE, pyatspi.ROLE_TREE_TABLE],
3938            pyatspi.ROLE_TABLE_ROW: [pyatspi.ROLE_TABLE, pyatspi.ROLE_TREE_TABLE],
3939        }
3940
3941        role = obj.getRole()
3942        isMatch = lambda x: isSelection(x) and x.getRole() in rolemap.get(role)
3943        return pyatspi.findAncestor(obj, isMatch)
3944
3945    def selectableChildCount(self, obj):
3946        if not (obj and "Selection" in pyatspi.listInterfaces(obj)):
3947            return 0
3948
3949        if "Table" in pyatspi.listInterfaces(obj):
3950            rows, cols = self.rowAndColumnCount(obj)
3951            return max(0, rows)
3952
3953        rolemap = {
3954            pyatspi.ROLE_LIST_BOX: [pyatspi.ROLE_LIST_ITEM],
3955            pyatspi.ROLE_TREE: [pyatspi.ROLE_TREE_ITEM],
3956        }
3957
3958        role = obj.getRole()
3959        if role not in rolemap:
3960            return obj.childCount
3961
3962        isMatch = lambda x: x.getRole() in rolemap.get(role)
3963        return len(self.findAllDescendants(obj, isMatch))
3964
3965    def selectedChildCount(self, obj):
3966        if "Table" in pyatspi.listInterfaces(obj):
3967            table = obj.queryTable()
3968            if table.nSelectedRows:
3969                return table.nSelectedRows
3970
3971        try:
3972            selection = obj.querySelection()
3973            count = selection.nSelectedChildren
3974        except NotImplementedError:
3975            msg = "INFO: %s does not implement the selection interface" % obj
3976            debug.println(debug.LEVEL_INFO, msg, True)
3977            return 0
3978        except:
3979            msg = "ERROR: Exception querying selection interface for %s" % obj
3980            debug.println(debug.LEVEL_INFO, msg, True)
3981            return 0
3982
3983        msg = "INFO: %s reports %i selected children" % (obj, count)
3984        debug.println(debug.LEVEL_INFO, msg, True)
3985        return count
3986
3987    def firstAndLastSelectedChildren(self, obj):
3988        try:
3989            selection = obj.querySelection()
3990            count = selection.nSelectedChildren
3991        except NotImplementedError:
3992            msg = "INFO: %s does not implement the selection interface" % obj
3993            debug.println(debug.LEVEL_INFO, msg, True)
3994            return None, None
3995        except:
3996            msg = "ERROR: Exception querying selection interface for %s" % obj
3997            debug.println(debug.LEVEL_INFO, msg, True)
3998            return None, None
3999
4000        return selection.getSelectedChild(0), selection.getSelectedChild(count-1)
4001
4002    def focusedChild(self, obj):
4003        isFocused = lambda x: x and x.getState().contains(pyatspi.STATE_FOCUSED)
4004        child = pyatspi.findDescendant(obj, isFocused)
4005        if child == obj:
4006            msg = "ERROR: focused child of %s is %s" % (obj, child)
4007            debug.println(debug.LEVEL_INFO, msg, True)
4008            return None
4009
4010        return child
4011
4012    def popupMenuFor(self, obj):
4013        if not obj:
4014            return None
4015
4016        try:
4017            childCount = obj.childCount
4018        except:
4019            msg = "ERROR: Exception getting childCount for %s" % obj
4020            debug.println(debug.LEVEL_INFO, msg, True)
4021            return None
4022
4023        menus = [child for child in obj if child.getRole() == pyatspi.ROLE_MENU]
4024        for menu in menus:
4025            try:
4026                state = menu.getState()
4027            except:
4028                msg = "ERROR: Exception getting state for %s" % menu
4029                debug.println(debug.LEVEL_INFO, msg, True)
4030                continue
4031            if state.contains(pyatspi.STATE_ENABLED):
4032                return menu
4033
4034        return None
4035
4036    def isButtonWithPopup(self, obj):
4037        if not obj:
4038            return False
4039
4040        try:
4041            role = obj.getRole()
4042            state = obj.getState()
4043        except:
4044            msg = "ERROR: Exception getting role and state for %s" % obj
4045            debug.println(debug.LEVEL_INFO, msg, True)
4046            return False
4047
4048        return role == pyatspi.ROLE_PUSH_BUTTON and state.contains(pyatspi.STATE_HAS_POPUP)
4049
4050    def isMenuButton(self, obj):
4051        if not obj:
4052            return False
4053
4054        try:
4055            role = obj.getRole()
4056        except:
4057            msg = "ERROR: Exception getting role for %s" % obj
4058            debug.println(debug.LEVEL_INFO, msg, True)
4059            return False
4060
4061        if role not in [pyatspi.ROLE_PUSH_BUTTON, pyatspi.ROLE_TOGGLE_BUTTON]:
4062            return False
4063
4064        return self.popupMenuFor(obj) is not None
4065
4066    def inMenu(self, obj=None):
4067        obj = obj or orca_state.locusOfFocus
4068        if not obj:
4069            return False
4070
4071        try:
4072            role = obj.getRole()
4073        except:
4074            msg = "ERROR: Exception getting role for %s" % obj
4075            debug.println(debug.LEVEL_INFO, msg, True)
4076            return False
4077
4078        menuRoles = [pyatspi.ROLE_MENU,
4079                     pyatspi.ROLE_MENU_ITEM,
4080                     pyatspi.ROLE_CHECK_MENU_ITEM,
4081                     pyatspi.ROLE_RADIO_MENU_ITEM,
4082                     pyatspi.ROLE_TEAROFF_MENU_ITEM]
4083        if role in menuRoles:
4084            return True
4085
4086        if role in [pyatspi.ROLE_PANEL, pyatspi.ROLE_SEPARATOR]:
4087            return obj.parent and obj.parent.getRole() in menuRoles
4088
4089        return False
4090
4091    def inContextMenu(self, obj=None):
4092        obj = obj or orca_state.locusOfFocus
4093        if not self.inMenu(obj):
4094            return False
4095
4096        return pyatspi.findAncestor(obj, self.isContextMenu) is not None
4097
4098    def _contextMenuParentRoles(self):
4099        return pyatspi.ROLE_FRAME, pyatspi.ROLE_WINDOW
4100
4101    def isContextMenu(self, obj):
4102        if not (obj and obj.getRole() == pyatspi.ROLE_MENU):
4103            return False
4104
4105        return obj.parent and obj.parent.getRole() in self._contextMenuParentRoles()
4106
4107    def isTopLevelMenu(self, obj):
4108        if obj.getRole() == pyatspi.ROLE_MENU:
4109            return obj.parent == self.topLevelObject(obj)
4110
4111        return False
4112
4113    def isSingleLineAutocompleteEntry(self, obj):
4114        try:
4115            role = obj.getRole()
4116            state = obj.getState()
4117        except:
4118            msg = "ERROR: Exception getting role and state for %s" % obj
4119            debug.println(debug.LEVEL_INFO, msg, True)
4120            return False
4121
4122        if role != pyatspi.ROLE_ENTRY:
4123            return False
4124
4125        return state.contains(pyatspi.STATE_SUPPORTS_AUTOCOMPLETION) \
4126            and state.contains(pyatspi.STATE_SINGLE_LINE)
4127
4128    def isEntryCompletionPopupItem(self, obj):
4129        return False
4130
4131    def getEntryForEditableComboBox(self, obj):
4132        if not obj:
4133            return None
4134
4135        try:
4136            role = obj.getRole()
4137        except:
4138            msg = "ERROR: Exception getting role for %s" % obj
4139            debug.println(debug.LEVEL_INFO, msg, True)
4140            return None
4141
4142        if role != pyatspi.ROLE_COMBO_BOX:
4143            return None
4144
4145        children = [x for x in obj if self.isEditableTextArea(x)]
4146        if len(children) == 1:
4147            return children[0]
4148
4149        return None
4150
4151    def isEditableComboBox(self, obj):
4152        return self.getEntryForEditableComboBox(obj) is not None
4153
4154    def isEditableDescendantOfComboBox(self, obj):
4155        if not obj:
4156            return False
4157
4158        try:
4159            state = obj.getState()
4160        except:
4161            msg = "ERROR: Exception getting state for %s" % obj
4162            debug.println(debug.LEVEL_INFO, msg, True)
4163            return False
4164
4165        if not state.contains(pyatspi.STATE_EDITABLE):
4166            return False
4167
4168        isComboBox = lambda x: x and x.getRole() == pyatspi.ROLE_COMBO_BOX
4169        return pyatspi.findAncestor(obj, isComboBox) is not None
4170
4171    def getComboBoxValue(self, obj):
4172        if not obj.childCount:
4173            return self.displayedText(obj)
4174
4175        entry = self.getEntryForEditableComboBox(obj)
4176        if entry:
4177            return self.displayedText(entry)
4178
4179        selected = self._script.utilities.selectedChildren(obj)
4180        selected = selected or self._script.utilities.selectedChildren(obj[0])
4181        if len(selected) == 1:
4182            return selected[0].name or self.displayedText(selected[0])
4183
4184        return self.displayedText(obj)
4185
4186    def isPopOver(self, obj):
4187        return False
4188
4189    def isNonModalPopOver(self, obj):
4190        if not self.isPopOver(obj):
4191            return False
4192
4193        try:
4194            state = obj.getState()
4195        except:
4196            msg = "ERROR: Exception getting state for %s" % obj
4197            debug.println(debug.LEVEL_INFO, msg, True)
4198            return False
4199
4200        return not state.contains(pyatspi.STATE_MODAL)
4201
4202    def isUselessPanel(self, obj):
4203        return False
4204
4205    def rgbFromString(self, attributeValue):
4206        regex = re.compile(r"rgb|[^\w,]", re.IGNORECASE)
4207        string = re.sub(regex, "", attributeValue)
4208        red, green, blue = string.split(",")
4209
4210        return int(red), int(green), int(blue)
4211
4212    def isClickableElement(self, obj):
4213        return False
4214
4215    def hasLongDesc(self, obj):
4216        return False
4217
4218    def hasDetails(self, obj):
4219        return False
4220
4221    def isDetails(self, obj):
4222        return False
4223
4224    def detailsFor(self, obj):
4225        return []
4226
4227    def hasVisibleCaption(self, obj):
4228        return False
4229
4230    def popupType(self, obj):
4231        return ''
4232
4233    def headingLevel(self, obj):
4234        if not (obj and obj.getRole() == pyatspi.ROLE_HEADING):
4235            return 0
4236
4237        attrs = self.objectAttributes(obj)
4238
4239        try:
4240            value = int(attrs.get('level', '0'))
4241        except ValueError:
4242            msg = "ERROR: Exception getting value for %s (%s)" % (obj, attrs)
4243            debug.println(debug.LEVEL_INFO, msg, True)
4244            return 0
4245
4246        return value
4247
4248    def hasMeaningfulToggleAction(self, obj):
4249        try:
4250            action = obj.queryAction()
4251        except NotImplementedError:
4252            return False
4253
4254        toggleActionNames = ["toggle", object_properties.ACTION_TOGGLE]
4255        for i in range(action.nActions):
4256            if action.getName(i) in toggleActionNames:
4257                return True
4258
4259        return False
4260
4261    def containingTableHeader(self, obj):
4262        if not obj:
4263            return None
4264
4265        roles = [pyatspi.ROLE_COLUMN_HEADER,
4266                 pyatspi.ROLE_ROW_HEADER,
4267                 pyatspi.ROLE_TABLE_COLUMN_HEADER,
4268                 pyatspi.ROLE_TABLE_ROW_HEADER]
4269        isHeader = lambda x: x and x.getRole() in roles
4270        if isHeader(obj):
4271            return obj
4272
4273        return pyatspi.findAncestor(obj, isHeader)
4274
4275    def columnHeadersForCell(self, obj):
4276        if not (obj and obj.getRole() == pyatspi.ROLE_TABLE_CELL):
4277            return []
4278
4279        isTable = lambda x: x and 'Table' in pyatspi.listInterfaces(x)
4280        parent = pyatspi.findAncestor(obj, isTable)
4281        try:
4282            table = parent.queryTable()
4283        except:
4284            return []
4285
4286        index = self.cellIndex(obj)
4287        row, col = table.getRowAtIndex(index), table.getColumnAtIndex(index)
4288        colspan = table.getColumnExtentAt(row, col)
4289
4290        headers = []
4291        for c in range(col, col+colspan):
4292            headers.append(table.getColumnHeader(c))
4293
4294        return headers
4295
4296    def rowHeadersForCell(self, obj):
4297        if not (obj and obj.getRole() == pyatspi.ROLE_TABLE_CELL):
4298            return []
4299
4300        isTable = lambda x: x and 'Table' in pyatspi.listInterfaces(x)
4301        parent = pyatspi.findAncestor(obj, isTable)
4302        try:
4303            table = parent.queryTable()
4304        except:
4305            return []
4306
4307        index = self.cellIndex(obj)
4308        row, col = table.getRowAtIndex(index), table.getColumnAtIndex(index)
4309        rowspan = table.getRowExtentAt(row, col)
4310
4311        headers = []
4312        for r in range(row, row+rowspan):
4313            headers.append(table.getRowHeader(r))
4314
4315        return headers
4316
4317    def columnHeaderForCell(self, obj):
4318        if not (obj and obj.getRole() == pyatspi.ROLE_TABLE_CELL):
4319            return None
4320
4321        isTable = lambda x: x and 'Table' in pyatspi.listInterfaces(x)
4322        parent = pyatspi.findAncestor(obj, isTable)
4323        try:
4324            table = parent.queryTable()
4325        except:
4326            return None
4327
4328        index = self.cellIndex(obj)
4329        columnIndex = table.getColumnAtIndex(index)
4330        return table.getColumnHeader(columnIndex)
4331
4332    def rowHeaderForCell(self, obj):
4333        if not (obj and obj.getRole() == pyatspi.ROLE_TABLE_CELL):
4334            return None
4335
4336        isTable = lambda x: x and 'Table' in pyatspi.listInterfaces(x)
4337        parent = pyatspi.findAncestor(obj, isTable)
4338        try:
4339            table = parent.queryTable()
4340        except:
4341            return None
4342
4343        index = self.cellIndex(obj)
4344        rowIndex = table.getRowAtIndex(index)
4345        return table.getRowHeader(rowIndex)
4346
4347    def coordinatesForCell(self, obj, preferAttribute=True):
4348        roles = [pyatspi.ROLE_TABLE_CELL,
4349                 pyatspi.ROLE_TABLE_COLUMN_HEADER,
4350                 pyatspi.ROLE_TABLE_ROW_HEADER,
4351                 pyatspi.ROLE_COLUMN_HEADER,
4352                 pyatspi.ROLE_ROW_HEADER]
4353        if not (obj and obj.getRole() in roles):
4354            return -1, -1
4355
4356        isTable = lambda x: x and 'Table' in pyatspi.listInterfaces(x)
4357        parent = pyatspi.findAncestor(obj, isTable)
4358        try:
4359            table = parent.queryTable()
4360        except:
4361            return -1, -1
4362
4363        index = self.cellIndex(obj)
4364        return table.getRowAtIndex(index), table.getColumnAtIndex(index)
4365
4366    def rowAndColumnSpan(self, obj):
4367        if not (obj and obj.getRole() == pyatspi.ROLE_TABLE_CELL):
4368            return -1, -1
4369
4370        isTable = lambda x: x and 'Table' in pyatspi.listInterfaces(x)
4371        parent = pyatspi.findAncestor(obj, isTable)
4372        try:
4373            table = parent.queryTable()
4374        except:
4375            return -1, -1
4376
4377        index = self.cellIndex(obj)
4378        row, col = table.getRowAtIndex(index), table.getColumnAtIndex(index)
4379        return table.getRowExtentAt(row, col), table.getColumnExtentAt(row, col)
4380
4381    def rowAndColumnCount(self, obj, preferAttribute=True):
4382        try:
4383            table = obj.queryTable()
4384        except:
4385            return -1, -1
4386
4387        return table.nRows, table.nColumns
4388
4389    def _objectBoundsMightBeBogus(self, obj):
4390        return False
4391
4392    def _objectMightBeBogus(self, obj):
4393        return False
4394
4395    def containsPoint(self, obj, x, y, coordType, margin=2):
4396        if self._objectBoundsMightBeBogus(obj) \
4397           and self.textAtPoint(obj, x, y, coordType) == ("", 0, 0):
4398            return False
4399
4400        if self._objectMightBeBogus(obj):
4401            return False
4402
4403        try:
4404            component = obj.queryComponent()
4405        except:
4406            return False
4407
4408        if coordType is None:
4409            coordType = pyatspi.DESKTOP_COORDS
4410
4411        if component.contains(x, y, coordType):
4412            return True
4413
4414        x1, y1 = x + margin, y + margin
4415        if component.contains(x1, y1, coordType):
4416            msg = "INFO: %s contains (%i,%i); not (%i,%i)" % (obj, x1, y1, x, y)
4417            debug.println(debug.LEVEL_INFO, msg, True)
4418            return True
4419
4420        return False
4421
4422    def _boundsIncludeChildren(self, obj):
4423        if not obj:
4424            return False
4425
4426        if self.hasNoSize(obj):
4427            return False
4428
4429        roles = [pyatspi.ROLE_MENU,
4430                 pyatspi.ROLE_PAGE_TAB]
4431
4432        return obj.getRole() not in roles
4433
4434    def treatAsEntry(self, obj):
4435        return False
4436
4437    def _treatAsLeafNode(self, obj):
4438        if not obj or self.isDead(obj):
4439            return False
4440
4441        if not obj.childCount:
4442            return True
4443
4444        role = obj.getRole()
4445        roles = [pyatspi.ROLE_AUTOCOMPLETE,
4446                 pyatspi.ROLE_TABLE_ROW]
4447        if role in roles:
4448            return False
4449
4450        if role == pyatspi.ROLE_COMBO_BOX:
4451            entry = pyatspi.findDescendant(obj, lambda x: x and x.getRole() == pyatspi.ROLE_ENTRY)
4452            return entry is None
4453
4454        if role == pyatspi.ROLE_LINK and obj.name:
4455            return True
4456
4457        state = obj.getState()
4458        if state.contains(pyatspi.STATE_EXPANDABLE):
4459            return not state.contains(pyatspi.STATE_EXPANDED)
4460
4461        roles = [pyatspi.ROLE_PUSH_BUTTON,
4462                 pyatspi.ROLE_TOGGLE_BUTTON]
4463
4464        return role in roles
4465
4466    def accessibleAtPoint(self, root, x, y, coordType=None):
4467        if self.isHidden(root):
4468            return None
4469
4470        try:
4471            component = root.queryComponent()
4472        except:
4473            msg = "INFO: Exception querying component of %s" % root
4474            debug.println(debug.LEVEL_INFO, msg, True)
4475            return None
4476
4477        result = component.getAccessibleAtPoint(x, y, coordType)
4478        msg = "INFO: %s is descendant of %s at (%i, %i)" % (result, root, x, y)
4479        debug.println(debug.LEVEL_INFO, msg, True)
4480        return result
4481
4482    def descendantAtPoint(self, root, x, y, coordType=None):
4483        if not root:
4484            return None
4485
4486        if not self.isShowingAndVisible(root):
4487            return None
4488
4489        if coordType is None:
4490            coordType = pyatspi.DESKTOP_COORDS
4491
4492        if self.containsPoint(root, x, y, coordType):
4493            if self._treatAsLeafNode(root) or not self._boundsIncludeChildren(root):
4494                return root
4495        elif self._treatAsLeafNode(root) or self._boundsIncludeChildren(root):
4496            return None
4497
4498        if "Table" in pyatspi.listInterfaces(root):
4499            child = self.accessibleAtPoint(root, x, y, coordType)
4500            if child and child != root:
4501                cell = self.descendantAtPoint(child, x, y, coordType)
4502                if cell:
4503                    return cell
4504                return child
4505
4506        candidates_showing = []
4507        candidates = []
4508        for child in root:
4509            obj = self.descendantAtPoint(child, x, y, coordType)
4510            if obj:
4511                return obj
4512            if not self.containsPoint(child, x, y, coordType):
4513                continue
4514            if self.queryNonEmptyText(child):
4515                string = child.queryText().getText(0, -1)
4516                if re.search("[^\ufffc\s]", string):
4517                    candidates.append(child)
4518                    if child.getState().contains(pyatspi.STATE_SHOWING):
4519                        candidates_showing.append(child)
4520
4521        if len(candidates_showing) == 1:
4522            return candidates_showing[0]
4523        if len(candidates) == 1:
4524            # It should have had state "showing" actually
4525            return candidates[0]
4526
4527        return None
4528
4529    def _adjustPointForObj(self, obj, x, y, coordType):
4530        return x, y
4531
4532    def isMultiParagraphObject(self, obj):
4533        if not obj:
4534            return False
4535
4536        if "Text" not in pyatspi.listInterfaces(obj):
4537            return False
4538
4539        text = obj.queryText()
4540        string = text.getText(0, -1)
4541        chunks = list(filter(lambda x: x.strip(), string.split("\n\n")))
4542        return len(chunks) > 1
4543
4544    def getWordAtOffsetAdjustedForNavigation(self, obj, offset=None):
4545        try:
4546            text = obj.queryText()
4547            if offset is None:
4548                offset = text.caretOffset
4549        except:
4550            return "", 0, 0
4551
4552        word, start, end = self.getWordAtOffset(obj, offset)
4553        prevObj, prevOffset = self._script.pointOfReference.get("penultimateCursorPosition", (None, -1))
4554        if prevObj != obj:
4555            return word, start, end
4556
4557        # If we're in an ongoing series of native navigation-by-word commands, just present the
4558        # newly-traversed string.
4559        prevWord, prevStart, prevEnd = self.getWordAtOffset(prevObj, prevOffset)
4560        if self._script.pointOfReference.get("lastTextUnitSpoken") == "word":
4561            if self.lastInputEventWasPrevWordNav():
4562                start = offset
4563                end = prevOffset
4564            elif self.lastInputEventWasNextWordNav():
4565                start = prevOffset
4566                end = offset
4567
4568            word = text.getText(start, end)
4569            msg = "INFO: Adjusted word at offset %i for ongoing word nav is '%s' (%i-%i)" \
4570                % (offset, word.replace("\n", "\\n"), start, end)
4571            debug.println(debug.LEVEL_INFO, msg, True)
4572            return word, start, end
4573
4574        # Otherwise, attempt some smarts so that the user winds up with the same presentation
4575        # they would get were this an ongoing series of native navigation-by-word commands.
4576        if self.lastInputEventWasPrevWordNav():
4577            # If we moved left via native nav, this should be the start of a native-navigation
4578            # word boundary, regardless of what ATK/AT-SPI2 tells us.
4579            start = offset
4580
4581            # The ATK/AT-SPI2 word typically ends in a space; if the ending is neither a space,
4582            # nor an alphanumeric character, then suspect that character is a navigation boundary
4583            # where we would have landed before via the native previous word command.
4584            if not (word[-1].isspace() or word[-1].isalnum()):
4585                end -= 1
4586
4587        elif self.lastInputEventWasNextWordNav():
4588            # If we moved right via native nav, this should be the end of a native-navigation
4589            # word boundary, regardless of what ATK/AT-SPI2 tells us.
4590            end = offset
4591
4592            # This suggests we just moved to the end of the previous word.
4593            if word != prevWord and prevStart < offset <= prevEnd:
4594                start = prevStart
4595
4596            # If the character to the left of our present position is neither a space, nor
4597            # an alphanumeric character, then suspect that character is a navigation boundary
4598            # where we would have landed before via the native next word command.
4599            lastChar = text.getText(offset - 1, offset)
4600            if not (lastChar.isspace() or lastChar.isalnum()):
4601                start = offset - 1
4602
4603        word = text.getText(start, end)
4604
4605        # We only want to present the newline character when we cross a boundary moving from one
4606        # word to another. If we're in the same word, strip it out.
4607        if "\n" in word and word == prevWord:
4608            if word.startswith("\n"):
4609                start += 1
4610            elif word.endswith("\n"):
4611                end -= 1
4612            word = text.getText(start, end)
4613
4614        word = text.getText(start, end)
4615        msg = "INFO: Adjusted word at offset %i for new word nav is '%s' (%i-%i)" \
4616            % (offset, word.replace("\n", "\\n"), start, end)
4617        debug.println(debug.LEVEL_INFO, msg, True)
4618        return word, start, end
4619
4620    def getWordAtOffset(self, obj, offset=None):
4621        try:
4622            text = obj.queryText()
4623            if offset is None:
4624                offset = text.caretOffset
4625        except:
4626            return "", 0, 0
4627
4628        word, start, end = text.getTextAtOffset(offset, pyatspi.TEXT_BOUNDARY_WORD_START)
4629        msg = "INFO: Word at %i is '%s' (%i-%i)" % (offset, word.replace("\n", "\\n"), start, end)
4630        debug.println(debug.LEVEL_INFO, msg, True)
4631        return word, start, end
4632
4633    def textAtPoint(self, obj, x, y, coordType=None, boundary=None):
4634        text = self.queryNonEmptyText(obj)
4635        if not text:
4636            return "", 0, 0
4637
4638        if coordType is None:
4639            coordType = pyatspi.DESKTOP_COORDS
4640
4641        if boundary is None:
4642            boundary = pyatspi.TEXT_BOUNDARY_LINE_START
4643
4644        x, y = self._adjustPointForObj(obj, x, y, coordType)
4645        offset = text.getOffsetAtPoint(x, y, coordType)
4646        if not 0 <= offset < text.characterCount:
4647            return "", 0, 0
4648
4649        string, start, end = text.getTextAtOffset(offset, boundary)
4650        if not string:
4651            return "", start, end
4652
4653        if boundary == pyatspi.TEXT_BOUNDARY_WORD_START and not string.strip():
4654            return "", 0, 0
4655
4656        extents = text.getRangeExtents(start, end, coordType)
4657        if not self.containsRegion(extents, (x, y, 1, 1)) and string != "\n":
4658            return "", 0, 0
4659
4660        if not string.endswith("\n") or string == "\n":
4661            return string, start, end
4662
4663        if boundary == pyatspi.TEXT_BOUNDARY_CHAR:
4664            return string, start, end
4665
4666        char = self.textAtPoint(obj, x, y, coordType, pyatspi.TEXT_BOUNDARY_CHAR)
4667        if char[0] == "\n" and char[2] - char[1] == 1:
4668            return char
4669
4670        return string, start, end
4671
4672    def visibleRows(self, obj, boundingbox):
4673        try:
4674            table = obj.queryTable()
4675            nRows = table.nRows
4676        except:
4677            return []
4678
4679        msg = "INFO: %s has %i rows" % (obj, nRows)
4680        debug.println(debug.LEVEL_INFO, msg, True)
4681
4682        x, y, width, height = boundingbox
4683        cell = self.descendantAtPoint(obj, x, y + 1)
4684        row, col = self.coordinatesForCell(cell)
4685        startIndex = max(0, row)
4686        msg = "INFO: First cell: %s (row: %i)" % (cell, row)
4687        debug.println(debug.LEVEL_INFO, msg, True)
4688
4689        # Just in case the row above is a static header row in a scrollable table.
4690        try:
4691            extents = cell.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
4692        except:
4693            nextIndex = startIndex
4694        else:
4695            cell = self.descendantAtPoint(obj, x, y + extents.height + 1)
4696            row, col = self.coordinatesForCell(cell)
4697            nextIndex = max(startIndex, row)
4698            msg = "INFO: Next cell: %s (row: %i)" % (cell, row)
4699            debug.println(debug.LEVEL_INFO, msg, True)
4700
4701        cell = self.descendantAtPoint(obj, x, y + height - 1)
4702        row, col = self.coordinatesForCell(cell)
4703        msg = "INFO: Last cell: %s (row: %i)" % (cell, row)
4704        debug.println(debug.LEVEL_INFO, msg, True)
4705
4706        if row == -1:
4707            row = nRows
4708        endIndex = row
4709
4710        rows = list(range(nextIndex, endIndex))
4711        if startIndex not in rows:
4712            rows.insert(0, startIndex)
4713
4714        return rows
4715
4716    def getVisibleTableCells(self, obj):
4717        try:
4718            table = obj.queryTable()
4719        except:
4720            return []
4721
4722        try:
4723            component = obj.queryComponent()
4724            extents = component.getExtents(pyatspi.DESKTOP_COORDS)
4725        except:
4726            msg = "ERROR: Exception getting extents of %s" % obj
4727            debug.println(debug.LEVEL_INFO, msg, True)
4728            return []
4729
4730        rows = self.visibleRows(obj, extents)
4731        if not rows:
4732            return []
4733
4734        colStartIndex, colEndIndex = self._getTableRowRange(obj)
4735        if colStartIndex == colEndIndex:
4736            return []
4737
4738        cells = []
4739        for col in range(colStartIndex, colEndIndex):
4740            colHeader = table.getColumnHeader(col)
4741            if colHeader:
4742                cells.append(colHeader)
4743            for row in rows:
4744                try:
4745                    cell = table.getAccessibleAt(row, col)
4746                except:
4747                    continue
4748                if cell and self.isOnScreen(cell):
4749                    cells.append(cell)
4750
4751        return cells
4752
4753    def _getTableRowRange(self, obj):
4754        rowCount, columnCount = self.rowAndColumnCount(obj)
4755        startIndex, endIndex = 0, columnCount
4756        if not self.isSpreadSheetCell(obj):
4757            return startIndex, endIndex
4758
4759        parent = self.getTable(obj)
4760        try:
4761            component = parent.queryComponent()
4762        except:
4763            msg = "ERROR: Exception querying component interface of %s" % parent
4764            debug.println(debug.LEVEL_INFO, msg, True)
4765            return startIndex, endIndex
4766
4767        x, y, width, height = component.getExtents(pyatspi.DESKTOP_COORDS)
4768        cell = component.getAccessibleAtPoint(x+1, y, pyatspi.DESKTOP_COORDS)
4769        if cell:
4770            row, column = self.coordinatesForCell(cell)
4771            startIndex = column
4772
4773        cell = component.getAccessibleAtPoint(x+width-1, y, pyatspi.DESKTOP_COORDS)
4774        if cell:
4775            row, column = self.coordinatesForCell(cell)
4776            endIndex = column + 1
4777
4778        return startIndex, endIndex
4779
4780    def getShowingCellsInSameRow(self, obj, forceFullRow=False):
4781        parent = self.getTable(obj)
4782        try:
4783            table = parent.queryTable()
4784        except:
4785            msg = "ERROR: Exception querying table interface of %s" % parent
4786            debug.println(debug.LEVEL_INFO, msg, True)
4787            return []
4788
4789        row, column = self.coordinatesForCell(obj, False)
4790        if row == -1:
4791            return []
4792
4793        if forceFullRow:
4794            startIndex, endIndex = 0, table.nColumns
4795        else:
4796            startIndex, endIndex = self._getTableRowRange(obj)
4797        if startIndex == endIndex:
4798            return []
4799
4800        cells = []
4801        for i in range(startIndex, endIndex):
4802            cell = table.getAccessibleAt(row, i)
4803            try:
4804                showing = cell.getState().contains(pyatspi.STATE_SHOWING)
4805            except:
4806                continue
4807            if showing:
4808                cells.append(cell)
4809
4810        return cells
4811
4812    def cellForCoordinates(self, obj, row, column, showingOnly=False):
4813        try:
4814            table = obj.queryTable()
4815        except:
4816            return None
4817
4818        cell = table.getAccessibleAt(row, column)
4819        if not showingOnly:
4820            return cell
4821
4822        try:
4823            state = cell.getState()
4824        except:
4825            msg = "ERROR: Exception getting state of %s" % cell
4826            debug.println(debug.LEVEL_INFO, msg, True)
4827            return None
4828
4829        if not state().contains(pyatspi.STATE_SHOWING):
4830            return None
4831
4832        return cell
4833
4834    def isLastCell(self, obj):
4835        try:
4836            role = obj.getRole()
4837        except:
4838            msg = "ERROR: Exception getting role of %s" % obj
4839            debug.println(debug.LEVEL_INFO, msg, True)
4840            return False
4841
4842        if not role == pyatspi.ROLE_TABLE_CELL:
4843            return False
4844
4845        isTable = lambda x: x and 'Table' in pyatspi.listInterfaces(x)
4846        parent = pyatspi.findAncestor(obj, isTable)
4847        try:
4848            table = parent.queryTable()
4849        except:
4850            return False
4851
4852        index = self.cellIndex(obj)
4853        row, col = table.getRowAtIndex(index), table.getColumnAtIndex(index)
4854        return row + 1 == table.nRows and col + 1 == table.nColumns
4855
4856    def isNonUniformTable(self, obj):
4857        try:
4858            table = obj.queryTable()
4859        except:
4860            return False
4861
4862        for r in range(table.nRows):
4863            for c in range(table.nColumns):
4864                if table.getRowExtentAt(r, c) > 1 \
4865                   or table.getColumnExtentAt(r, c) > 1:
4866                    return True
4867
4868        return False
4869
4870    def isShowingOrVisible(self, obj):
4871        try:
4872            state = obj.getState()
4873        except:
4874            msg = "ERROR: Exception getting state of %s" % obj
4875            debug.println(debug.LEVEL_INFO, msg, True)
4876            return False
4877
4878        if state.contains(pyatspi.STATE_SHOWING) \
4879           or state.contains(pyatspi.STATE_VISIBLE):
4880            return True
4881
4882        msg = "INFO: %s is neither showing nor visible" % obj
4883        debug.println(debug.LEVEL_INFO, msg, True)
4884        return False
4885
4886    def isShowingAndVisible(self, obj):
4887        try:
4888            state = obj.getState()
4889            role = obj.getRole()
4890        except:
4891            msg = "ERROR: Exception getting state and role of %s" % obj
4892            debug.println(debug.LEVEL_INFO, msg, True)
4893            return False
4894
4895        if state.contains(pyatspi.STATE_SHOWING) \
4896           and state.contains(pyatspi.STATE_VISIBLE):
4897            return True
4898
4899        # TODO - JD: This really should be in the toolkit scripts. But it
4900        # seems to be present in multiple toolkits, so it's either being
4901        # inherited (e.g. from Gtk in Firefox Chrome, LO, Eclipse) or it
4902        # may be an AT-SPI2 bug. For now, handling it here.
4903        menuRoles = [pyatspi.ROLE_MENU,
4904                     pyatspi.ROLE_MENU_ITEM,
4905                     pyatspi.ROLE_CHECK_MENU_ITEM,
4906                     pyatspi.ROLE_RADIO_MENU_ITEM,
4907                     pyatspi.ROLE_SEPARATOR]
4908
4909        if role in menuRoles and self.isInOpenMenuBarMenu(obj):
4910            msg = "HACK: Treating %s as showing and visible" % obj
4911            debug.println(debug.LEVEL_INFO, msg, True)
4912            return True
4913
4914        return False
4915
4916    def isDead(self, obj):
4917        try:
4918            name = obj.name
4919        except:
4920            debug.println(debug.LEVEL_INFO, "DEAD: %s" % obj, True)
4921            return True
4922
4923        return False
4924
4925    def isZombie(self, obj):
4926        try:
4927            index = obj.getIndexInParent()
4928            state = obj.getState()
4929            role = obj.getRole()
4930        except:
4931            debug.println(debug.LEVEL_INFO, "ZOMBIE: %s is null or dead" % obj, True)
4932            return True
4933
4934        topLevelRoles = [pyatspi.ROLE_APPLICATION,
4935                         pyatspi.ROLE_ALERT,
4936                         pyatspi.ROLE_DIALOG,
4937                         pyatspi.ROLE_LABEL, # For Unity Panel Service bug
4938                         pyatspi.ROLE_PAGE, # For Evince bug
4939                         pyatspi.ROLE_WINDOW,
4940                         pyatspi.ROLE_FRAME]
4941        if index == -1 and role not in topLevelRoles:
4942            debug.println(debug.LEVEL_INFO, "ZOMBIE: %s's index is -1" % obj, True)
4943            return True
4944        if state.contains(pyatspi.STATE_DEFUNCT):
4945            debug.println(debug.LEVEL_INFO, "ZOMBIE: %s is defunct" % obj, True)
4946            return True
4947        if state.contains(pyatspi.STATE_INVALID):
4948            debug.println(debug.LEVEL_INFO, "ZOMBIE: %s is invalid" % obj, True)
4949            return True
4950
4951        return False
4952
4953    def findReplicant(self, root, obj):
4954        msg = "INFO: Searching for replicant for %s in %s" % (obj, root)
4955        debug.println(debug.LEVEL_INFO, msg, True)
4956        if not (root and obj):
4957            return None
4958
4959        # Given an broken table hierarchy, findDescendant can hang. And the
4960        # reason we're here in the first place is to work around the app or
4961        # toolkit killing accessibles. There's only so much we can do....
4962        if root.getRole() in [pyatspi.ROLE_TABLE, pyatspi.ROLE_EMBEDDED]:
4963            return None
4964
4965        isSame = lambda x: x and self.isSameObject(
4966            x, obj, comparePaths=True, ignoreNames=True)
4967        if isSame(root):
4968            replicant = root
4969        else:
4970            try:
4971                replicant = pyatspi.findDescendant(root, isSame)
4972            except:
4973                msg = "INFO: Exception from findDescendant for %s" % root
4974                debug.println(debug.LEVEL_INFO, msg, True)
4975                replicant = None
4976
4977        msg = "HACK: Returning %s as replicant for Zombie %s" % (replicant, obj)
4978        debug.println(debug.LEVEL_INFO, msg, True)
4979        return replicant
4980
4981    def getFunctionalChildCount(self, obj):
4982        if not obj:
4983            return None
4984
4985        result = []
4986        pred = lambda r: r.getRelationType() == pyatspi.RELATION_NODE_PARENT_OF
4987        relations = list(filter(pred, obj.getRelationSet()))
4988        if relations:
4989            return relations[0].getNTargets()
4990
4991        return obj.childCount
4992
4993    def getFunctionalChildren(self, obj):
4994        if not obj:
4995            return None
4996
4997        result = []
4998        pred = lambda r: r.getRelationType() == pyatspi.RELATION_NODE_PARENT_OF
4999        relations = list(filter(pred, obj.getRelationSet()))
5000        if relations:
5001            r = relations[0]
5002            result = [r.getTarget(i) for i in range(r.getNTargets())]
5003
5004        return result or [child for child in obj]
5005
5006    def getFunctionalParent(self, obj):
5007        if not obj:
5008            return None
5009
5010        result = None
5011        pred = lambda r: r.getRelationType() == pyatspi.RELATION_NODE_CHILD_OF
5012        relations = list(filter(pred, obj.getRelationSet()))
5013        if relations:
5014            result = relations[0].getTarget(0)
5015
5016        return result or obj.parent
5017
5018    def getPositionAndSetSize(self, obj, **args):
5019        if not obj:
5020            return -1, -1
5021
5022        if obj.getRole() == pyatspi.ROLE_TABLE_CELL and args.get("readingRow"):
5023            row, col = self.coordinatesForCell(obj)
5024            rowcount, colcount = self.rowAndColumnCount(self.getTable(obj))
5025            return row, rowcount
5026
5027        isComboBox = obj.getRole() == pyatspi.ROLE_COMBO_BOX
5028        if isComboBox:
5029            selected = self.selectedChildren(obj)
5030            if selected:
5031                obj = selected[0]
5032            else:
5033                isMenu = lambda x: x and x.getRole() in [pyatspi.ROLE_MENU, pyatspi.ROLE_LIST_BOX]
5034                selected = self.selectedChildren(pyatspi.findDescendant(obj, isMenu))
5035                if selected:
5036                    obj = selected[0]
5037                else:
5038                    return -1, -1
5039
5040        parent = self.getFunctionalParent(obj)
5041        childCount = self.getFunctionalChildCount(parent)
5042        if childCount > 100 and parent == obj.parent:
5043            return obj.getIndexInParent(), childCount
5044
5045        siblings = self.getFunctionalChildren(parent)
5046        if len(siblings) < 100 and not pyatspi.utils.findAncestor(obj, isComboBox):
5047            layoutRoles = [pyatspi.ROLE_SEPARATOR, pyatspi.ROLE_TEAROFF_MENU_ITEM]
5048            isNotLayoutOnly = lambda x: not (self.isZombie(x) or x.getRole() in layoutRoles)
5049            siblings = list(filter(isNotLayoutOnly, siblings))
5050
5051        if not (siblings and obj in siblings):
5052            return -1, -1
5053
5054        if self.isFocusableLabel(obj):
5055            siblings = list(filter(self.isFocusableLabel, siblings))
5056            if len(siblings) == 1:
5057                return -1, -1
5058
5059        position = siblings.index(obj)
5060        setSize = len(siblings)
5061        return position, setSize
5062
5063    def getRoleDescription(self, obj, isBraille=False):
5064        return ""
5065
5066    def getCachedTextSelection(self, obj):
5067        textSelections = self._script.pointOfReference.get('textSelections', {})
5068        start, end, string = textSelections.get(hash(obj), (0, 0, ''))
5069        msg = "INFO: Cached selection for %s is '%s' (%i, %i)" % (obj, string, start, end)
5070        debug.println(debug.LEVEL_INFO, msg, True)
5071        return start, end, string
5072
5073    def updateCachedTextSelection(self, obj):
5074        try:
5075            text = obj.queryText()
5076        except NotImplementedError:
5077            msg = "ERROR: %s doesn't implement AtspiText" % obj
5078            debug.println(debug.LEVEL_INFO, msg, True)
5079            text = None
5080        except:
5081            msg = "ERROR: Exception querying text interface for %s" % obj
5082            debug.println(debug.LEVEL_INFO, msg, True)
5083            text = None
5084
5085        if self._script.pointOfReference.get('entireDocumentSelected'):
5086            selectedText, selectedStart, selectedEnd = self.allSelectedText(obj)
5087            if not selectedText:
5088                self._script.pointOfReference['entireDocumentSelected'] = False
5089                self._script.pointOfReference['textSelections'] = {}
5090
5091        textSelections = self._script.pointOfReference.get('textSelections', {})
5092
5093        # Because some apps and toolkits create, destroy, and duplicate objects
5094        # and events.
5095        if hash(obj) in textSelections:
5096            value = textSelections.pop(hash(obj))
5097            for x in [k for k in textSelections.keys() if textSelections.get(k) == value]:
5098                textSelections.pop(x)
5099
5100        # TODO: JD - this doesn't yet handle the case of multiple non-contiguous
5101        # selections in a single accessible object.
5102        start, end, string = 0, 0, ''
5103        if text:
5104            try:
5105                start, end = text.getSelection(0)
5106            except:
5107                msg = "ERROR: Exception getting selected text for %s" % obj
5108                debug.println(debug.LEVEL_INFO, msg, True)
5109                start = end = 0
5110            if start != end:
5111                string = text.getText(start, end)
5112
5113        msg = "INFO: New selection for %s is '%s' (%i, %i)" % (obj, string, start, end)
5114        debug.println(debug.LEVEL_INFO, msg, True)
5115        textSelections[hash(obj)] = start, end, string
5116        self._script.pointOfReference['textSelections'] = textSelections
5117
5118    @staticmethod
5119    def onClipboardContentsChanged(*args):
5120        script = orca_state.activeScript
5121        if not script:
5122            return
5123
5124        if time.time() - Utilities._last_clipboard_update < 0.05:
5125            msg = "INFO: Clipboard contents change notification believed to be duplicate"
5126            debug.println(debug.LEVEL_INFO, msg, True)
5127            return
5128
5129        Utilities._last_clipboard_update = time.time()
5130        script.onClipboardContentsChanged(*args)
5131
5132    def connectToClipboard(self):
5133        if self._clipboardHandlerId is not None:
5134            return
5135
5136        clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
5137        self._clipboardHandlerId = clipboard.connect(
5138            'owner-change', self.onClipboardContentsChanged)
5139
5140    def disconnectFromClipboard(self):
5141        if self._clipboardHandlerId is None:
5142            return
5143
5144        clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
5145        clipboard.disconnect(self._clipboardHandlerId)
5146
5147    def getClipboardContents(self):
5148        clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
5149        return clipboard.wait_for_text()
5150
5151    def setClipboardText(self, text):
5152        clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
5153        clipboard.set_text(text, -1)
5154
5155    def appendTextToClipboard(self, text):
5156        clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
5157        clipboard.request_text(self._appendTextToClipboardCallback, text)
5158
5159    def _appendTextToClipboardCallback(self, clipboard, text, newText, separator="\n"):
5160        text = text.rstrip("\n")
5161        text = "%s%s%s" % (text, separator, newText)
5162        clipboard.set_text(text, -1)
5163
5164    def lastInputEventCameFromThisApp(self):
5165        if not isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
5166            return False
5167
5168        event = orca_state.lastNonModifierKeyEvent
5169        return event and event.isFromApplication(self._script.app)
5170
5171    def lastInputEventWasPrintableKey(self):
5172        event = orca_state.lastInputEvent
5173        if not isinstance(event, input_event.KeyboardEvent):
5174            return False
5175
5176        return event.isPrintableKey()
5177
5178    def lastInputEventWasCommand(self):
5179        keyString, mods = self.lastKeyAndModifiers()
5180        return mods & keybindings.CTRL_MODIFIER_MASK
5181
5182    def lastInputEventWasPageSwitch(self):
5183        keyString, mods = self.lastKeyAndModifiers()
5184        if keyString.isnumeric():
5185            return mods & keybindings.ALT_MODIFIER_MASK
5186
5187        if keyString in ["Page_Up", "Page_Down"]:
5188            return mods & keybindings.CTRL_MODIFIER_MASK
5189
5190        return False
5191
5192    def lastInputEventWasUnmodifiedArrow(self):
5193        keyString, mods = self.lastKeyAndModifiers()
5194        if not keyString in ["Left", "Right", "Up", "Down"]:
5195            return False
5196
5197        if mods & keybindings.CTRL_MODIFIER_MASK \
5198           or mods & keybindings.SHIFT_MODIFIER_MASK \
5199           or mods & keybindings.ALT_MODIFIER_MASK \
5200           or mods & keybindings.ORCA_MODIFIER_MASK:
5201            return False
5202
5203        return True
5204
5205    def lastInputEventWasCaretNav(self):
5206        return self.lastInputEventWasCharNav() \
5207            or self.lastInputEventWasWordNav() \
5208            or self.lastInputEventWasLineNav() \
5209            or self.lastInputEventWasLineBoundaryNav()
5210
5211    def lastInputEventWasCharNav(self):
5212        keyString, mods = self.lastKeyAndModifiers()
5213        if not keyString in ["Left", "Right"]:
5214            return False
5215
5216        return not (mods & keybindings.CTRL_MODIFIER_MASK)
5217
5218    def lastInputEventWasWordNav(self):
5219        keyString, mods = self.lastKeyAndModifiers()
5220        if not keyString in ["Left", "Right"]:
5221            return False
5222
5223        return mods & keybindings.CTRL_MODIFIER_MASK
5224
5225    def lastInputEventWasPrevWordNav(self):
5226        keyString, mods = self.lastKeyAndModifiers()
5227        if not keyString == "Left":
5228            return False
5229
5230        return mods & keybindings.CTRL_MODIFIER_MASK
5231
5232    def lastInputEventWasNextWordNav(self):
5233        keyString, mods = self.lastKeyAndModifiers()
5234        if not keyString == "Right":
5235            return False
5236
5237        return mods & keybindings.CTRL_MODIFIER_MASK
5238
5239    def lastInputEventWasLineNav(self):
5240        keyString, mods = self.lastKeyAndModifiers()
5241        if not keyString in ["Up", "Down"]:
5242            return False
5243
5244        if self.isEditableDescendantOfComboBox(orca_state.locusOfFocus):
5245            return False
5246
5247        return not (mods & keybindings.CTRL_MODIFIER_MASK)
5248
5249    def lastInputEventWasLineBoundaryNav(self):
5250        keyString, mods = self.lastKeyAndModifiers()
5251        if not keyString in ["Home", "End"]:
5252            return False
5253
5254        return not (mods & keybindings.CTRL_MODIFIER_MASK)
5255
5256    def lastInputEventWasPageNav(self):
5257        keyString, mods = self.lastKeyAndModifiers()
5258        if not keyString in ["Page_Up", "Page_Down"]:
5259            return False
5260
5261        if self.isEditableDescendantOfComboBox(orca_state.locusOfFocus):
5262            return False
5263
5264        return not (mods & keybindings.CTRL_MODIFIER_MASK)
5265
5266    def lastInputEventWasFileBoundaryNav(self):
5267        keyString, mods = self.lastKeyAndModifiers()
5268        if not keyString in ["Home", "End"]:
5269            return False
5270
5271        return mods & keybindings.CTRL_MODIFIER_MASK
5272
5273    def lastInputEventWasCaretNavWithSelection(self):
5274        keyString, mods = self.lastKeyAndModifiers()
5275        if mods & keybindings.SHIFT_MODIFIER_MASK:
5276            return keyString in ["Home", "End", "Up", "Down", "Left", "Right"]
5277
5278        return False
5279
5280    def lastInputEventWasUndo(self):
5281        keycode, mods = self._lastKeyCodeAndModifiers()
5282        keynames = self._allNamesForKeyCode(keycode)
5283        if 'z' not in keynames:
5284            return False
5285
5286        if mods & keybindings.CTRL_MODIFIER_MASK:
5287            return not (mods & keybindings.SHIFT_MODIFIER_MASK)
5288
5289        return False
5290
5291    def lastInputEventWasRedo(self):
5292        keycode, mods = self._lastKeyCodeAndModifiers()
5293        keynames = self._allNamesForKeyCode(keycode)
5294        if 'z' not in keynames:
5295            return False
5296
5297        if mods & keybindings.CTRL_MODIFIER_MASK:
5298            return mods & keybindings.SHIFT_MODIFIER_MASK
5299
5300        return False
5301
5302    def lastInputEventWasCut(self):
5303        keycode, mods = self._lastKeyCodeAndModifiers()
5304        keynames = self._allNamesForKeyCode(keycode)
5305        if 'x' not in keynames:
5306            return False
5307
5308        if mods & keybindings.CTRL_MODIFIER_MASK:
5309            return not (mods & keybindings.SHIFT_MODIFIER_MASK)
5310
5311        return False
5312
5313    def lastInputEventWasCopy(self):
5314        keycode, mods = self._lastKeyCodeAndModifiers()
5315        keynames = self._allNamesForKeyCode(keycode)
5316        if 'c' not in keynames:
5317            return False
5318
5319        if mods & keybindings.CTRL_MODIFIER_MASK:
5320            return not (mods & keybindings.SHIFT_MODIFIER_MASK)
5321
5322        return False
5323
5324    def lastInputEventWasPaste(self):
5325        keycode, mods = self._lastKeyCodeAndModifiers()
5326        keynames = self._allNamesForKeyCode(keycode)
5327        if 'v' not in keynames:
5328            return False
5329
5330        if mods & keybindings.CTRL_MODIFIER_MASK:
5331            return not (mods & keybindings.SHIFT_MODIFIER_MASK)
5332
5333        return False
5334
5335    def lastInputEventWasSelectAll(self):
5336        keycode, mods = self._lastKeyCodeAndModifiers()
5337        keynames = self._allNamesForKeyCode(keycode)
5338        if 'a' not in keynames:
5339            return False
5340
5341        if mods & keybindings.CTRL_MODIFIER_MASK:
5342            return not (mods & keybindings.SHIFT_MODIFIER_MASK)
5343
5344        return False
5345
5346    def lastInputEventWasDelete(self):
5347        keyString, mods = self.lastKeyAndModifiers()
5348        if keyString == "Delete":
5349            return True
5350
5351        keycode, mods = self._lastKeyCodeAndModifiers()
5352        keynames = self._allNamesForKeyCode(keycode)
5353        if 'd' not in keynames:
5354            return False
5355
5356        return mods & keybindings.CTRL_MODIFIER_MASK
5357
5358    def lastInputEventWasTab(self):
5359        keyString, mods = self.lastKeyAndModifiers()
5360        if keyString not in ["Tab", "ISO_Left_Tab"]:
5361            return False
5362
5363        if mods & keybindings.CTRL_MODIFIER_MASK \
5364           or mods & keybindings.ALT_MODIFIER_MASK \
5365           or mods & keybindings.ORCA_MODIFIER_MASK:
5366            return False
5367
5368        return True
5369
5370    def lastInputEventWasMouseButton(self):
5371        return isinstance(orca_state.lastInputEvent, input_event.MouseButtonEvent)
5372
5373    def lastInputEventWasPrimaryMouseClick(self):
5374        event = orca_state.lastInputEvent
5375        if isinstance(event, input_event.MouseButtonEvent):
5376            return event.button == "1" and event.pressed
5377
5378        return False
5379
5380    def lastInputEventWasMiddleMouseClick(self):
5381        event = orca_state.lastInputEvent
5382        if isinstance(event, input_event.MouseButtonEvent):
5383            return event.button == "2" and event.pressed
5384
5385        return False
5386
5387    def lastInputEventWasSecondaryMouseClick(self):
5388        event = orca_state.lastInputEvent
5389        if isinstance(event, input_event.MouseButtonEvent):
5390            return event.button == "3" and event.pressed
5391
5392        return False
5393
5394    def lastInputEventWasPrimaryMouseRelease(self):
5395        event = orca_state.lastInputEvent
5396        if isinstance(event, input_event.MouseButtonEvent):
5397            return event.button == "1" and not event.pressed
5398
5399        return False
5400
5401    def lastInputEventWasMiddleMouseRelease(self):
5402        event = orca_state.lastInputEvent
5403        if isinstance(event, input_event.MouseButtonEvent):
5404            return event.button == "2" and not event.pressed
5405
5406        return False
5407
5408    def lastInputEventWasSecondaryMouseRelease(self):
5409        event = orca_state.lastInputEvent
5410        if isinstance(event, input_event.MouseButtonEvent):
5411            return event.button == "3" and not event.pressed
5412
5413        return False
5414
5415    def lastInputEventWasTableSort(self, delta=0.5):
5416        event = orca_state.lastInputEvent
5417        if not event:
5418            return False
5419
5420        now = time.time()
5421        if now - event.time > delta:
5422            return False
5423
5424        lastSortTime = self._script.pointOfReference.get('last-table-sort-time', 0.0)
5425        if now - lastSortTime < delta:
5426            return False
5427
5428        if isinstance(event, input_event.MouseButtonEvent):
5429            if not self.lastInputEventWasPrimaryMouseRelease():
5430                return False
5431        elif isinstance(event, input_event.KeyboardEvent):
5432            if not event.isHandledBy(self._script.leftClickReviewItem):
5433                keyString, mods = self.lastKeyAndModifiers()
5434                if keyString not in ["Return", "space", " "]:
5435                    return False
5436
5437        try:
5438            role = orca_state.locusOfFocus.getRole()
5439        except:
5440            msg = "ERROR: Exception getting role for %s" % orca_state.locusOfFocus
5441            debug.println(debug.LEVEL_INFO, msg, True)
5442            return False
5443
5444        roles = [pyatspi.ROLE_COLUMN_HEADER,
5445                 pyatspi.ROLE_ROW_HEADER,
5446                 pyatspi.ROLE_TABLE_COLUMN_HEADER,
5447                 pyatspi.ROLE_TABLE_ROW_HEADER]
5448
5449        return role in roles
5450
5451    def isPresentableExpandedChangedEvent(self, event):
5452        if self.isSameObject(event.source, orca_state.locusOfFocus):
5453            return True
5454
5455        try:
5456            role = event.source.getRole()
5457            state = event.source.getState()
5458        except:
5459            msg = "ERROR: Exception getting role and state of %s" % event.source
5460            debug.println(debug.LEVEL_INFO, msg, True)
5461            return False
5462
5463        if role in [pyatspi.ROLE_TABLE_ROW, pyatspi.ROLE_LIST_BOX]:
5464            return True
5465
5466        if role == pyatspi.ROLE_COMBO_BOX:
5467            return state.contains(pyatspi.STATE_FOCUSED)
5468
5469        if role == pyatspi.ROLE_PUSH_BUTTON:
5470            return state.contains(pyatspi.STATE_FOCUSED)
5471
5472        return False
5473
5474    def isPresentableTextChangedEventForLocusOfFocus(self, event):
5475        if not event.type.startswith("object:text-changed:") \
5476           and not event.type.startswith("object:text-attributes-changed"):
5477            return False
5478
5479        try:
5480            role = event.source.getRole()
5481            state = event.source.getState()
5482        except:
5483            msg = "ERROR: Exception getting role and state of %s" % event.source
5484            debug.println(debug.LEVEL_INFO, msg, True)
5485            return False
5486
5487        ignoreRoles = [pyatspi.ROLE_LABEL,
5488                       pyatspi.ROLE_MENU,
5489                       pyatspi.ROLE_MENU_ITEM,
5490                       pyatspi.ROLE_SLIDER,
5491                       pyatspi.ROLE_SPIN_BUTTON]
5492        if role in ignoreRoles:
5493            msg = "INFO: Event is not being presented due to role"
5494            debug.println(debug.LEVEL_INFO, msg, True)
5495            return False
5496
5497        if role == pyatspi.ROLE_TABLE_CELL \
5498           and not state.contains(pyatspi.STATE_FOCUSED) \
5499           and not state.contains(pyatspi.STATE_SELECTED):
5500            msg = "INFO: Event is not being presented due to role and states"
5501            debug.println(debug.LEVEL_INFO, msg, True)
5502            return False
5503
5504        if self.isTypeahead(event.source):
5505            return state.contains(pyatspi.STATE_FOCUSED)
5506
5507        if role == pyatspi.ROLE_PASSWORD_TEXT and state.contains(pyatspi.STATE_FOCUSED):
5508            return True
5509
5510        if orca_state.locusOfFocus in [event.source, event.source.parent]:
5511            return True
5512
5513        if self.isDead(orca_state.locusOfFocus):
5514            return True
5515
5516        msg = "INFO: Event is not being presented due to lack of cause"
5517        debug.println(debug.LEVEL_INFO, msg, True)
5518        return False
5519
5520    def isBackSpaceCommandTextDeletionEvent(self, event):
5521        if not event.type.startswith("object:text-changed:delete"):
5522            return False
5523
5524        if self.isHidden(event.source):
5525            return False
5526
5527        keyString, mods = self.lastKeyAndModifiers()
5528        if keyString == "BackSpace":
5529            return True
5530
5531        return False
5532
5533    def isDeleteCommandTextDeletionEvent(self, event):
5534        if not event.type.startswith("object:text-changed:delete"):
5535            return False
5536
5537        return self.lastInputEventWasDelete()
5538
5539    def isUndoCommandTextDeletionEvent(self, event):
5540        if not event.type.startswith("object:text-changed:delete"):
5541            return False
5542
5543        if not self.lastInputEventWasUndo():
5544            return False
5545
5546        start, end, string = self.getCachedTextSelection(event.source)
5547        return not string
5548
5549    def isSelectedTextDeletionEvent(self, event):
5550        if not event.type.startswith("object:text-changed:delete"):
5551            return False
5552
5553        if self.lastInputEventWasPaste():
5554            return False
5555
5556        start, end, string = self.getCachedTextSelection(event.source)
5557        return string and string.strip() == event.any_data.strip()
5558
5559    def isSelectedTextInsertionEvent(self, event):
5560        if not event.type.startswith("object:text-changed:insert"):
5561            return False
5562
5563        self.updateCachedTextSelection(event.source)
5564        start, end, string = self.getCachedTextSelection(event.source)
5565        return string and string == event.any_data and start == event.detail1
5566
5567    def isSelectedTextRestoredEvent(self, event):
5568        if not self.lastInputEventWasUndo():
5569            return False
5570
5571        if self.isSelectedTextInsertionEvent(event):
5572            return True
5573
5574        return False
5575
5576    def isMiddleMouseButtonTextInsertionEvent(self, event):
5577        if not event.type.startswith("object:text-changed:insert"):
5578            return False
5579
5580        return self.lastInputEventWasMiddleMouseClick()
5581
5582    def isEchoableTextInsertionEvent(self, event):
5583        if not event.type.startswith("object:text-changed:insert"):
5584            return False
5585
5586        try:
5587            role = event.source.getRole()
5588            state = event.source.getState()
5589        except:
5590            msg = "ERROR: Exception getting role and state of %s" % event.source
5591            debug.println(debug.LEVEL_INFO, msg, True)
5592            return False
5593
5594        if state.contains(pyatspi.STATE_FOCUSABLE) and not state.contains(pyatspi.STATE_FOCUSED) \
5595           and event.source != orca_state.locusOfFocus:
5596            msg = "INFO: Not echoable text insertion event: focusable source is not focused"
5597            debug.println(debug.LEVEL_INFO, msg, True)
5598            return False
5599
5600        if role == pyatspi.ROLE_PASSWORD_TEXT:
5601            return _settingsManager.getSetting("enableKeyEcho")
5602
5603        if len(event.any_data.strip()) == 1:
5604            return _settingsManager.getSetting("enableEchoByCharacter")
5605
5606        return False
5607
5608    def isEditableTextArea(self, obj):
5609        if not self.isTextArea(obj):
5610            return False
5611
5612        try:
5613            state = obj.getState()
5614        except:
5615            msg = "ERROR: Exception getting state of %s" % obj
5616            debug.println(debug.LEVEL_INFO, msg, True)
5617            return False
5618
5619        return state.contains(pyatspi.STATE_EDITABLE)
5620
5621    def isClipboardTextChangedEvent(self, event):
5622        if not event.type.startswith("object:text-changed"):
5623            return False
5624
5625        if not self.lastInputEventWasCommand() or self.lastInputEventWasUndo():
5626            return False
5627
5628        if self.isBackSpaceCommandTextDeletionEvent(event):
5629            return False
5630
5631        if "delete" in event.type and self.lastInputEventWasPaste():
5632            return False
5633
5634        if not self.isEditableTextArea(event.source):
5635            return False
5636
5637        contents = self.getClipboardContents()
5638        if not contents:
5639            return False
5640        if event.any_data == contents:
5641            return True
5642        if bool(re.search(r"\w", event.any_data)) != bool(re.search(r"\w", contents)):
5643            return False
5644
5645        # HACK: If the application treats each paragraph as a separate object,
5646        # we'll get individual events for each paragraph rather than a single
5647        # event whose any_data matches the clipboard contents.
5648        if "\n" in contents and event.any_data.rstrip() in contents:
5649            return True
5650
5651        return False
5652
5653    def objectContentsAreInClipboard(self, obj=None):
5654        obj = obj or orca_state.locusOfFocus
5655        if not obj or self.isDead(obj):
5656            return False
5657
5658        contents = self.getClipboardContents()
5659        if not contents:
5660            return False
5661
5662        string, start, end = self.selectedText(obj)
5663        if string and string in contents:
5664            return True
5665
5666        obj = self.realActiveDescendant(obj) or obj
5667        if self.isDead(obj):
5668            return False
5669
5670        return obj and obj.name in contents
5671
5672    def clearCachedCommandState(self):
5673        self._script.pointOfReference['undo'] = False
5674        self._script.pointOfReference['redo'] = False
5675        self._script.pointOfReference['paste'] = False
5676        self._script.pointOfReference['last-selection-message'] = ''
5677
5678    def handleUndoTextEvent(self, event):
5679        if self.lastInputEventWasUndo():
5680            if not self._script.pointOfReference.get('undo'):
5681                self._script.presentMessage(messages.UNDO)
5682                self._script.pointOfReference['undo'] = True
5683            self.updateCachedTextSelection(event.source)
5684            return True
5685
5686        if self.lastInputEventWasRedo():
5687            if not self._script.pointOfReference.get('redo'):
5688                self._script.presentMessage(messages.REDO)
5689                self._script.pointOfReference['redo'] = True
5690            self.updateCachedTextSelection(event.source)
5691            return True
5692
5693        return False
5694
5695    def handleUndoLocusOfFocusChange(self):
5696        if self._locusOfFocusIsTopLevelObject():
5697            return False
5698
5699        if self.lastInputEventWasUndo():
5700            if not self._script.pointOfReference.get('undo'):
5701                self._script.presentMessage(messages.UNDO)
5702                self._script.pointOfReference['undo'] = True
5703            return True
5704
5705        if self.lastInputEventWasRedo():
5706            if not self._script.pointOfReference.get('redo'):
5707                self._script.presentMessage(messages.REDO)
5708                self._script.pointOfReference['redo'] = True
5709            return True
5710
5711        return False
5712
5713    def handlePasteLocusOfFocusChange(self):
5714        if self._locusOfFocusIsTopLevelObject():
5715            return False
5716
5717        if self.lastInputEventWasPaste():
5718            if not self._script.pointOfReference.get('paste'):
5719                self._script.presentMessage(
5720                    messages.CLIPBOARD_PASTED_FULL, messages.CLIPBOARD_PASTED_BRIEF)
5721                self._script.pointOfReference['paste'] = True
5722            return True
5723
5724        return False
5725
5726    def eventIsUserTriggered(self, event):
5727        if not orca_state.lastInputEvent:
5728            msg = "INFO: Not user triggered: No last input event."
5729            debug.println(debug.LEVEL_INFO, msg, True)
5730            return False
5731
5732        delta = time.time() - orca_state.lastInputEvent.time
5733        if delta > 1:
5734            msg = "INFO: Not user triggered: Last input event %.2fs ago." % delta
5735            debug.println(debug.LEVEL_INFO, msg, True)
5736            return False
5737
5738        if self.isKeyGrabEvent(event):
5739            msg = "INFO: Last key was consumed. Probably a bogus event from a key grab"
5740            debug.println(debug.LEVEL_INFO, msg, True)
5741            return False
5742
5743        return True
5744
5745    def isKeyGrabEvent(self, event):
5746        """ Returns True if this event appears to be a side-effect of an
5747        X11 key grab. """
5748        if not isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
5749            return False
5750        return orca_state.lastInputEvent.didConsume() and not orca_state.openingDialog
5751
5752    def presentFocusChangeReason(self):
5753        if self.handleUndoLocusOfFocusChange():
5754            return True
5755        if self.handlePasteLocusOfFocusChange():
5756            return True
5757        return False
5758
5759    def allItemsSelected(self, obj):
5760        interfaces = pyatspi.listInterfaces(obj)
5761        if "Selection" not in interfaces:
5762            return False
5763
5764        state = obj.getState()
5765        if state.contains(pyatspi.STATE_EXPANDABLE) \
5766           and not state.contains(pyatspi.STATE_EXPANDED):
5767            return False
5768
5769        role = obj.getRole()
5770        if role in [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_MENU]:
5771            return False
5772
5773        selection = obj.querySelection()
5774        if not selection.nSelectedChildren:
5775            return False
5776
5777        if self.selectedChildCount(obj) == obj.childCount:
5778            # The selection interface gives us access to what is selected, which might
5779            # not actually be a direct child.
5780            child = selection.getSelectedChild(0)
5781            if child not in obj:
5782                return False
5783
5784            msg = "INFO: All %i children believed to be selected" % obj.childCount
5785            debug.println(debug.LEVEL_INFO, msg, True)
5786            return True
5787
5788        if "Table" not in interfaces:
5789            return False
5790
5791        table = obj.queryTable()
5792        if table.nSelectedRows == table.nRows:
5793            msg = "INFO: All %i rows believed to be selected" % table.nRows
5794            debug.println(debug.LEVEL_INFO, msg, True)
5795            return True
5796
5797        if table.nSelectedColumns == table.nColumns:
5798            msg = "INFO: All %i columns believed to be selected" % table.nColumns
5799            debug.println(debug.LEVEL_INFO, msg, True)
5800            return True
5801
5802        return False
5803
5804    def handleContainerSelectionChange(self, obj):
5805        allAlreadySelected = self._script.pointOfReference.get('allItemsSelected')
5806        allCurrentlySelected = self.allItemsSelected(obj)
5807        if allAlreadySelected and allCurrentlySelected:
5808            return True
5809
5810        self._script.pointOfReference['allItemsSelected'] = allCurrentlySelected
5811        if self.lastInputEventWasSelectAll() and allCurrentlySelected:
5812            self._script.presentMessage(messages.CONTAINER_SELECTED_ALL)
5813            orca.setLocusOfFocus(None, obj, False)
5814            return True
5815
5816        return False
5817
5818    def _findSelectionBoundaryObject(self, root, findStart=True):
5819        try:
5820            text = root.queryText()
5821            childCount = root.childCount
5822        except:
5823            msg = "ERROR: Exception querying text and getting childCount for %s" % root
5824            debug.println(debug.LEVEL_INFO, msg, True)
5825            return None
5826
5827        if not text.getNSelections():
5828            return None
5829
5830        start, end = text.getSelection(0)
5831        string = text.getText(start, end)
5832        if not string:
5833            return None
5834
5835        if findStart and not string.startswith(self.EMBEDDED_OBJECT_CHARACTER):
5836            return root
5837
5838        if not findStart and not string.endswith(self.EMBEDDED_OBJECT_CHARACTER):
5839            return root
5840
5841        indices = list(range(childCount))
5842        if not findStart:
5843            indices.reverse()
5844
5845        for i in indices:
5846            result = self._findSelectionBoundaryObject(root[i], findStart)
5847            if result:
5848                return result
5849
5850        return None
5851
5852    def _getSelectionAnchorAndFocus(self, root):
5853        # Any scripts which need to make a distinction between the anchor and
5854        # the focus should override this method.
5855        obj1 = self._findSelectionBoundaryObject(root, True)
5856        obj2 = self._findSelectionBoundaryObject(root, False)
5857        return obj1, obj2
5858
5859    def _getSubtree(self, startObj, endObj):
5860        if not (startObj and endObj):
5861            return []
5862
5863        _include = lambda x: x
5864        _exclude = self.isStaticTextLeaf
5865
5866        subtree = []
5867        for i in range(startObj.getIndexInParent(), startObj.parent.childCount):
5868            child = startObj.parent[i]
5869            if self.isStaticTextLeaf(child):
5870                continue
5871            subtree.append(child)
5872            subtree.extend(self.findAllDescendants(child, _include, _exclude))
5873            if endObj in subtree:
5874                break
5875
5876        if endObj == startObj:
5877            return subtree
5878
5879        if endObj not in subtree:
5880            subtree.append(endObj)
5881            subtree.extend(self.findAllDescendants(endObj, _include, _exclude))
5882
5883        try:
5884            lastObj = endObj.parent[endObj.getIndexInParent() + 1]
5885        except:
5886            lastObj = endObj
5887
5888        try:
5889            endIndex = subtree.index(lastObj)
5890        except ValueError:
5891            pass
5892        else:
5893            if lastObj == endObj:
5894                endIndex += 1
5895            subtree = subtree[:endIndex]
5896
5897        return subtree
5898
5899    def handleTextSelectionChange(self, obj, speakMessage=True):
5900        # Note: This guesswork to figure out what actually changed with respect
5901        # to text selection will get eliminated once the new text-selection API
5902        # is added to ATK and implemented by the toolkits. (BGO 638378)
5903
5904        if not (obj and 'Text' in pyatspi.listInterfaces(obj)):
5905            return False
5906
5907        oldStart, oldEnd, oldString = self.getCachedTextSelection(obj)
5908        self.updateCachedTextSelection(obj)
5909        newStart, newEnd, newString = self.getCachedTextSelection(obj)
5910
5911        if self._speakTextSelectionState(len(newString)):
5912            return True
5913
5914        changes = []
5915        oldChars = set(range(oldStart, oldEnd))
5916        newChars = set(range(newStart, newEnd))
5917        if not oldChars.union(newChars):
5918            return False
5919
5920        if oldChars and newChars and not oldChars.intersection(newChars):
5921            # A simultaneous unselection and selection centered at one offset.
5922            changes.append([oldStart, oldEnd, messages.TEXT_UNSELECTED])
5923            changes.append([newStart, newEnd, messages.TEXT_SELECTED])
5924        else:
5925            change = sorted(oldChars.symmetric_difference(newChars))
5926            if not change:
5927                return False
5928
5929            changeStart, changeEnd = change[0], change[-1] + 1
5930            if oldChars < newChars:
5931                changes.append([changeStart, changeEnd, messages.TEXT_SELECTED])
5932                if oldString.endswith(self.EMBEDDED_OBJECT_CHARACTER) and oldEnd == changeStart:
5933                    # There's a possibility that we have a link spanning multiple lines. If so,
5934                    # we want to present the continuation that just became selected.
5935                    child = self.getChildAtOffset(obj, oldEnd - 1)
5936                    self.handleTextSelectionChange(child, False)
5937            else:
5938                changes.append([changeStart, changeEnd, messages.TEXT_UNSELECTED])
5939                if newString.endswith(self.EMBEDDED_OBJECT_CHARACTER):
5940                    # There's a possibility that we have a link spanning multiple lines. If so,
5941                    # we want to present the continuation that just became unselected.
5942                    child = self.getChildAtOffset(obj, newEnd - 1)
5943                    self.handleTextSelectionChange(child, False)
5944
5945        speakMessage = speakMessage and not _settingsManager.getSetting('onlySpeakDisplayedText')
5946        text = obj.queryText()
5947        for start, end, message in changes:
5948            string = text.getText(start, end)
5949            endsWithChild = string.endswith(self.EMBEDDED_OBJECT_CHARACTER)
5950            if endsWithChild:
5951                end -= 1
5952
5953            self._script.sayPhrase(obj, start, end)
5954            if speakMessage and not endsWithChild:
5955                self._script.speakMessage(message, interrupt=False)
5956
5957            if endsWithChild:
5958                child = self.getChildAtOffset(obj, end)
5959                self.handleTextSelectionChange(child, speakMessage)
5960
5961        return True
5962
5963    def _getCtrlShiftSelectionsStrings(self):
5964        """Hacky and to-be-obsoleted method."""
5965        return [messages.PARAGRAPH_SELECTED_DOWN,
5966                messages.PARAGRAPH_UNSELECTED_DOWN,
5967                messages.PARAGRAPH_SELECTED_UP,
5968                messages.PARAGRAPH_UNSELECTED_UP]
5969
5970    def _speakTextSelectionState(self, nSelections):
5971        """Hacky and to-be-obsoleted method."""
5972
5973        if _settingsManager.getSetting('onlySpeakDisplayedText'):
5974            return False
5975
5976        eventStr, mods = self.lastKeyAndModifiers()
5977        isControlKey = mods & keybindings.CTRL_MODIFIER_MASK
5978        isShiftKey = mods & keybindings.SHIFT_MODIFIER_MASK
5979        selectedText = nSelections > 0
5980
5981        line = None
5982        if (eventStr == "Page_Down") and isShiftKey and isControlKey:
5983            line = messages.LINE_SELECTED_RIGHT
5984        elif (eventStr == "Page_Up") and isShiftKey and isControlKey:
5985            line = messages.LINE_SELECTED_LEFT
5986        elif (eventStr == "Page_Down") and isShiftKey and not isControlKey:
5987            if selectedText:
5988                line = messages.PAGE_SELECTED_DOWN
5989            else:
5990                line = messages.PAGE_UNSELECTED_DOWN
5991        elif (eventStr == "Page_Up") and isShiftKey and not isControlKey:
5992            if selectedText:
5993                line = messages.PAGE_SELECTED_UP
5994            else:
5995                line = messages.PAGE_UNSELECTED_UP
5996        elif (eventStr == "Down") and isShiftKey and isControlKey:
5997            strings = self._getCtrlShiftSelectionsStrings()
5998            if selectedText:
5999                line = strings[0]
6000            else:
6001                line = strings[1]
6002        elif (eventStr == "Up") and isShiftKey and isControlKey:
6003            strings = self._getCtrlShiftSelectionsStrings()
6004            if selectedText:
6005                line = strings[2]
6006            else:
6007                line = strings[3]
6008        elif (eventStr == "Home") and isShiftKey and isControlKey:
6009            if selectedText:
6010                line = messages.DOCUMENT_SELECTED_UP
6011            else:
6012                line = messages.DOCUMENT_UNSELECTED_UP
6013        elif (eventStr == "End") and isShiftKey and isControlKey:
6014            if selectedText:
6015                line = messages.DOCUMENT_SELECTED_DOWN
6016            else:
6017                line = messages.DOCUMENT_SELECTED_UP
6018        elif self.lastInputEventWasSelectAll() and selectedText:
6019            if not self._script.pointOfReference.get('entireDocumentSelected'):
6020                self._script.pointOfReference['entireDocumentSelected'] = True
6021                line = messages.DOCUMENT_SELECTED_ALL
6022            else:
6023                return True
6024
6025        if not line:
6026            return False
6027
6028        if line != self._script.pointOfReference.get('last-selection-message'):
6029            self._script.pointOfReference['last-selection-message'] = line
6030            self._script.speakMessage(line)
6031
6032        return True
6033