1# Orca 2# 3# Copyright 2005-2009 Sun Microsystems Inc. 4# Copyright 2010-2011 Orca Team 5# Copyright 2011-2015 Igalia, S.L. 6# 7# This library is free software; you can redistribute it and/or 8# modify it under the terms of the GNU Lesser General Public 9# License as published by the Free Software Foundation; either 10# version 2.1 of the License, or (at your option) any later version. 11# 12# This library is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15# Lesser General Public License for more details. 16# 17# You should have received a copy of the GNU Lesser General Public 18# License along with this library; if not, write to the 19# Free Software Foundation, Inc., Franklin Street, Fifth Floor, 20# Boston MA 02110-1301 USA. 21 22__id__ = "$Id$" 23__version__ = "$Revision$" 24__date__ = "$Date$" 25__copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." \ 26 "Copyright (c) 2010-2011 Orca Team" \ 27 "Copyright (c) 2011-2015 Igalia, S.L." 28__license__ = "LGPL" 29 30import pyatspi 31import urllib 32 33from orca import debug 34from orca import messages 35from orca import object_properties 36from orca import orca_state 37from orca import settings 38from orca import settings_manager 39from orca import speech_generator 40 41_settingsManager = settings_manager.getManager() 42 43 44class SpeechGenerator(speech_generator.SpeechGenerator): 45 46 def __init__(self, script): 47 super().__init__(script) 48 49 def _generateOldAncestors(self, obj, **args): 50 if args.get('index', 0) > 0: 51 return [] 52 53 priorObj = args.get('priorObj') 54 if self._script.utilities.isInlineIframeDescendant(priorObj): 55 return [] 56 57 return super()._generateOldAncestors(obj, **args) 58 59 def _generateNewAncestors(self, obj, **args): 60 if args.get('index', 0) > 0 \ 61 and not self._script.utilities.isListDescendant(obj): 62 return [] 63 64 if self._script.utilities.isInlineIframeDescendant(obj): 65 return [] 66 67 return super()._generateNewAncestors(obj, **args) 68 69 def _generateAncestors(self, obj, **args): 70 if not self._script.utilities.inDocumentContent(obj): 71 return super()._generateAncestors(obj, **args) 72 73 if self._script.inSayAll() and obj == orca_state.locusOfFocus: 74 return [] 75 76 result = [] 77 priorObj = args.get('priorObj') 78 if priorObj and self._script.utilities.inDocumentContent(priorObj): 79 priorDoc = self._script.utilities.getDocumentForObject(priorObj) 80 doc = self._script.utilities.getDocumentForObject(obj) 81 if priorDoc != doc and not self._script.utilities.getDocumentForObject(doc): 82 result = [super()._generateName(doc)] 83 84 if self._script.utilities.isLink(obj) \ 85 or self._script.utilities.isLandmark(obj) \ 86 or self._script.utilities.isMath(obj) \ 87 or obj.getRole() in [pyatspi.ROLE_TOOL_TIP, pyatspi.ROLE_STATUS_BAR]: 88 return result 89 90 args['stopAtRoles'] = [pyatspi.ROLE_DOCUMENT_FRAME, 91 pyatspi.ROLE_DOCUMENT_WEB, 92 pyatspi.ROLE_EMBEDDED, 93 pyatspi.ROLE_INTERNAL_FRAME, 94 pyatspi.ROLE_MATH, 95 pyatspi.ROLE_MENU_BAR] 96 args['skipRoles'] = [pyatspi.ROLE_PARAGRAPH, 97 pyatspi.ROLE_HEADING, 98 pyatspi.ROLE_LABEL, 99 pyatspi.ROLE_LINK, 100 pyatspi.ROLE_LIST_ITEM, 101 pyatspi.ROLE_TEXT] 102 args['stopAfterRoles'] = [pyatspi.ROLE_TOOL_BAR] 103 104 if self._script.utilities.isEditableDescendantOfComboBox(obj): 105 args['skipRoles'].append(pyatspi.ROLE_COMBO_BOX) 106 107 result.extend(super()._generateAncestors(obj, **args)) 108 109 return result 110 111 def _generateAllTextSelection(self, obj, **args): 112 if self._script.utilities.isZombie(obj) \ 113 or obj != orca_state.locusOfFocus: 114 return [] 115 116 # TODO - JD: These (and the default script's) need to 117 # call utility methods rather than generate it. 118 return super()._generateAllTextSelection(obj, **args) 119 120 def _generateAnyTextSelection(self, obj, **args): 121 if self._script.utilities.isZombie(obj) \ 122 or obj != orca_state.locusOfFocus: 123 return [] 124 125 # TODO - JD: These (and the default script's) need to 126 # call utility methods rather than generate it. 127 return super()._generateAnyTextSelection(obj, **args) 128 129 def _generateHasPopup(self, obj, **args): 130 if _settingsManager.getSetting('onlySpeakDisplayedText'): 131 return [] 132 133 if not self._script.utilities.inDocumentContent(obj): 134 return [] 135 136 result = [] 137 popupType = self._script.utilities.popupType(obj) 138 if popupType == 'dialog': 139 result = [messages.HAS_POPUP_DIALOG] 140 elif popupType == 'grid': 141 result = [messages.HAS_POPUP_GRID] 142 elif popupType == 'listbox': 143 result = [messages.HAS_POPUP_LISTBOX] 144 elif popupType in ('menu', 'true'): 145 result = [messages.HAS_POPUP_MENU] 146 elif popupType == 'tree': 147 result = [messages.HAS_POPUP_TREE] 148 149 if result: 150 result.extend(self.voice(speech_generator.SYSTEM)) 151 152 return result 153 154 def _generateClickable(self, obj, **args): 155 if _settingsManager.getSetting('onlySpeakDisplayedText'): 156 return [] 157 158 if not self._script.utilities.inDocumentContent(obj): 159 return [] 160 161 if not args.get('mode', None): 162 args['mode'] = self._mode 163 164 args['stringType'] = 'clickable' 165 if self._script.utilities.isClickableElement(obj): 166 result = [self._script.formatting.getString(**args)] 167 result.extend(self.voice(speech_generator.SYSTEM)) 168 return result 169 170 return [] 171 172 def _generateDescription(self, obj, **args): 173 if _settingsManager.getSetting('onlySpeakDisplayedText'): 174 return [] 175 176 if not self._script.utilities.inDocumentContent(obj): 177 return super()._generateDescription(obj, **args) 178 179 if self._script.utilities.isZombie(obj): 180 return [] 181 182 if self._script.utilities.preferDescriptionOverName(obj): 183 return [] 184 185 role = args.get('role', obj.getRole()) 186 if obj != orca_state.locusOfFocus: 187 if role in [pyatspi.ROLE_ALERT, pyatspi.ROLE_DIALOG]: 188 return super()._generateDescription(obj, **args) 189 if not args.get('inMouseReview'): 190 return [] 191 192 formatType = args.get('formatType') 193 if formatType == 'basicWhereAmI' and self._script.utilities.isLiveRegion(obj): 194 return self._script.liveRegionManager.generateLiveRegionDescription(obj, **args) 195 196 if role == pyatspi.ROLE_TEXT and formatType != 'basicWhereAmI': 197 return [] 198 199 # TODO - JD: This is private. 200 if role == pyatspi.ROLE_LINK and self._script._lastCommandWasCaretNav: 201 return [] 202 203 return super()._generateDescription(obj, **args) 204 205 def _generateHasLongDesc(self, obj, **args): 206 if _settingsManager.getSetting('onlySpeakDisplayedText'): 207 return [] 208 209 if not self._script.utilities.inDocumentContent(obj): 210 return [] 211 212 if not args.get('mode', None): 213 args['mode'] = self._mode 214 215 args['stringType'] = 'haslongdesc' 216 if self._script.utilities.hasLongDesc(obj): 217 result = [self._script.formatting.getString(**args)] 218 result.extend(self.voice(speech_generator.SYSTEM)) 219 return result 220 221 return [] 222 223 def _generateHasDetails(self, obj, **args): 224 if _settingsManager.getSetting('onlySpeakDisplayedText'): 225 return [] 226 227 if not self._script.utilities.inDocumentContent(obj): 228 return super()._generateHasDetails(obj, **args) 229 230 objs = self._script.utilities.detailsIn(obj) 231 if not objs: 232 return [] 233 234 objString = lambda x: str.strip("%s %s" % (x.name, self.getLocalizedRoleName(x))) 235 toPresent = ", ".join(set(map(objString, objs))) 236 237 args['stringType'] = 'hasdetails' 238 result = [self._script.formatting.getString(**args) % toPresent] 239 result.extend(self.voice(speech_generator.SYSTEM)) 240 return result 241 242 def _generateAllDetails(self, obj, **args): 243 if _settingsManager.getSetting('onlySpeakDisplayedText'): 244 return [] 245 246 objs = self._script.utilities.detailsIn(obj) 247 if not objs: 248 container = pyatspi.findAncestor(obj, self._script.utilities.hasDetails) 249 objs = self._script.utilities.detailsIn(container) 250 251 if not objs: 252 return [] 253 254 args['stringType'] = 'hasdetails' 255 result = [self._script.formatting.getString(**args) % ""] 256 result.extend(self.voice(speech_generator.SYSTEM)) 257 258 result = [] 259 for o in objs: 260 result.append(self.getLocalizedRoleName(o)) 261 result.extend(self.voice(speech_generator.SYSTEM)) 262 263 string = self._script.utilities.expandEOCs(o) 264 if not string.strip(): 265 continue 266 267 result.append(string) 268 result.extend(self.voice(speech_generator.DEFAULT)) 269 result.extend(self._generatePause(o)) 270 271 return result 272 273 def _generateDetailsFor(self, obj, **args): 274 if _settingsManager.getSetting('onlySpeakDisplayedText'): 275 return [] 276 277 if not self._script.utilities.inDocumentContent(obj): 278 return super()._generateDetailsFor(obj, **args) 279 280 objs = self._script.utilities.detailsFor(obj) 281 if not objs: 282 return [] 283 284 if args.get('leaving'): 285 return [] 286 287 lastKey, mods = self._script.utilities.lastKeyAndModifiers() 288 if (lastKey in ['Down', 'Right'] or self._script.inSayAll()) and args.get('startOffset'): 289 return [] 290 if lastKey in ['Up', 'Left']: 291 text = self._script.utilities.queryNonEmptyText(obj) 292 if text and args.get('endOffset') not in [None, text.characterCount]: 293 return [] 294 295 result = [] 296 objArgs = {'stringType': 'detailsfor', 'mode': args.get('mode')} 297 for o in objs: 298 string = self._script.utilities.displayedText(o) or self.getLocalizedRoleName(o) 299 words = string.split() 300 if len(words) > 5: 301 words = words[0:5] + ['...'] 302 303 result.append(self._script.formatting.getString(**objArgs) % " ".join(words)) 304 result.extend(self.voice(speech_generator.SYSTEM)) 305 result.extend(self._generatePause(o, **objArgs)) 306 307 return result 308 309 def _generateLabelOrName(self, obj, **args): 310 if not self._script.utilities.inDocumentContent(obj): 311 return super()._generateLabelOrName(obj, **args) 312 313 if self._script.utilities.isTextBlockElement(obj) \ 314 and not self._script.utilities.isLandmark(obj) \ 315 and not self._script.utilities.isDocument(obj) \ 316 and not self._script.utilities.isDPub(obj) \ 317 and not self._script.utilities.isContentSuggestion(obj): 318 return [] 319 320 priorObj = args.get("priorObj") 321 if obj == priorObj: 322 return [] 323 324 if priorObj and priorObj in self._script.utilities.labelsForObject(obj): 325 return [] 326 327 if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \ 328 or self._script.utilities.isDocument(obj): 329 lastKey, mods = self._script.utilities.lastKeyAndModifiers() 330 if lastKey in ["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]: 331 return [] 332 333 if priorObj and priorObj.getRole() == pyatspi.ROLE_PAGE_TAB and priorObj.name == obj.name: 334 return [] 335 336 if obj.name: 337 name = obj.name 338 if not self._script.utilities.hasExplicitName(obj): 339 name = name.strip() 340 341 if self._script.utilities.shouldVerbalizeAllPunctuation(obj): 342 name = self._script.utilities.verbalizeAllPunctuation(name) 343 344 result = [name] 345 result.extend(self.voice(speech_generator.DEFAULT)) 346 return result 347 348 if obj.getRole() == pyatspi.ROLE_CHECK_BOX: 349 gridCell = pyatspi.findAncestor(obj, self._script.utilities.isGridCell) 350 if gridCell: 351 return super()._generateLabelOrName(gridCell, **args) 352 353 return super()._generateLabelOrName(obj, **args) 354 355 def _generateName(self, obj, **args): 356 if not self._script.utilities.inDocumentContent(obj): 357 return super()._generateName(obj, **args) 358 359 if self._script.utilities.isTextBlockElement(obj) \ 360 and not self._script.utilities.isLandmark(obj) \ 361 and not self._script.utilities.isDPub(obj) \ 362 and not args.get('inFlatReview'): 363 return [] 364 365 if self._script.utilities.hasVisibleCaption(obj): 366 return [] 367 368 if self._script.utilities.isFigure(obj) and args.get('ancestorOf'): 369 caption = args.get('ancestorOf') 370 if caption.getRole() != pyatspi.ROLE_CAPTION: 371 isCaption = lambda x: x and x.getRole() == pyatspi.ROLE_CAPTION 372 caption = pyatspi.findAncestor(caption, isCaption) 373 if caption and hash(obj) in self._script.utilities.labelTargets(caption): 374 return [] 375 376 role = args.get('role', obj.getRole()) 377 378 # TODO - JD: Once the formatting strings are vastly cleaned up 379 # or simply removed, hacks like this won't be needed. 380 if role in [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_SPIN_BUTTON]: 381 return super()._generateName(obj, **args) 382 383 if obj.name: 384 if self._script.utilities.preferDescriptionOverName(obj): 385 result = [obj.description] 386 elif self._script.utilities.isLink(obj) \ 387 and not self._script.utilities.hasExplicitName(obj): 388 return [] 389 else: 390 name = obj.name 391 if not self._script.utilities.hasExplicitName(obj): 392 name = name.strip() 393 result = [name] 394 395 result.extend(self.voice(speech_generator.DEFAULT)) 396 return result 397 398 return super()._generateName(obj, **args) 399 400 def _generateLabel(self, obj, **args): 401 if not self._script.utilities.inDocumentContent(obj): 402 return super()._generateLabel(obj, **args) 403 404 if self._script.utilities.isTextBlockElement(obj): 405 return [] 406 407 label, objects = self._script.utilities.inferLabelFor(obj) 408 if label: 409 result = [label] 410 result.extend(self.voice(speech_generator.DEFAULT)) 411 return result 412 413 return super()._generateLabel(obj, **args) 414 415 def _generateNewNodeLevel(self, obj, **args): 416 if _settingsManager.getSetting('onlySpeakDisplayedText'): 417 return [] 418 419 if self._script.utilities.isTextBlockElement(obj) \ 420 or self._script.utilities.isLink(obj): 421 return [] 422 423 return super()._generateNewNodeLevel(obj, **args) 424 425 def _generateLeaving(self, obj, **args): 426 if _settingsManager.getSetting('onlySpeakDisplayedText'): 427 return [] 428 429 if not args.get('leaving'): 430 return [] 431 432 if self._script.utilities.inDocumentContent(obj) \ 433 and not self._script.utilities.inDocumentContent(orca_state.locusOfFocus): 434 result = [''] 435 result.extend(self.voice(speech_generator.SYSTEM)) 436 return result 437 438 return super()._generateLeaving(obj, **args) 439 440 def _generateNewRadioButtonGroup(self, obj, **args): 441 # TODO - JD: Looking at the default speech generator's method, this 442 # is all kinds of broken. Until that can be sorted out, try to filter 443 # out some of the noise.... 444 return [] 445 446 def _generateNumberOfChildren(self, obj, **args): 447 if _settingsManager.getSetting('onlySpeakDisplayedText') \ 448 or _settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: 449 return [] 450 451 # We handle things even for non-document content due to issues in 452 # other toolkits (e.g. exposing list items to us that are not 453 # exposed to sighted users) 454 role = args.get('role', obj.getRole()) 455 if role not in [pyatspi.ROLE_LIST, pyatspi.ROLE_LIST_BOX]: 456 return super()._generateNumberOfChildren(obj, **args) 457 458 setsize = self._script.utilities.getSetSize(obj[0]) 459 if setsize is None: 460 children = [x for x in obj if x.getRole() == pyatspi.ROLE_LIST_ITEM] 461 setsize = len(children) 462 463 if not setsize: 464 return [] 465 466 result = [messages.listItemCount(setsize)] 467 result.extend(self.voice(speech_generator.SYSTEM)) 468 return result 469 470 # TODO - JD: Yet another dumb generator method we should kill. 471 def _generateTextRole(self, obj, **args): 472 return self._generateRoleName(obj, **args) 473 474 def getLocalizedRoleName(self, obj, **args): 475 if not self._script.utilities.inDocumentContent(obj): 476 return super().getLocalizedRoleName(obj, **args) 477 478 roledescription = self._script.utilities.getRoleDescription(obj) 479 if roledescription: 480 return roledescription 481 482 return super().getLocalizedRoleName(obj, **args) 483 484 def _generateRealActiveDescendantDisplayedText(self, obj, **args): 485 if not self._script.utilities.inDocumentContent(obj): 486 return super()._generateRealActiveDescendantDisplayedText(obj, **args) 487 488 return self._generateDisplayedText(obj, **args) 489 490 def _generateRoleName(self, obj, **args): 491 if _settingsManager.getSetting('onlySpeakDisplayedText'): 492 return [] 493 494 if not self._script.utilities.inDocumentContent(obj): 495 return super()._generateRoleName(obj, **args) 496 497 if obj == args.get('priorObj'): 498 return [] 499 500 result = [] 501 acss = self.voice(speech_generator.SYSTEM) 502 503 roledescription = self._script.utilities.getRoleDescription(obj) 504 if roledescription: 505 result = [roledescription] 506 result.extend(acss) 507 return result 508 509 role = args.get('role', obj.getRole()) 510 enabled, disabled = self._getEnabledAndDisabledContextRoles() 511 if role in disabled: 512 return [] 513 514 force = args.get('force', False) 515 start = args.get('startOffset') 516 end = args.get('endOffset') 517 index = args.get('index', 0) 518 total = args.get('total', 1) 519 520 if not force: 521 doNotSpeak = [pyatspi.ROLE_FOOTER, 522 pyatspi.ROLE_FORM, 523 pyatspi.ROLE_LABEL, 524 pyatspi.ROLE_MENU_ITEM, 525 pyatspi.ROLE_PARAGRAPH, 526 pyatspi.ROLE_SECTION, 527 pyatspi.ROLE_REDUNDANT_OBJECT, 528 pyatspi.ROLE_UNKNOWN] 529 else: 530 doNotSpeak = [pyatspi.ROLE_UNKNOWN] 531 532 if not force: 533 doNotSpeak.append(pyatspi.ROLE_TABLE_CELL) 534 doNotSpeak.append(pyatspi.ROLE_TEXT) 535 doNotSpeak.append(pyatspi.ROLE_STATIC) 536 if args.get('string'): 537 doNotSpeak.append("ROLE_CONTENT_SUGGESTION") 538 if args.get('formatType', 'unfocused') != 'basicWhereAmI': 539 doNotSpeak.append(pyatspi.ROLE_LIST_ITEM) 540 doNotSpeak.append(pyatspi.ROLE_LIST) 541 if (start or end): 542 doNotSpeak.append(pyatspi.ROLE_DOCUMENT_FRAME) 543 doNotSpeak.append(pyatspi.ROLE_DOCUMENT_WEB) 544 doNotSpeak.append(pyatspi.ROLE_ALERT) 545 if self._script.utilities.isAnchor(obj): 546 doNotSpeak.append(obj.getRole()) 547 if total > 1: 548 doNotSpeak.append(pyatspi.ROLE_ROW_HEADER) 549 if self._script.utilities.isMenuInCollapsedSelectElement(obj): 550 doNotSpeak.append(pyatspi.ROLE_MENU) 551 552 lastKey, mods = self._script.utilities.lastKeyAndModifiers() 553 isEditable = obj.getState().contains(pyatspi.STATE_EDITABLE) 554 555 if isEditable and not self._script.utilities.isContentEditableWithEmbeddedObjects(obj): 556 if ((lastKey in ["Down", "Right"] and not mods) or self._script.inSayAll()) and start: 557 return [] 558 if lastKey in ["Up", "Left"] and not mods: 559 text = self._script.utilities.queryNonEmptyText(obj) 560 if text and end not in [None, text.characterCount]: 561 return [] 562 if role not in doNotSpeak: 563 result.append(self.getLocalizedRoleName(obj, **args)) 564 result.extend(acss) 565 566 elif isEditable and self._script.utilities.isDocument(obj): 567 if obj.parent and not obj.parent.getState().contains(pyatspi.STATE_EDITABLE) \ 568 and lastKey not in ["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]: 569 result.append(object_properties.ROLE_EDITABLE_CONTENT) 570 result.extend(acss) 571 572 elif role == pyatspi.ROLE_HEADING: 573 if index == total - 1 or not self._script.utilities.isFocusableWithMathChild(obj): 574 level = self._script.utilities.headingLevel(obj) 575 if level: 576 result.append(object_properties.ROLE_HEADING_LEVEL_SPEECH % { 577 'role': self.getLocalizedRoleName(obj, **args), 578 'level': level}) 579 result.extend(acss) 580 else: 581 result.append(self.getLocalizedRoleName(obj, **args)) 582 result.extend(acss) 583 584 elif self._script.utilities.isLink(obj): 585 if obj.parent.getRole() == pyatspi.ROLE_IMAGE: 586 result.append(messages.IMAGE_MAP_LINK) 587 result.extend(acss) 588 else: 589 if self._script.utilities.hasUselessCanvasDescendant(obj): 590 result.append(self.getLocalizedRoleName(obj, role=pyatspi.ROLE_IMAGE)) 591 result.extend(acss) 592 if index == total - 1 or not self._script.utilities.isFocusableWithMathChild(obj): 593 result.append(self.getLocalizedRoleName(obj, **args)) 594 result.extend(acss) 595 596 elif role not in doNotSpeak and args.get('priorObj') != obj: 597 result.append(self.getLocalizedRoleName(obj, **args)) 598 result.extend(acss) 599 600 if self._script.utilities.isMath(obj) and not self._script.utilities.isMathTopLevel(obj): 601 return result 602 603 ancestorRoles = [pyatspi.ROLE_HEADING, pyatspi.ROLE_LINK] 604 speakRoles = lambda x: x and x.getRole() in ancestorRoles 605 ancestor = pyatspi.findAncestor(obj, speakRoles) 606 if ancestor and ancestor.getRole() != role and (index == total - 1 or obj.name == ancestor.name): 607 result.extend(self._generateRoleName(ancestor)) 608 609 return result 610 611 def _generatePageSummary(self, obj, **args): 612 if not self._script.utilities.inDocumentContent(obj): 613 return [] 614 615 onlyIfFound = args.get('formatType') != 'detailedWhereAmI' 616 617 string = self._script.utilities.getPageSummary(obj, onlyIfFound) 618 if not string: 619 return [] 620 621 result = [string] 622 result.extend(self.voice(speech_generator.SYSTEM)) 623 return result 624 625 def _generateSiteDescription(self, obj, **args): 626 if not self._script.utilities.inDocumentContent(obj): 627 return [] 628 629 link_uri = self._script.utilities.uri(obj) 630 if not link_uri: 631 return [] 632 633 link_uri_info = urllib.parse.urlparse(link_uri) 634 doc_uri = self._script.utilities.documentFrameURI() 635 if not doc_uri: 636 return [] 637 638 result = [] 639 doc_uri_info = urllib.parse.urlparse(doc_uri) 640 if link_uri_info[1] == doc_uri_info[1]: 641 if link_uri_info[2] == doc_uri_info[2]: 642 result.append(messages.LINK_SAME_PAGE) 643 else: 644 result.append(messages.LINK_SAME_SITE) 645 else: 646 linkdomain = link_uri_info[1].split('.') 647 docdomain = doc_uri_info[1].split('.') 648 if len(linkdomain) > 1 and len(docdomain) > 1 \ 649 and linkdomain[-1] == docdomain[-1] \ 650 and linkdomain[-2] == docdomain[-2]: 651 result.append(messages.LINK_SAME_SITE) 652 else: 653 result.append(messages.LINK_DIFFERENT_SITE) 654 655 if result: 656 result.extend(self.voice(speech_generator.HYPERLINK)) 657 658 return result 659 660 def _generateExpandedEOCs(self, obj, **args): 661 if not self._script.utilities.inDocumentContent(obj): 662 return super()._generateExpandedEOCs(obj, **args) 663 664 result = [] 665 startOffset = args.get('startOffset', 0) 666 endOffset = args.get('endOffset', -1) 667 text = self._script.utilities.expandEOCs(obj, startOffset, endOffset) 668 if text: 669 result.append(text) 670 return result 671 672 def _generatePositionInList(self, obj, **args): 673 if _settingsManager.getSetting('onlySpeakDisplayedText'): 674 return [] 675 676 if not args.get('forceList', False) \ 677 and not _settingsManager.getSetting('enablePositionSpeaking'): 678 return [] 679 680 if not self._script.utilities.inDocumentContent(obj): 681 return super()._generatePositionInList(obj, **args) 682 683 menuRoles = [pyatspi.ROLE_MENU_ITEM, 684 pyatspi.ROLE_TEAROFF_MENU_ITEM, 685 pyatspi.ROLE_CHECK_MENU_ITEM, 686 pyatspi.ROLE_RADIO_MENU_ITEM, 687 pyatspi.ROLE_MENU] 688 if obj.getRole() in menuRoles: 689 return super()._generatePositionInList(obj, **args) 690 691 if self._script.utilities.isEditableComboBox(obj): 692 return [] 693 694 if args.get('formatType') not in ['basicWhereAmI', 'detailedWhereAmI']: 695 if args.get('priorObj') == obj: 696 return [] 697 698 position = self._script.utilities.getPositionInSet(obj) 699 total = self._script.utilities.getSetSize(obj) 700 if position is None or total is None: 701 return super()._generatePositionInList(obj, **args) 702 703 position = int(position) 704 total = int(total) 705 if position < 0 or total < 0: 706 return [] 707 708 result = [] 709 result.append(self._script.formatting.getString( 710 mode='speech', 711 stringType='groupindex') \ 712 % {"index" : position, 713 "total" : total}) 714 result.extend(self.voice(speech_generator.SYSTEM)) 715 return result 716 717 def _generateUnselectedCell(self, obj, **args): 718 if not self._script.inFocusMode(): 719 return [] 720 721 return super()._generateUnselectedCell(obj, **args) 722 723 def _generateRealTableCell(self, obj, **args): 724 result = super()._generateRealTableCell(obj, **args) 725 if not self._script.inFocusMode(): 726 return result 727 728 if _settingsManager.getSetting('speakCellCoordinates'): 729 label = self._script.utilities.labelForCellCoordinates(obj) 730 if label: 731 result.append(label) 732 result.extend(self.voice(speech_generator.SYSTEM)) 733 return result 734 735 row, col = self._script.utilities.coordinatesForCell(obj) 736 if self._script.utilities.cellRowChanged(obj): 737 result.append(messages.TABLE_ROW % (row + 1)) 738 result.extend(self.voice(speech_generator.SYSTEM)) 739 if self._script.utilities.cellColumnChanged(obj): 740 result.append(messages.TABLE_COLUMN % (col + 1)) 741 result.extend(self.voice(speech_generator.SYSTEM)) 742 743 return result 744 745 def _generateTableCellRow(self, obj, **args): 746 if not self._script.utilities.inDocumentContent(obj): 747 return super()._generateTableCellRow(obj, **args) 748 749 if not self._script.utilities.shouldReadFullRow(obj): 750 return self._generateRealTableCell(obj, **args) 751 752 isRow = lambda x: x and x.getRole() == pyatspi.ROLE_TABLE_ROW 753 row = pyatspi.findAncestor(obj, isRow) 754 if row and row.name and not self._script.utilities.isLayoutOnly(row): 755 return self.generate(row) 756 757 return super()._generateTableCellRow(obj, **args) 758 759 def _generateRowHeader(self, obj, **args): 760 if self._script.utilities.lastInputEventWasLineNav(): 761 return [] 762 763 return super()._generateRowHeader(obj) 764 765 def generateSpeech(self, obj, **args): 766 if not self._script.utilities.inDocumentContent(obj): 767 msg = "WEB: %s is not in document content. Calling default speech generator." % obj 768 debug.println(debug.LEVEL_INFO, msg, True) 769 return super().generateSpeech(obj, **args) 770 771 msg = "WEB: Generating speech for document object %s" % obj 772 debug.println(debug.LEVEL_INFO, msg, True) 773 774 result = [] 775 if args.get('formatType') == 'detailedWhereAmI': 776 oldRole = self._overrideRole('default', args) 777 elif self._script.utilities.isLink(obj): 778 oldRole = self._overrideRole(pyatspi.ROLE_LINK, args) 779 elif self._script.utilities.isCustomImage(obj): 780 oldRole = self._overrideRole(pyatspi.ROLE_IMAGE, args) 781 elif self._script.utilities.treatAsDiv(obj, offset=args.get('startOffset')): 782 oldRole = self._overrideRole(pyatspi.ROLE_SECTION, args) 783 else: 784 oldRole = self._overrideRole(self._getAlternativeRole(obj, **args), args) 785 786 if not 'priorObj' in args: 787 document = self._script.utilities.getTopLevelDocumentForObject(obj) 788 args['priorObj'] = self._script.utilities.getPriorContext(document)[0] 789 790 if not result: 791 result = list(filter(lambda x: x, super().generateSpeech(obj, **args))) 792 793 self._restoreRole(oldRole, args) 794 msg = "WEB: Speech generation for document object %s complete." % obj 795 debug.println(debug.LEVEL_INFO, msg, True) 796 return result 797 798 def generateContents(self, contents, **args): 799 if not len(contents): 800 return [] 801 802 result = [] 803 contents = self._script.utilities.filterContentsForPresentation(contents, True) 804 msg = "WEB: Generating speech contents (length: %i)" % len(contents) 805 debug.println(debug.LEVEL_INFO, msg, True) 806 for i, content in enumerate(contents): 807 obj, start, end, string = content 808 msg = "ITEM %i: %s, start: %i, end: %i, string: '%s'" \ 809 % (i, obj, start, end, string) 810 debug.println(debug.LEVEL_INFO, msg, True) 811 utterance = self.generateSpeech( 812 obj, startOffset=start, endOffset=end, string=string, 813 index=i, total=len(contents), **args) 814 if isinstance(utterance, list): 815 isNotEmptyList = lambda x: not (isinstance(x, list) and not x) 816 utterance = list(filter(isNotEmptyList, utterance)) 817 if utterance and utterance[0]: 818 result.append(utterance) 819 args['priorObj'] = obj 820 821 if not result: 822 if self._script.inSayAll(treatInterruptedAsIn=False) \ 823 or not _settingsManager.getSetting('speakBlankLines'): 824 string = "" 825 else: 826 string = messages.BLANK 827 result = [string, self.voice(speech_generator.DEFAULT)] 828 829 return result 830