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