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