1# Orca 2# 3# Copyright 2005-2009 Sun Microsystems Inc. 4# 5# This library is free software; you can redistribute it and/or 6# modify it under the terms of the GNU Lesser General Public 7# License as published by the Free Software Foundation; either 8# version 2.1 of the License, or (at your option) any later version. 9# 10# This library is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# Lesser General Public License for more details. 14# 15# You should have received a copy of the GNU Lesser General Public 16# License along with this library; if not, write to the 17# Free Software Foundation, Inc., Franklin Street, Fifth Floor, 18# Boston MA 02110-1301 USA. 19 20"""Utilities for obtaining speech utterances for objects.""" 21 22__id__ = "$Id:$" 23__version__ = "$Revision:$" 24__date__ = "$Date:$" 25__copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." 26__license__ = "LGPL" 27 28import pyatspi 29import urllib.parse, urllib.request, urllib.error, urllib.parse 30 31from . import chnames 32from . import debug 33from . import generator 34from . import messages 35from . import object_properties 36from . import settings 37from . import settings_manager 38from . import text_attribute_names 39from . import acss 40 41class Pause: 42 """A dummy class to indicate we want to insert a pause into an 43 utterance.""" 44 def __init__(self): 45 pass 46 47 def __str__(self): 48 return "PAUSE" 49 50PAUSE = [Pause()] 51 52class LineBreak: 53 """A dummy class to indicate we want to break an utterance into 54 separate calls to speak.""" 55 def __init__(self): 56 pass 57 58LINE_BREAK = [LineBreak()] 59 60# [[[WDW - general note -- for all the _generate* methods, it would be great if 61# we could return an empty array if we can determine the method does not 62# apply to the object. This would allow us to reduce the number of strings 63# needed in formatting.py.]]] 64 65# The prefix to use for the individual generator methods 66# 67METHOD_PREFIX = "_generate" 68 69DEFAULT = "default" 70UPPERCASE = "uppercase" 71HYPERLINK = "hyperlink" 72SYSTEM = "system" 73STATE = "state" # Candidate for sound 74VALUE = "value" # Candidate for sound 75 76voiceType = { 77 DEFAULT: settings.DEFAULT_VOICE, 78 UPPERCASE: settings.UPPERCASE_VOICE, 79 HYPERLINK: settings.HYPERLINK_VOICE, 80 SYSTEM: settings.SYSTEM_VOICE, 81 STATE: settings.SYSTEM_VOICE, # Users may prefer DEFAULT_VOICE here 82 VALUE: settings.SYSTEM_VOICE, # Users may prefer DEFAULT_VOICE here 83} 84 85_settingsManager = settings_manager.getManager() 86 87class SpeechGenerator(generator.Generator): 88 """Takes accessible objects and produces a string to speak for 89 those objects. See the generateSpeech method, which is the primary 90 entry point. Subclasses can feel free to override/extend the 91 speechGenerators instance field as they see fit.""" 92 93 # pylint: disable-msg=W0142 94 95 def __init__(self, script): 96 generator.Generator.__init__(self, script, "speech") 97 98 def _getACSS(self, obj, string): 99 if obj.getRole() == pyatspi.ROLE_LINK: 100 acss = self.voice(HYPERLINK) 101 elif isinstance(string, str) \ 102 and string.isupper() \ 103 and string.strip().isalpha(): 104 acss = self.voice(UPPERCASE) 105 else: 106 acss = self.voice(DEFAULT) 107 108 return acss 109 110 def _addGlobals(self, globalsDict): 111 """Other things to make available from the formatting string. 112 """ 113 generator.Generator._addGlobals(self, globalsDict) 114 globalsDict['voice'] = self.voice 115 116 def generateSpeech(self, obj, **args): 117 rv = self.generate(obj, **args) 118 if rv and not list(filter(lambda x: not isinstance(x, Pause), rv)): 119 msg = 'SPEECH GENERATOR: Results for %s are pauses only' % obj 120 debug.println(debug.LEVEL_INFO, msg, True) 121 rv = [] 122 123 return rv 124 125 def _resultElementToString(self, element, includeAll=True): 126 if debug.LEVEL_ALL < debug.debugLevel: 127 return str(element) 128 129 if isinstance(element, str): 130 return super()._resultElementToString(element, includeAll) 131 132 if not isinstance(element, acss.ACSS): 133 return str(element) 134 135 if not includeAll: 136 return "" 137 138 voices = {"default": self.voice(DEFAULT)[0], 139 "system": self.voice(SYSTEM)[0], 140 "hyperlink": self.voice(HYPERLINK)[0], 141 "uppercase": self.voice(UPPERCASE)[0]} 142 143 voicetypes = [k for k in voices if voices.get(k) == element] 144 return "Voice(s): (%s)" % ", ".join(voicetypes) 145 146 ##################################################################### 147 # # 148 # Name, role, and label information # 149 # # 150 ##################################################################### 151 152 def _generateName(self, obj, **args): 153 """Returns an array of strings for use by speech and braille that 154 represent the name of the object. If the object is directly 155 displaying any text, that text will be treated as the name. 156 Otherwise, the accessible name of the object will be used. If 157 there is no accessible name, then the description of the 158 object will be used. This method will return an empty array 159 if nothing can be found. [[[WDW - I wonder if we should just 160 have _generateName, _generateDescription, 161 _generateDisplayedText, etc., that don't do any fallback. 162 Then, we can allow the formatting to do the fallback (e.g., 163 'displayedText or name or description'). [[[JD to WDW - I 164 needed a _generateDescription for whereAmI. :-) See below. 165 """ 166 167 try: 168 role = args.get('role', obj.getRole()) 169 except (LookupError, RuntimeError): 170 debug.println(debug.LEVEL_FINE, "Error getting role for: %s" % obj) 171 role = None 172 173 if role == pyatspi.ROLE_LAYERED_PANE: 174 if _settingsManager.getSetting('onlySpeakDisplayedText'): 175 return [] 176 else: 177 acss = self.voice(SYSTEM) 178 else: 179 acss = self.voice(DEFAULT) 180 result = generator.Generator._generateName(self, obj, **args) 181 if result: 182 result.extend(acss) 183 return result 184 185 def _generateLabel(self, obj, **args): 186 """Returns the label for an object as an array of strings for use by 187 speech and braille. The label is determined by the displayedLabel 188 method of the script utility, and an empty array will be returned if 189 no label can be found. 190 """ 191 acss = self.voice(DEFAULT) 192 result = generator.Generator._generateLabel(self, obj, **args) 193 if result: 194 result.extend(acss) 195 return result 196 197 def _generateLabelOrName(self, obj, **args): 198 """Returns the label as an array of strings for speech and braille. 199 If the label cannot be found, the name will be used instead. 200 If the name cannot be found, an empty array will be returned. 201 """ 202 result = [] 203 acss = self.voice(DEFAULT) 204 result.extend(self._generateLabel(obj, **args)) 205 if not result: 206 try: 207 name = obj.name 208 except: 209 msg = 'ERROR: Could not get name for %s' % obj 210 debug.println(debug.LEVEL_INFO, msg) 211 return result 212 if name: 213 result.append(name) 214 result.extend(acss) 215 if not result and obj.parent and obj.parent.getRole() == pyatspi.ROLE_AUTOCOMPLETE: 216 result = self._generateLabelOrName(obj.parent, **args) 217 218 return result 219 220 def _generatePlaceholderText(self, obj, **args): 221 """Returns an array of strings for use by speech and braille that 222 represent the 'placeholder' text. This is typically text that 223 serves as a functional label and is found in a text widget until 224 that widget is given focus at which point the text is removed, 225 the assumption being that the user was able to see the text prior 226 to giving the widget focus. 227 """ 228 acss = self.voice(DEFAULT) 229 result = generator.Generator._generatePlaceholderText(self, obj, **args) 230 if result: 231 result.extend(acss) 232 return result 233 234 def _generateAlertText(self, obj, **args): 235 result = self._generateExpandedEOCs(obj, **args) or self._generateUnrelatedLabels(obj, **args) 236 if result: 237 self._script.pointOfReference['usedDescriptionForAlert'] = False 238 return result 239 240 args['alerttext'] = True 241 result = self._generateDescription(obj, **args) 242 if result: 243 self._script.pointOfReference['usedDescriptionForAlert'] = True 244 245 return result 246 247 def _generateDescription(self, obj, **args): 248 """Returns an array of strings fo use by speech and braille that 249 represent the description of the object, if that description 250 is different from that of the name and label. 251 """ 252 253 alreadyUsed = False 254 role = args.get('role', obj.getRole()) 255 if role == pyatspi.ROLE_ALERT: 256 try: 257 alreadyUsed = self._script.pointOfReference.pop('usedDescriptionForAlert') 258 except: 259 pass 260 261 if alreadyUsed: 262 return [] 263 264 if _settingsManager.getSetting('onlySpeakDisplayedText'): 265 return [] 266 267 if not _settingsManager.getSetting('speakDescription') and not args.get('alerttext'): 268 return [] 269 270 if args.get('inMouseReview') and not _settingsManager.getSetting('presentToolTips'): 271 return [] 272 273 priorObj = args.get('priorObj') 274 if priorObj and priorObj.getRole() == pyatspi.ROLE_TOOL_TIP: 275 return [] 276 277 if priorObj == obj: 278 return [] 279 280 acss = self.voice(SYSTEM) 281 result = generator.Generator._generateDescription(self, obj, **args) 282 if result: 283 result.extend(acss) 284 return result 285 286 def _generateImageDescription(self, obj, **args ): 287 """Returns an array of strings for use by speech and braille that 288 represent the description of the image on the object.""" 289 290 if _settingsManager.getSetting('onlySpeakDisplayedText'): 291 return [] 292 293 if not _settingsManager.getSetting('speakDescription'): 294 return [] 295 296 acss = self.voice(SYSTEM) 297 result = generator.Generator._generateImageDescription(self, obj, **args) 298 if result: 299 result.extend(acss) 300 return result 301 302 def _generateReadOnly(self, obj, **args): 303 """Returns an array of strings for use by speech and braille that 304 represent the read only state of this object, but only if it 305 is read only (i.e., it is a text area that cannot be edited). 306 """ 307 acss = self.voice(SYSTEM) 308 result = generator.Generator._generateReadOnly(self, obj, **args) 309 if result: 310 result.extend(acss) 311 return result 312 313 def _generateHasPopup(self, obj, **args): 314 if _settingsManager.getSetting('onlySpeakDisplayedText') \ 315 or _settingsManager.getSetting('speechVerbosityLevel') \ 316 == settings.VERBOSITY_LEVEL_BRIEF: 317 return [] 318 319 acss = self.voice(SYSTEM) 320 result = generator.Generator._generateHasPopup(self, obj, **args) 321 if result: 322 result.extend(acss) 323 return result 324 325 def _generateClickable(self, obj, **args): 326 if _settingsManager.getSetting('onlySpeakDisplayedText') \ 327 or _settingsManager.getSetting('speechVerbosityLevel') \ 328 == settings.VERBOSITY_LEVEL_BRIEF: 329 return [] 330 331 acss = self.voice(SYSTEM) 332 result = generator.Generator._generateClickable(self, obj, **args) 333 if result: 334 result.extend(acss) 335 return result 336 337 def _generateHasLongDesc(self, obj, **args): 338 if _settingsManager.getSetting('onlySpeakDisplayedText'): 339 return [] 340 341 acss = self.voice(SYSTEM) 342 result = generator.Generator._generateHasLongDesc(self, obj, **args) 343 if result: 344 result.extend(acss) 345 return result 346 347 def _generateHasDetails(self, obj, **args): 348 if _settingsManager.getSetting('onlySpeakDisplayedText'): 349 return [] 350 351 acss = self.voice(SYSTEM) 352 result = generator.Generator._generateHasDetails(self, obj, **args) 353 if result: 354 result.extend(acss) 355 return result 356 357 def _generateDetailsFor(self, obj, **args): 358 if _settingsManager.getSetting('onlySpeakDisplayedText'): 359 return [] 360 361 acss = self.voice(SYSTEM) 362 result = generator.Generator._generateDetailsFor(self, obj, **args) 363 if result: 364 result.extend(acss) 365 return result 366 367 def _generateAllDetails(self, obj, **args): 368 if _settingsManager.getSetting('onlySpeakDisplayedText'): 369 return [] 370 371 acss = self.voice(SYSTEM) 372 result = generator.Generator._generateAllDetails(self, obj, **args) 373 if result: 374 result.extend(acss) 375 return result 376 377 def _generateDeletionStart(self, obj, **args): 378 if _settingsManager.getSetting('onlySpeakDisplayedText'): 379 return [] 380 381 startOffset = args.get('startOffset', 0) 382 if startOffset != 0: 383 return [] 384 385 result = [] 386 if self._script.utilities.isFirstItemInInlineContentSuggestion(obj): 387 result.extend([object_properties.ROLE_CONTENT_SUGGESTION]) 388 result.extend(self.voice(SYSTEM)) 389 result.extend(self._generatePause(obj, **args)) 390 391 result.extend([messages.CONTENT_DELETION_START]) 392 result.extend(self.voice(SYSTEM)) 393 return result 394 395 def _generateDeletionEnd(self, obj, **args): 396 if _settingsManager.getSetting('onlySpeakDisplayedText'): 397 return [] 398 399 endOffset = args.get('endOffset') 400 if endOffset is not None: 401 text = self._script.utilities.queryNonEmptyText(obj) 402 if text and text.characterCount != endOffset: 403 return [] 404 405 result = [messages.CONTENT_DELETION_END] 406 result.extend(self.voice(SYSTEM)) 407 408 if self._script.utilities.isLastItemInInlineContentSuggestion(obj): 409 result.extend(self._generatePause(obj, **args)) 410 result.extend([messages.CONTENT_SUGGESTION_END]) 411 result.extend(self.voice(SYSTEM)) 412 413 container = pyatspi.findAncestor(obj, self._script.utilities.hasDetails) 414 if self._script.utilities.isContentSuggestion(container): 415 result.extend(self._generatePause(obj, **args)) 416 result.extend(self._generateHasDetails(container, mode=args.get('mode'))) 417 418 return result 419 420 def _generateInsertionStart(self, obj, **args): 421 if _settingsManager.getSetting('onlySpeakDisplayedText'): 422 return [] 423 424 startOffset = args.get('startOffset', 0) 425 if startOffset != 0: 426 return [] 427 428 result = [] 429 if self._script.utilities.isFirstItemInInlineContentSuggestion(obj): 430 result.extend([object_properties.ROLE_CONTENT_SUGGESTION]) 431 result.extend(self.voice(SYSTEM)) 432 result.extend(self._generatePause(obj, **args)) 433 434 result.extend([messages.CONTENT_INSERTION_START]) 435 result.extend(self.voice(SYSTEM)) 436 return result 437 438 def _generateInsertionEnd(self, obj, **args): 439 if _settingsManager.getSetting('onlySpeakDisplayedText'): 440 return [] 441 442 endOffset = args.get('endOffset') 443 if endOffset is not None: 444 text = self._script.utilities.queryNonEmptyText(obj) 445 if text and text.characterCount != endOffset: 446 return [] 447 448 result = [messages.CONTENT_INSERTION_END] 449 result.extend(self.voice(SYSTEM)) 450 451 if self._script.utilities.isLastItemInInlineContentSuggestion(obj): 452 result.extend(self._generatePause(obj, **args)) 453 result.extend([messages.CONTENT_SUGGESTION_END]) 454 result.extend(self.voice(SYSTEM)) 455 456 container = pyatspi.findAncestor(obj, self._script.utilities.hasDetails) 457 if self._script.utilities.isContentSuggestion(container): 458 result.extend(self._generatePause(obj, **args)) 459 result.extend(self._generateHasDetails(container, mode=args.get('mode'))) 460 461 return result 462 463 def _generateMarkStart(self, obj, **args): 464 if _settingsManager.getSetting('onlySpeakDisplayedText'): 465 return [] 466 467 startOffset = args.get('startOffset', 0) 468 if startOffset != 0: 469 return [] 470 471 result = [] 472 roledescription = self._script.utilities.getRoleDescription(obj) 473 if roledescription: 474 result.append(roledescription) 475 result.extend(self.voice(SYSTEM)) 476 result.extend(self._generatePause(obj, **args)) 477 478 result.append(messages.CONTENT_MARK_START) 479 result.extend(self.voice(SYSTEM)) 480 return result 481 482 def _generateMarkEnd(self, obj, **args): 483 if _settingsManager.getSetting('onlySpeakDisplayedText'): 484 return [] 485 486 endOffset = args.get('endOffset') 487 if endOffset is not None: 488 text = self._script.utilities.queryNonEmptyText(obj) 489 if text and text.characterCount != endOffset: 490 return [] 491 492 result = [messages.CONTENT_MARK_END] 493 result.extend(self.voice(SYSTEM)) 494 return result 495 496 def _generateSuggestionStart(self, obj, **args): 497 if _settingsManager.getSetting('onlySpeakDisplayedText'): 498 return [] 499 500 result = [messages.CONTENT_SUGGESTION_START] 501 result.extend(self.voice(SYSTEM)) 502 return result 503 504 def _generateAvailability(self, obj, **args): 505 if _settingsManager.getSetting('onlySpeakDisplayedText'): 506 return [] 507 508 acss = self.voice(SYSTEM) 509 result = generator.Generator._generateAvailability(self, obj, **args) 510 if result: 511 result.extend(acss) 512 return result 513 514 def _generateInvalid(self, obj, **args): 515 if _settingsManager.getSetting('onlySpeakDisplayedText'): 516 return [] 517 518 acss = self.voice(SYSTEM) 519 result = generator.Generator._generateInvalid(self, obj, **args) 520 if result: 521 result.extend(acss) 522 return result 523 524 def _generateRequired(self, obj, **args): 525 if _settingsManager.getSetting('onlySpeakDisplayedText'): 526 return [] 527 528 acss = self.voice(SYSTEM) 529 result = generator.Generator._generateRequired(self, obj, **args) 530 if result: 531 result.extend(acss) 532 return result 533 534 def _generateTable(self, obj, **args): 535 if _settingsManager.getSetting('onlySpeakDisplayedText'): 536 return [] 537 538 if args.get("leaving"): 539 return[] 540 541 if self._script.utilities.isTextDocumentTable(obj): 542 role = args.get('role', obj.getRole()) 543 enabled, disabled = self._getEnabledAndDisabledContextRoles() 544 if role in disabled: 545 return [] 546 elif _settingsManager.getSetting('speechVerbosityLevel') == \ 547 settings.VERBOSITY_LEVEL_BRIEF: 548 return [] 549 550 acss = self.voice(SYSTEM) 551 result = generator.Generator._generateTable(self, obj, **args) 552 if result: 553 result.extend(acss) 554 return result 555 556 def _generateTextRole(self, obj, **args): 557 """A convenience method to prevent the pyatspi.ROLE_PARAGRAPH role 558 from being spoken. In the case of a pyatspi.ROLE_PARAGRAPH 559 role, an empty array will be returned. In all other cases, the 560 role name will be returned as an array of strings (and 561 possibly voice and audio specifications). Note that a 'role' 562 attribute in args will override the accessible role of the 563 obj. [[[WDW - I wonder if this should be moved to 564 _generateRoleName. Or, maybe make a 'do not speak roles' attribute 565 of a speech generator that we can update and the user can 566 override.]]] 567 """ 568 if _settingsManager.getSetting('onlySpeakDisplayedText'): 569 return [] 570 571 result = [] 572 role = args.get('role', obj.getRole()) 573 if role != pyatspi.ROLE_PARAGRAPH: 574 result.extend(self._generateRoleName(obj, **args)) 575 return result 576 577 def _generateRoleName(self, obj, **args): 578 """Returns the role name for the object in an array of strings (and 579 possibly voice and audio specifications), with the exception 580 that the pyatspi.ROLE_UNKNOWN role will yield an empty array. 581 Note that a 'role' attribute in args will override the 582 accessible role of the obj. 583 """ 584 if _settingsManager.getSetting('onlySpeakDisplayedText'): 585 return [] 586 587 if self._script.utilities.isStatusBarNotification(obj): 588 return [] 589 590 if self._script.utilities.isDesktop(obj): 591 return [] 592 593 result = [] 594 acss = self.voice(SYSTEM) 595 role = args.get('role', obj.getRole()) 596 597 doNotPresent = [pyatspi.ROLE_UNKNOWN, 598 pyatspi.ROLE_REDUNDANT_OBJECT, 599 pyatspi.ROLE_FILLER, 600 pyatspi.ROLE_EXTENDED] 601 602 try: 603 parentRole = obj.parent.getRole() 604 except: 605 parentRole = None 606 607 if role == pyatspi.ROLE_MENU and parentRole == pyatspi.ROLE_COMBO_BOX: 608 return self._generateRoleName(obj.parent) 609 610 if self._script.utilities.isSingleLineAutocompleteEntry(obj): 611 result.append(self.getLocalizedRoleName(obj, role=pyatspi.ROLE_AUTOCOMPLETE)) 612 result.extend(acss) 613 return result 614 615 if role == pyatspi.ROLE_PANEL and obj.getState().contains(pyatspi.STATE_SELECTED): 616 return [] 617 618 # egg-list-box, e.g. privacy panel in gnome-control-center 619 if parentRole == pyatspi.ROLE_LIST_BOX: 620 doNotPresent.append(obj.getRole()) 621 622 if self._script.utilities.isStatusBarDescendant(obj): 623 doNotPresent.append(pyatspi.ROLE_LABEL) 624 625 if _settingsManager.getSetting('speechVerbosityLevel') \ 626 == settings.VERBOSITY_LEVEL_BRIEF: 627 doNotPresent.extend([pyatspi.ROLE_ICON, pyatspi.ROLE_CANVAS]) 628 629 if role == pyatspi.ROLE_HEADING: 630 level = self._script.utilities.headingLevel(obj) 631 if level: 632 result.append(object_properties.ROLE_HEADING_LEVEL_SPEECH % { 633 'role': self.getLocalizedRoleName(obj, **args), 634 'level': level}) 635 result.extend(acss) 636 637 if role not in doNotPresent and not result: 638 result.append(self.getLocalizedRoleName(obj, **args)) 639 result.extend(acss) 640 return result 641 642 def getRoleName(self, obj, **args): 643 """Returns the role name for the object in an array of strings (and 644 possibly voice and audio specifications), with the exception 645 that the pyatspi.ROLE_UNKNOWN role will yield an empty array. 646 Note that a 'role' attribute in args will override the 647 accessible role of the obj. This is provided mostly as a 648 method for scripts to call. 649 """ 650 generated = self._generateRoleName(obj, **args) 651 if generated: 652 return generated[0] 653 654 return "" 655 656 def getName(self, obj, **args): 657 generated = self._generateName(obj, **args) 658 if generated: 659 return generated[0] 660 661 return "" 662 663 def getLocalizedRoleName(self, obj, **args): 664 """Returns the localized name of the given Accessible object; the name 665 is suitable to be spoken. 666 667 Arguments: 668 - obj: an Accessible object 669 """ 670 671 if self._script.utilities.isEditableComboBox(obj) \ 672 or self._script.utilities.isEditableDescendantOfComboBox(obj): 673 return object_properties.ROLE_EDITABLE_COMBO_BOX 674 675 role = args.get('role', obj.getRole()) 676 state = obj.getState() 677 if role == pyatspi.ROLE_LINK and state.contains(pyatspi.STATE_VISITED): 678 return object_properties.ROLE_VISITED_LINK 679 680 return super().getLocalizedRoleName(obj, **args) 681 682 def _generateUnrelatedLabels(self, obj, **args): 683 """Returns, as an array of strings (and possibly voice 684 specifications), all the labels which are underneath the obj's 685 hierarchy and which are not in a label for or labelled by 686 relation. 687 """ 688 result = [] 689 acss = self.voice(DEFAULT) 690 visibleOnly = not self._script.utilities.isStatusBarNotification(obj) 691 692 minimumWords = 1 693 role = args.get('role', obj.getRole()) 694 if role in [pyatspi.ROLE_DIALOG, pyatspi.ROLE_PANEL]: 695 minimumWords = 3 696 697 labels = self._script.utilities.unrelatedLabels(obj, visibleOnly, minimumWords) 698 for label in labels: 699 name = self._generateName(label, **args) 700 if name and len(name[0]) == 1: 701 charname = chnames.getCharacterName(name[0]) 702 if charname: 703 name[0] = charname 704 result.extend(name) 705 if result: 706 result.extend(acss) 707 return result 708 709 ##################################################################### 710 # # 711 # State information # 712 # # 713 ##################################################################### 714 715 def _generateCheckedState(self, obj, **args): 716 """Returns an array of strings for use by speech and braille that 717 represent the checked state of the object. This is typically 718 for check boxes. [[[WDW - should we return an empty array if 719 we can guarantee we know this thing is not checkable?]]] 720 """ 721 if _settingsManager.getSetting('onlySpeakDisplayedText'): 722 return [] 723 724 acss = self.voice(STATE) 725 result = generator.Generator._generateCheckedState(self, obj, **args) 726 if result: 727 result.extend(acss) 728 return result 729 730 def _generateExpandableState(self, obj, **args): 731 """Returns an array of strings for use by speech and braille that 732 represent the expanded/collapsed state of an object, such as a 733 tree node. If the object is not expandable, an empty array 734 will be returned. 735 """ 736 if _settingsManager.getSetting('onlySpeakDisplayedText'): 737 return [] 738 739 acss = self.voice(STATE) 740 result = generator.Generator._generateExpandableState(self, obj, **args) 741 if result: 742 result.extend(acss) 743 return result 744 745 def _generateCheckedStateIfCheckable(self, obj, **args): 746 if _settingsManager.getSetting('onlySpeakDisplayedText'): 747 return [] 748 749 acss = self.voice(STATE) 750 result = super()._generateCheckedStateIfCheckable(obj, **args) 751 if result: 752 result.extend(acss) 753 return result 754 755 def _generateMenuItemCheckedState(self, obj, **args): 756 """Returns an array of strings for use by speech and braille that 757 represent the checked state of the menu item, only if it is 758 checked. Otherwise, and empty array will be returned. 759 """ 760 if _settingsManager.getSetting('onlySpeakDisplayedText'): 761 return [] 762 763 acss = self.voice(STATE) 764 result = generator.Generator.\ 765 _generateMenuItemCheckedState(self, obj, **args) 766 if result: 767 result.extend(acss) 768 return result 769 770 def _generateMultiselectableState(self, obj, **args): 771 """Returns an array of strings (and possibly voice and audio 772 specifications) that represent the multiselectable state of 773 the object. This is typically for list boxes. If the object 774 is not multiselectable, an empty array will be returned. 775 """ 776 if _settingsManager.getSetting('onlySpeakDisplayedText'): 777 return [] 778 779 acss = self.voice(STATE) 780 result = super()._generateMultiselectableState(obj, **args) 781 if result: 782 result.extend(acss) 783 return result 784 785 def _generateRadioState(self, obj, **args): 786 """Returns an array of strings for use by speech and braille that 787 represent the checked state of the object. This is typically 788 for check boxes. [[[WDW - should we return an empty array if 789 we can guarantee we know this thing is not checkable?]]] 790 """ 791 if _settingsManager.getSetting('onlySpeakDisplayedText'): 792 return [] 793 794 acss = self.voice(STATE) 795 result = generator.Generator._generateRadioState(self, obj, **args) 796 if result: 797 result.extend(acss) 798 return result 799 800 def _generateSwitchState(self, obj, **args): 801 """Returns an array of strings indicating the on/off state of obj.""" 802 if _settingsManager.getSetting('onlySpeakDisplayedText'): 803 return [] 804 805 acss = self.voice(STATE) 806 result = generator.Generator._generateSwitchState(self, obj, **args) 807 if result: 808 result.extend(acss) 809 return result 810 811 def _generateToggleState(self, obj, **args): 812 """Returns an array of strings for use by speech and braille that 813 represent the checked state of the object. This is typically 814 for check boxes. [[[WDW - should we return an empty array if 815 we can guarantee we know this thing is not checkable?]]] 816 """ 817 if _settingsManager.getSetting('onlySpeakDisplayedText'): 818 return [] 819 820 acss = self.voice(STATE) 821 result = generator.Generator._generateToggleState(self, obj, **args) 822 if result: 823 result.extend(acss) 824 return result 825 826 ##################################################################### 827 # # 828 # Link information # 829 # # 830 ##################################################################### 831 832 def generateLinkInfo(self, obj, **args): 833 result = self._generateLinkInfo(obj, **args) 834 result.extend(self._generatePause(obj, **args)) 835 result.append(self._generateSiteDescription(obj, **args)) 836 result.extend(self._generatePause(obj, **args)) 837 result.append(self._generateFileSize(obj, **args)) 838 return result 839 840 def _generateLinkInfo(self, obj, **args): 841 """Returns an array of strings (and possibly voice and audio 842 specifications) that represent the protocol of the URI of 843 the link associated with obj. 844 """ 845 result = [] 846 acss = self.voice(HYPERLINK) 847 # Get the URI for the link of interest and parse it. The parsed 848 # URI is returned as a tuple containing six components: 849 # scheme://netloc/path;parameters?query#fragment. 850 # 851 link_uri = self._script.utilities.uri(obj) 852 if not link_uri: 853 # [[[TODO - JD: For some reason, this is failing for certain 854 # links. The current whereAmI code says, "It might be an anchor. 855 # Try to speak the text." and passes things off to whereAmI's 856 # _speakText method. That won't work in the new world order. 857 # Therefore, for now, I will hack in some code to do that 858 # work here so that the before and after end results match.]]] 859 # 860 result.extend(self._generateLabel(obj)) 861 result.extend(self._generateRoleName(obj)) 862 result.append(self._script.utilities.displayedText(obj)) 863 else: 864 link_uri_info = urllib.parse.urlparse(link_uri) 865 if link_uri_info[0] in ["ftp", "ftps", "file"]: 866 fileName = link_uri_info[2].split('/') 867 result.append(messages.LINK_TO_FILE \ 868 % {"uri" : link_uri_info[0], 869 "file" : fileName[-1]}) 870 else: 871 linkOutput = messages.LINK_WITH_PROTOCOL % link_uri_info[0] 872 text = self._script.utilities.displayedText(obj) 873 try: 874 isVisited = obj.getState().contains(pyatspi.STATE_VISITED) 875 except: 876 isVisited = False 877 if not isVisited: 878 linkOutput = messages.LINK_WITH_PROTOCOL % link_uri_info[0] 879 else: 880 linkOutput = messages.LINK_WITH_PROTOCOL_VISITED % link_uri_info[0] 881 if not text: 882 # If there's no text for the link, expose part of the 883 # URI to the user. 884 # 885 text = self._script.utilities.linkBasename(obj) 886 if text: 887 linkOutput += " " + text 888 result.append(linkOutput) 889 if obj.childCount and obj[0].getRole() == pyatspi.ROLE_IMAGE: 890 result.extend(self._generateRoleName(obj[0])) 891 if result: 892 result.extend(acss) 893 return result 894 895 def _generateSiteDescription(self, obj, **args): 896 """Returns an array of strings (and possibly voice and audio 897 specifications) that describe the site (same or different) 898 pointed to by the URI of the link associated with obj. 899 """ 900 result = [] 901 acss = self.voice(HYPERLINK) 902 link_uri = self._script.utilities.uri(obj) 903 if link_uri: 904 link_uri_info = urllib.parse.urlparse(link_uri) 905 else: 906 return result 907 doc_uri = self._script.utilities.documentFrameURI() 908 if doc_uri: 909 doc_uri_info = urllib.parse.urlparse(doc_uri) 910 if link_uri_info[1] == doc_uri_info[1]: 911 if link_uri_info[2] == doc_uri_info[2]: 912 result.append(messages.LINK_SAME_PAGE) 913 else: 914 result.append(messages.LINK_SAME_SITE) 915 else: 916 # check for different machine name on same site 917 # 918 linkdomain = link_uri_info[1].split('.') 919 docdomain = doc_uri_info[1].split('.') 920 if len(linkdomain) > 1 and len(docdomain) > 1 \ 921 and linkdomain[-1] == docdomain[-1] \ 922 and linkdomain[-2] == docdomain[-2]: 923 result.append(messages.LINK_SAME_SITE) 924 else: 925 result.append(messages.LINK_DIFFERENT_SITE) 926 927 if result: 928 result.extend(acss) 929 return result 930 931 def _generateFileSize(self, obj, **args): 932 """Returns an array of strings (and possibly voice and audio 933 specifications) that represent the size (Content-length) of 934 the file pointed to by the URI of the link associated with 935 obj. 936 """ 937 result = [] 938 acss = self.voice(HYPERLINK) 939 sizeString = "" 940 uri = self._script.utilities.uri(obj) 941 if not uri: 942 return result 943 try: 944 x = urllib.request.urlopen(uri) 945 try: 946 sizeString = x.info()['Content-length'] 947 except KeyError: 948 pass 949 except (ValueError, urllib.error.URLError, OSError): 950 pass 951 if sizeString: 952 size = int(sizeString) 953 if size < 10000: 954 result.append(messages.fileSizeBytes(size)) 955 elif size < 1000000: 956 result.append(messages.FILE_SIZE_KB % (float(size) * .001)) 957 elif size >= 1000000: 958 result.append(messages.FILE_SIZE_MB % (float(size) * .000001)) 959 if result: 960 result.extend(acss) 961 return result 962 963 ##################################################################### 964 # # 965 # Image information # 966 # # 967 ##################################################################### 968 969 def _generateImage(self, obj, **args): 970 """Returns an array of strings (and possibly voice and audio 971 specifications) that represent the image on the object, if 972 it exists. Otherwise, an empty array is returned. 973 """ 974 result = [] 975 acss = self.voice(DEFAULT) 976 try: 977 image = obj.queryImage() 978 except: 979 pass 980 else: 981 args['role'] = pyatspi.ROLE_IMAGE 982 result.extend(self.generate(obj, **args)) 983 result.extend(acss) 984 return result 985 986 ##################################################################### 987 # # 988 # Table interface information # 989 # # 990 ##################################################################### 991 992 def _generateColumnHeader(self, obj, **args): 993 if self._script.inSayAll(): 994 return [] 995 996 result = super()._generateColumnHeader(obj, **args) 997 if result: 998 result.extend(self.voice(DEFAULT)) 999 1000 return result 1001 1002 def _generateRowHeader(self, obj, **args): 1003 if self._script.inSayAll(): 1004 return [] 1005 1006 result = super()._generateRowHeader(obj, **args) 1007 if result: 1008 result.extend(self.voice(DEFAULT)) 1009 1010 return result 1011 1012 def _generateSortOrder(self, obj, **args): 1013 result = super()._generateSortOrder(obj, **args) 1014 if result: 1015 result.extend(self.voice(SYSTEM)) 1016 1017 return result 1018 1019 def _generateNewRowHeader(self, obj, **args): 1020 """Returns an array of strings (and possibly voice and audio 1021 specifications) that represent the row header for an object 1022 that is in a table, if it exists and if it is different from 1023 the previous row header. Otherwise, an empty array is 1024 returned. The previous row header is determined by looking at 1025 the row header for the 'priorObj' attribute of the args 1026 dictionary. The 'priorObj' is typically set by Orca to be the 1027 previous object with focus. 1028 """ 1029 1030 if not self._script.utilities.cellRowChanged(obj): 1031 return [] 1032 1033 if args.get('readingRow'): 1034 return [] 1035 1036 if args.get('inMouseReview') and args.get('priorObj'): 1037 thisrow, thiscol = self._script.utilities.coordinatesForCell(obj) 1038 lastrow, lastcol = self._script.utilities.coordinatesForCell(args.get('priorObj')) 1039 if thisrow == lastrow: 1040 return [] 1041 1042 args['newOnly'] = True 1043 return self._generateRowHeader(obj, **args) 1044 1045 def _generateNewColumnHeader(self, obj, **args): 1046 """Returns an array of strings (and possibly voice and audio 1047 specifications) that represent the column header for an object 1048 that is in a table, if it exists and if it is different from 1049 the previous column header. Otherwise, an empty array is 1050 returned. The previous column header is determined by looking 1051 at the column header for the 'priorObj' attribute of the args 1052 dictionary. The 'priorObj' is typically set by Orca to be the 1053 previous object with focus. 1054 """ 1055 1056 if not self._script.utilities.cellColumnChanged(obj): 1057 return [] 1058 1059 if args.get('readingRow'): 1060 return [] 1061 1062 if args.get('inMouseReview') and args.get('priorObj'): 1063 thisrow, thiscol = self._script.utilities.coordinatesForCell(obj) 1064 lastrow, lastcol = self._script.utilities.coordinatesForCell(args.get('priorObj')) 1065 if thiscol == lastcol: 1066 return [] 1067 1068 args['newOnly'] = True 1069 return self._generateColumnHeader(obj, **args) 1070 1071 def _generateRealTableCell(self, obj, **args): 1072 """Orca has a feature to automatically read an entire row of a table 1073 as the user arrows up/down the roles. This leads to complexity in 1074 the code. This method is used to return an array of strings 1075 (and possibly voice and audio specifications) for a single table 1076 cell itself. The string, 'blank', is added for empty cells. 1077 """ 1078 result = [] 1079 acss = self.voice(DEFAULT) 1080 oldRole = self._overrideRole('REAL_ROLE_TABLE_CELL', args) 1081 result.extend(self.generate(obj, **args)) 1082 self._restoreRole(oldRole, args) 1083 if not (result and result[0]) \ 1084 and _settingsManager.getSetting('speakBlankLines') \ 1085 and not args.get('readingRow', False): 1086 result.append(messages.BLANK) 1087 if result: 1088 result.extend(acss) 1089 1090 return result 1091 1092 def _generateUnselectedStateIfSelectable(self, obj, **args): 1093 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1094 return [] 1095 1096 if args.get('inMouseReview'): 1097 return [] 1098 1099 if not obj: 1100 return [] 1101 1102 if not (obj.parent and 'Selection' in pyatspi.listInterfaces(obj.parent)): 1103 return [] 1104 1105 state = obj.getState() 1106 if state.contains(pyatspi.STATE_SELECTED): 1107 return [] 1108 1109 result = [object_properties.STATE_UNSELECTED_LIST_ITEM] 1110 result.extend(self.voice(STATE)) 1111 1112 return result 1113 1114 def _generateUnselectedCell(self, obj, **args): 1115 """Returns an array of strings (and possibly voice and audio 1116 specifications) if this is an icon within an layered pane or a 1117 table cell within a table or a tree table and the item is 1118 focused but not selected. Otherwise, an empty array is 1119 returned. [[[WDW - I wonder if this string should be moved to 1120 settings.py.]]] 1121 """ 1122 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1123 return [] 1124 1125 if args.get('inMouseReview'): 1126 return [] 1127 1128 if not obj: 1129 return [] 1130 1131 if not (obj.parent and 'Selection' in pyatspi.listInterfaces(obj.parent)): 1132 return [] 1133 1134 state = obj.getState() 1135 if state.contains(pyatspi.STATE_SELECTED): 1136 return [] 1137 1138 if obj.getRole() == pyatspi.ROLE_TEXT: 1139 return [] 1140 1141 table = self._script.utilities.getTable(obj) 1142 if table: 1143 lastKey, mods = self._script.utilities.lastKeyAndModifiers() 1144 if lastKey in ["Left", "Right"]: 1145 return [] 1146 if self._script.utilities.isLayoutOnly(table): 1147 return [] 1148 elif obj.parent.getRole() == pyatspi.ROLE_LAYERED_PANE: 1149 if obj in self._script.utilities.selectedChildren(obj.parent): 1150 return [] 1151 else: 1152 return [] 1153 1154 result = [object_properties.STATE_UNSELECTED_TABLE_CELL] 1155 result.extend(self.voice(STATE)) 1156 1157 return result 1158 1159 def _generateColumn(self, obj, **args): 1160 """Returns an array of strings (and possibly voice and audio 1161 specifications) reflecting the column number of a cell. 1162 """ 1163 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1164 return [] 1165 1166 result = [] 1167 acss = self.voice(SYSTEM) 1168 col = -1 1169 if obj.parent.getRole() == pyatspi.ROLE_TABLE_CELL: 1170 obj = obj.parent 1171 parent = obj.parent 1172 try: 1173 table = parent.queryTable() 1174 except: 1175 if args.get('guessCoordinates', False): 1176 col = self._script.pointOfReference.get('lastColumn', -1) 1177 else: 1178 index = self._script.utilities.cellIndex(obj) 1179 col = table.getColumnAtIndex(index) 1180 if col >= 0: 1181 result.append(messages.TABLE_COLUMN % (col + 1)) 1182 if result: 1183 result.extend(acss) 1184 return result 1185 1186 def _generateRow(self, obj, **args): 1187 """Returns an array of strings (and possibly voice and audio 1188 specifications) reflecting the row number of a cell. 1189 """ 1190 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1191 return [] 1192 1193 result = [] 1194 acss = self.voice(SYSTEM) 1195 row = -1 1196 if obj.parent.getRole() == pyatspi.ROLE_TABLE_CELL: 1197 obj = obj.parent 1198 parent = obj.parent 1199 try: 1200 table = parent.queryTable() 1201 except: 1202 if args.get('guessCoordinates', False): 1203 row = self._script.pointOfReference.get('lastRow', -1) 1204 else: 1205 index = self._script.utilities.cellIndex(obj) 1206 row = table.getRowAtIndex(index) 1207 if row >= 0: 1208 result.append(messages.TABLE_ROW % (row + 1)) 1209 if result: 1210 result.extend(acss) 1211 return result 1212 1213 def _generateColumnAndRow(self, obj, **args): 1214 """Returns an array of strings (and possibly voice and audio 1215 specifications) reflecting the position of the cell in terms 1216 of its column number, the total number of columns, its row, 1217 and the total number of rows. 1218 """ 1219 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1220 return [] 1221 1222 result = [] 1223 acss = self.voice(SYSTEM) 1224 if obj.parent.getRole() == pyatspi.ROLE_TABLE_CELL: 1225 obj = obj.parent 1226 parent = obj.parent 1227 try: 1228 table = parent.queryTable() 1229 except: 1230 table = None 1231 else: 1232 index = self._script.utilities.cellIndex(obj) 1233 col = table.getColumnAtIndex(index) 1234 row = table.getRowAtIndex(index) 1235 result.append(messages.TABLE_COLUMN_DETAILED \ 1236 % {"index" : (col + 1), 1237 "total" : table.nColumns}) 1238 result.append(messages.TABLE_ROW_DETAILED \ 1239 % {"index" : (row + 1), 1240 "total" : table.nRows}) 1241 if result: 1242 result.extend(acss) 1243 return result 1244 1245 def _generateEndOfTableIndicator(self, obj, **args): 1246 """Returns an array of strings (and possibly voice and audio 1247 specifications) indicating that this cell is the last cell 1248 in the table. 1249 """ 1250 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1251 return [] 1252 1253 if _settingsManager.getSetting('speechVerbosityLevel') \ 1254 != settings.VERBOSITY_LEVEL_VERBOSE: 1255 return [] 1256 1257 if self._script.utilities.isLastCell(obj): 1258 result = [messages.TABLE_END] 1259 result.extend(self.voice(SYSTEM)) 1260 return result 1261 1262 return [] 1263 1264 ##################################################################### 1265 # # 1266 # Text interface information # 1267 # # 1268 ##################################################################### 1269 1270 def _generateCurrentLineText(self, obj, **args): 1271 """Returns an array of strings for use by speech and braille 1272 that represents the current line of text, if 1273 this is a text object. [[[WDW - consider returning an empty 1274 array if this is not a text object.]]] 1275 """ 1276 1277 if args.get('inMouseReview') and obj.getState().contains(pyatspi.STATE_EDITABLE): 1278 return [] 1279 1280 acss = self.voice(DEFAULT) 1281 result = generator.Generator._generateCurrentLineText(self, obj, **args) 1282 if not (result and result[0]): 1283 return [] 1284 1285 if result == ['\n'] and _settingsManager.getSetting('speakBlankLines') \ 1286 and not self._script.inSayAll() and args.get('total', 1) == 1: 1287 result = [messages.BLANK] 1288 1289 result[0] = self._script.utilities.adjustForRepeats(result[0]) 1290 1291 if self._script.utilities.shouldVerbalizeAllPunctuation(obj): 1292 result[0] = self._script.utilities.verbalizeAllPunctuation(result[0]) 1293 1294 if len(result) == 1: 1295 result.extend(acss) 1296 1297 return result 1298 1299 def _generateDisplayedText(self, obj, **args): 1300 result = self._generateSubstring(obj, **args) 1301 if result and result[0]: 1302 return result 1303 1304 acss = self.voice(DEFAULT) 1305 result = generator.Generator._generateDisplayedText(self, obj, **args) 1306 if not (result and result[0]): 1307 return [] 1308 1309 string = result[0].strip() 1310 if len(string) == 1 and self._script.utilities.isMath(obj): 1311 charname = chnames.getCharacterName(string, preferMath=True) 1312 if charname != string: 1313 result[0] = charname 1314 result.extend(acss) 1315 1316 return result 1317 1318 def _getCharacterAttributes(self, 1319 obj, 1320 text, 1321 textOffset, 1322 lineIndex, 1323 keys=["style", "weight", "underline"]): 1324 """Helper function that returns a string containing the 1325 given attributes from keys for the given character. 1326 """ 1327 attribStr = "" 1328 1329 defaultAttributes = text.getDefaultAttributes() 1330 keyList, attributesDictionary = \ 1331 self._script.utilities.stringToKeysAndDict(defaultAttributes) 1332 1333 charAttributes = text.getAttributes(textOffset) 1334 if charAttributes[0]: 1335 keyList, charDict = \ 1336 self._script.utilities.stringToKeysAndDict(charAttributes[0]) 1337 for key in keyList: 1338 attributesDictionary[key] = charDict[key] 1339 1340 if attributesDictionary: 1341 for key in keys: 1342 localizedKey = text_attribute_names.getTextAttributeName( 1343 key, self._script) 1344 if key in attributesDictionary: 1345 attribute = attributesDictionary[key] 1346 localizedValue = text_attribute_names.getTextAttributeName( 1347 attribute, self._script) 1348 if attribute: 1349 # If it's the 'weight' attribute and greater than 400, 1350 # just speak it as bold, otherwise speak the weight. 1351 # 1352 if key == "weight": 1353 if int(attribute) > 400: 1354 attribStr += " %s" % messages.BOLD 1355 elif key == "underline": 1356 if attribute != "none": 1357 attribStr += " %s" % localizedKey 1358 elif key == "style": 1359 if attribute != "normal": 1360 attribStr += " %s" % localizedValue 1361 else: 1362 attribStr += " " 1363 attribStr += (localizedKey + " " + localizedValue) 1364 1365 # Also check to see if this is a hypertext link. 1366 # 1367 if self._script.utilities.linkIndex(obj, textOffset) >= 0: 1368 attribStr += " %s" % messages.LINK 1369 1370 return attribStr 1371 1372 def _getTextInformation(self, obj): 1373 """Returns [textContents, startOffset, endOffset, selected] as 1374 follows: 1375 1376 A. if no text on the current line is selected, the current line 1377 B. if text is selected, the selected text 1378 C. if the current line is blank/empty, 'blank' 1379 1380 Also sets up a 'textInformation' attribute in 1381 self._script.generatorCache to prevent computing this 1382 information repeatedly while processing a single event. 1383 """ 1384 1385 try: 1386 return self._script.generatorCache['textInformation'] 1387 except: 1388 pass 1389 1390 textObj = obj.queryText() 1391 caretOffset = textObj.caretOffset 1392 1393 textContents, startOffset, endOffset = self._script.utilities.allSelectedText(obj) 1394 selected = textContents != "" 1395 1396 if not selected: 1397 # Get the line containing the caret 1398 # 1399 [line, startOffset, endOffset] = textObj.getTextAtOffset( 1400 textObj.caretOffset, 1401 pyatspi.TEXT_BOUNDARY_LINE_START) 1402 if len(line): 1403 line = self._script.utilities.adjustForRepeats(line) 1404 textContents = line 1405 else: 1406 char = textObj.getTextAtOffset(caretOffset, 1407 pyatspi.TEXT_BOUNDARY_CHAR) 1408 if char[0] == "\n" and startOffset == caretOffset: 1409 textContents = char[0] 1410 1411 if self._script.utilities.shouldVerbalizeAllPunctuation(obj): 1412 textContents = self._script.utilities.verbalizeAllPunctuation(textContents) 1413 1414 self._script.generatorCache['textInformation'] = \ 1415 [textContents, startOffset, endOffset, selected] 1416 1417 return self._script.generatorCache['textInformation'] 1418 1419 def _generateTextContent(self, obj, **args): 1420 """Returns an array of strings (and possibly voice and audio 1421 specifications) containing the text content. This requires 1422 _generateTextInformation to have been called prior to this method. 1423 """ 1424 1425 result = self._generateSubstring(obj, **args) 1426 if result: 1427 return result 1428 1429 try: 1430 text = obj.queryText() 1431 except NotImplementedError: 1432 return [] 1433 1434 result = [] 1435 acss = self.voice(DEFAULT) 1436 [line, startOffset, endOffset, selected] = \ 1437 self._getTextInformation(obj) 1438 1439 # The empty string seems to be messing with using 'or' in 1440 # formatting strings. 1441 # 1442 if line: 1443 result.append(line) 1444 result.extend(acss) 1445 1446 return result 1447 1448 def _generateTextContentWithAttributes(self, obj, **args): 1449 """Returns an array of strings (and possibly voice and audio 1450 specifications) containing the text content, obtained from the 1451 'textInformation' value, with character attribute information 1452 mixed in. This requires _generateTextInformation to have been 1453 called prior to this method. 1454 """ 1455 1456 try: 1457 text = obj.queryText() 1458 except NotImplementedError: 1459 return [] 1460 1461 acss = self.voice(DEFAULT) 1462 [line, startOffset, endOffset, selected] = \ 1463 self._getTextInformation(obj) 1464 1465 newLine = "" 1466 lastAttribs = None 1467 textOffset = startOffset 1468 for i in range(0, len(line)): 1469 attribs = self._getCharacterAttributes(obj, text, textOffset, i) 1470 if attribs and attribs != lastAttribs: 1471 if newLine: 1472 newLine += " ; " 1473 newLine += attribs 1474 newLine += " " 1475 lastAttribs = attribs 1476 newLine += line[i] 1477 textOffset += 1 1478 1479 attribs = self._getCharacterAttributes(obj, 1480 text, 1481 startOffset, 1482 0, 1483 ["paragraph-style"]) 1484 1485 if attribs: 1486 if newLine: 1487 newLine += " ; " 1488 newLine += attribs 1489 1490 result = [newLine] 1491 result.extend(acss) 1492 return result 1493 1494 def _generateAnyTextSelection(self, obj, **args): 1495 """Returns an array of strings (and possibly voice and audio 1496 specifications) that says if any of the text for the entire 1497 object is selected. [[[WDW - I wonder if this string should be 1498 moved to settings.py.]]] 1499 """ 1500 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1501 return [] 1502 1503 try: 1504 text = obj.queryText() 1505 except NotImplementedError: 1506 return [] 1507 1508 result = [] 1509 acss = self.voice(SYSTEM) 1510 1511 [line, startOffset, endOffset, selected] = \ 1512 self._getTextInformation(obj) 1513 1514 if selected: 1515 result.append(messages.TEXT_SELECTED) 1516 result.extend(acss) 1517 return result 1518 1519 def _generateAllTextSelection(self, obj, **args): 1520 """Returns an array of strings (and possibly voice and audio 1521 specifications) that says if all the text for the entire 1522 object is selected. [[[WDW - I wonder if this string should be 1523 moved to settings.py.]]] 1524 """ 1525 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1526 return [] 1527 1528 result = [] 1529 acss = self.voice(SYSTEM) 1530 try: 1531 textObj = obj.queryText() 1532 except: 1533 pass 1534 else: 1535 noOfSelections = textObj.getNSelections() 1536 if noOfSelections == 1: 1537 [string, startOffset, endOffset] = \ 1538 textObj.getTextAtOffset(0, pyatspi.TEXT_BOUNDARY_LINE_START) 1539 if startOffset == 0 and endOffset == len(string): 1540 result = [messages.TEXT_SELECTED] 1541 result.extend(acss) 1542 return result 1543 1544 def _generateSubstring(self, obj, **args): 1545 result = super()._generateSubstring(obj, **args) 1546 if not (result and result[0]): 1547 return [] 1548 1549 if not obj.getState().contains(pyatspi.STATE_EDITABLE): 1550 result[0] = result[0].strip() 1551 1552 result.extend(self._getACSS(obj, result[0])) 1553 if result[0] in ['\n', ''] and _settingsManager.getSetting('speakBlankLines') \ 1554 and not self._script.inSayAll() and args.get('total', 1) == 1: 1555 result[0] = messages.BLANK 1556 1557 if self._script.utilities.shouldVerbalizeAllPunctuation(obj): 1558 result[0] = self._script.utilities.verbalizeAllPunctuation(result[0]) 1559 1560 return result 1561 1562 def _generateTextIndentation(self, obj, **args): 1563 """Speaks a summary of the number of spaces and/or tabs at the 1564 beginning of the given line. 1565 1566 Arguments: 1567 - obj: the text object. 1568 """ 1569 1570 if not _settingsManager.getSetting('enableSpeechIndentation'): 1571 return [] 1572 1573 line, caretOffset, startOffset = self._script.getTextLineAtCaret(obj) 1574 description = self._script.utilities.indentationDescription(line) 1575 if not description: 1576 return [] 1577 1578 result = [description] 1579 result.extend(self.voice(SYSTEM)) 1580 return result 1581 1582 def _generateNestingLevel(self, obj, **args): 1583 result = super()._generateNestingLevel(obj, **args) 1584 if result: 1585 result.extend(self.voice(SYSTEM)) 1586 1587 return result 1588 1589 ##################################################################### 1590 # # 1591 # Tree interface information # 1592 # # 1593 ##################################################################### 1594 1595 def _generateNewNodeLevel(self, obj, **args): 1596 """Returns an array of strings (and possibly voice and audio 1597 specifications) that represents the tree node level of the 1598 object, or an empty array if the object is not a tree node or 1599 if the node level is not different from the 'priorObj' 1600 'priorObj' attribute of the args dictionary. The 'priorObj' 1601 is typically set by Orca to be the previous object with 1602 focus. 1603 """ 1604 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1605 return [] 1606 1607 result = [] 1608 acss = self.voice(SYSTEM) 1609 oldLevel = self._script.utilities.nodeLevel(args.get('priorObj', None)) 1610 newLevel = self._script.utilities.nodeLevel(obj) 1611 if (oldLevel != newLevel) and (newLevel >= 0): 1612 result.extend(self._generateNodeLevel(obj, **args)) 1613 result.extend(acss) 1614 return result 1615 1616 ##################################################################### 1617 # # 1618 # Value interface information # 1619 # # 1620 ##################################################################### 1621 1622 def _generateValue(self, obj, **args): 1623 result = super()._generateValue(obj, **args) 1624 if result: 1625 result.extend(self.voice(DEFAULT)) 1626 1627 return result 1628 1629 def _generatePercentage(self, obj, **args ): 1630 """Returns an array of strings (and possibly voice and audio 1631 specifications) that represents the percentage value of the 1632 object. This is typically for progress bars. [[[WDW - we 1633 should consider returning an empty array if there is no value. 1634 """ 1635 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1636 return [] 1637 1638 percentValue = self._script.utilities.getValueAsPercent(obj) 1639 if percentValue is not None: 1640 result = [messages.percentage(percentValue)] 1641 result.extend(self.voice(SYSTEM)) 1642 return result 1643 1644 return [] 1645 1646 ##################################################################### 1647 # # 1648 # Hierarchy and related dialog information # 1649 # # 1650 ##################################################################### 1651 1652 def _generateNewRadioButtonGroup(self, obj, **args): 1653 """Returns an array of strings (and possibly voice and audio 1654 specifications) that represents the radio button group label 1655 of the object, or an empty array if the object has no such 1656 label or if the radio button group is not different from the 1657 'priorObj' 'priorObj' attribute of the args dictionary. The 1658 'priorObj' is typically set by Orca to be the previous object 1659 with focus. 1660 """ 1661 # [[[TODO: WDW - hate duplicating code from _generateRadioButtonGroup 1662 # but don't want to call it because it will make the same 1663 # AT-SPI method calls.]]] 1664 # 1665 result = [] 1666 acss = self.voice(DEFAULT) 1667 priorObj = args.get('priorObj', None) 1668 if obj and obj.getRole() == pyatspi.ROLE_RADIO_BUTTON: 1669 radioGroupLabel = None 1670 inSameGroup = False 1671 relations = obj.getRelationSet() 1672 for relation in relations: 1673 if (not radioGroupLabel) \ 1674 and (relation.getRelationType() \ 1675 == pyatspi.RELATION_LABELLED_BY): 1676 radioGroupLabel = relation.getTarget(0) 1677 if (not inSameGroup) \ 1678 and (relation.getRelationType() \ 1679 == pyatspi.RELATION_MEMBER_OF): 1680 for i in range(0, relation.getNTargets()): 1681 target = relation.getTarget(i) 1682 if target == priorObj: 1683 inSameGroup = True 1684 break 1685 if (not inSameGroup) and radioGroupLabel: 1686 result.append(self._script.utilities.\ 1687 displayedText(radioGroupLabel)) 1688 result.extend(acss) 1689 return result 1690 1691 def _generateNumberOfChildren(self, obj, **args): 1692 """Returns an array of strings (and possibly voice and audio 1693 specifications) that represents the number of children the 1694 object has. [[[WDW - can we always return an empty array if 1695 this doesn't apply?]]] [[[WDW - I wonder if this string should 1696 be moved to settings.py.]]] 1697 """ 1698 1699 if _settingsManager.getSetting('onlySpeakDisplayedText') \ 1700 or _settingsManager.getSetting('speechVerbosityLevel') == settings.VERBOSITY_LEVEL_BRIEF: 1701 return [] 1702 1703 result = [] 1704 acss = self.voice(SYSTEM) 1705 childNodes = self._script.utilities.childNodes(obj) 1706 children = len(childNodes) 1707 if children: 1708 result.append(messages.itemCount(children)) 1709 result.extend(acss) 1710 return result 1711 1712 role = args.get('role', obj.getRole()) 1713 if role in [pyatspi.ROLE_LIST, pyatspi.ROLE_LIST_BOX]: 1714 children = [x for x in obj if x.getRole() == pyatspi.ROLE_LIST_ITEM] 1715 setsize = len(children) 1716 if not setsize: 1717 return [] 1718 1719 result = [messages.listItemCount(setsize)] 1720 result.extend(acss) 1721 1722 return result 1723 1724 def _generateNoShowingChildren(self, obj, **args): 1725 """Returns an array of strings (and possibly voice and audio 1726 specifications) that says if this object has no showing 1727 children (e.g., it's an empty table or list). object has. 1728 [[[WDW - can we always return an empty array if this doesn't 1729 apply?]]] [[[WDW - I wonder if this string should be moved to 1730 settings.py.]]] 1731 """ 1732 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1733 return [] 1734 1735 result = [] 1736 acss = self.voice(SYSTEM) 1737 hasItems = False 1738 for child in obj: 1739 state = child.getState() 1740 if state.contains(pyatspi.STATE_SHOWING): 1741 hasItems = True 1742 break 1743 if not hasItems: 1744 result.append(messages.ZERO_ITEMS) 1745 result.extend(acss) 1746 return result 1747 1748 def _generateNoChildren(self, obj, **args ): 1749 """Returns an array of strings (and possibly voice and audio 1750 specifications) that says if this object has no children at 1751 all (e.g., it's an empty table or list). object has. [[[WDW 1752 - can we always return an empty array if this doesn't 1753 apply?]]] [[[WDW - I wonder if this string should be moved to 1754 settings.py.]]] 1755 """ 1756 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1757 return [] 1758 1759 result = [] 1760 acss = self.voice(SYSTEM) 1761 if not obj.childCount: 1762 result.append(messages.ZERO_ITEMS) 1763 result.extend(acss) 1764 return result 1765 1766 def _generateFocusedItem(self, obj, **args): 1767 result = [] 1768 role = args.get('role', obj.getRole()) 1769 if role not in [pyatspi.ROLE_LIST, pyatspi.ROLE_LIST_BOX]: 1770 return result 1771 1772 if 'Selection' in pyatspi.listInterfaces(obj): 1773 items = self._script.utilities.selectedChildren(obj) 1774 else: 1775 items = [self._script.utilities.focusedChild(obj)] 1776 if not (items and items[0]): 1777 return result 1778 1779 for item in map(self._generateName, items): 1780 result.extend(item) 1781 1782 return result 1783 1784 def _generateSelectedItemCount(self, obj, **args): 1785 """Returns an array of strings (and possibly voice and audio 1786 specifications) indicating how many items are selected in this 1787 and the position of the current item. This object will be an icon 1788 panel or a layered pane. 1789 """ 1790 1791 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1792 return [] 1793 1794 container = obj 1795 if not 'Selection' in pyatspi.listInterfaces(container): 1796 container = obj.parent 1797 if not 'Selection' in pyatspi.listInterfaces(container): 1798 return [] 1799 1800 result = [] 1801 acss = self.voice(SYSTEM) 1802 childCount = container.childCount 1803 selectedCount = len(self._script.utilities.selectedChildren(container)) 1804 result.append(messages.selectedItemsCount(selectedCount, childCount)) 1805 result.extend(acss) 1806 result.append(self._script.formatting.getString( 1807 mode='speech', 1808 stringType='iconindex') \ 1809 % {"index" : obj.getIndexInParent() + 1, 1810 "total" : childCount}) 1811 result.extend(acss) 1812 return result 1813 1814 def _generateSelectedItems(self, obj, **args): 1815 """Returns an array of strings (and possibly voice and audio 1816 specifications) containing the names of all the selected items. 1817 This object will be an icon panel or a layered pane. 1818 """ 1819 1820 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1821 return [] 1822 1823 container = obj 1824 if not 'Selection' in pyatspi.listInterfaces(container): 1825 container = obj.parent 1826 if not 'Selection' in pyatspi.listInterfaces(container): 1827 return [] 1828 1829 selectedItems = self._script.utilities.selectedChildren(container) 1830 return list(map(self._generateLabelAndName, selectedItems)) 1831 1832 def generateSelectedItems(self, obj, **args): 1833 return self._generateSelectedItems(obj, **args) 1834 1835 def _generateUnfocusedDialogCount(self, obj, **args): 1836 """Returns an array of strings (and possibly voice and audio 1837 specifications) that says how many unfocused alerts and 1838 dialogs are associated with the application for this object. 1839 [[[WDW - I wonder if this string should be moved to 1840 settings.py.]]] 1841 """ 1842 if _settingsManager.getSetting('onlySpeakDisplayedText'): 1843 return [] 1844 1845 result = [] 1846 acss = self.voice(SYSTEM) 1847 # If this application has more than one unfocused alert or 1848 # dialog window, then speak '<m> unfocused dialogs' 1849 # to let the user know. 1850 # 1851 try: 1852 alertAndDialogCount = \ 1853 self._script.utilities.unfocusedAlertAndDialogCount(obj) 1854 except: 1855 alertAndDialogCount = 0 1856 if alertAndDialogCount > 0: 1857 result.append(messages.dialogCountSpeech(alertAndDialogCount)) 1858 result.extend(acss) 1859 return result 1860 1861 def _getEnabledAndDisabledContextRoles(self): 1862 allRoles = [pyatspi.ROLE_BLOCK_QUOTE, 1863 'ROLE_CONTENT_DELETION', 1864 'ROLE_CONTENT_INSERTION', 1865 'ROLE_CONTENT_MARK', 1866 'ROLE_CONTENT_SUGGESTION', 1867 'ROLE_DPUB_LANDMARK', 1868 'ROLE_DPUB_SECTION', 1869 pyatspi.ROLE_FORM, 1870 pyatspi.ROLE_LANDMARK, 1871 pyatspi.ROLE_LIST, 1872 pyatspi.ROLE_PANEL, 1873 'ROLE_REGION', 1874 pyatspi.ROLE_TABLE, 1875 pyatspi.ROLE_TOOL_TIP] 1876 1877 enabled, disabled = [], [] 1878 if self._script.inSayAll(): 1879 if _settingsManager.getSetting('sayAllContextBlockquote'): 1880 enabled.append(pyatspi.ROLE_BLOCK_QUOTE) 1881 if _settingsManager.getSetting('sayAllContextLandmark'): 1882 enabled.extend([pyatspi.ROLE_LANDMARK, 'ROLE_DPUB_LANDMARK']) 1883 if _settingsManager.getSetting('sayAllContextList'): 1884 enabled.append(pyatspi.ROLE_LIST) 1885 if _settingsManager.getSetting('sayAllContextPanel'): 1886 enabled.extend([pyatspi.ROLE_PANEL, 1887 pyatspi.ROLE_TOOL_TIP, 1888 'ROLE_CONTENT_DELETION', 1889 'ROLE_CONTENT_INSERTION', 1890 'ROLE_CONTENT_MARK', 1891 'ROLE_CONTENT_SUGGESTION', 1892 'ROLE_DPUB_SECTION']) 1893 if _settingsManager.getSetting('sayAllContextNonLandmarkForm'): 1894 enabled.append(pyatspi.ROLE_FORM) 1895 if _settingsManager.getSetting('sayAllContextTable'): 1896 enabled.append(pyatspi.ROLE_TABLE) 1897 else: 1898 if _settingsManager.getSetting('speakContextBlockquote'): 1899 enabled.append(pyatspi.ROLE_BLOCK_QUOTE) 1900 if _settingsManager.getSetting('speakContextLandmark'): 1901 enabled.extend([pyatspi.ROLE_LANDMARK, 'ROLE_DPUB_LANDMARK', 'ROLE_REGION']) 1902 if _settingsManager.getSetting('speakContextList'): 1903 enabled.append(pyatspi.ROLE_LIST) 1904 if _settingsManager.getSetting('speakContextPanel'): 1905 enabled.extend([pyatspi.ROLE_PANEL, 1906 pyatspi.ROLE_TOOL_TIP, 1907 'ROLE_CONTENT_DELETION', 1908 'ROLE_CONTENT_INSERTION', 1909 'ROLE_CONTENT_MARK', 1910 'ROLE_CONTENT_SUGGESTION', 1911 'ROLE_DPUB_SECTION']) 1912 if _settingsManager.getSetting('speakContextNonLandmarkForm'): 1913 enabled.append(pyatspi.ROLE_FORM) 1914 if _settingsManager.getSetting('speakContextTable'): 1915 enabled.append(pyatspi.ROLE_TABLE) 1916 1917 disabled = list(set(allRoles).symmetric_difference(enabled)) 1918 return enabled, disabled 1919 1920 def _generateLeaving(self, obj, **args): 1921 if not args.get('leaving'): 1922 return [] 1923 1924 role = args.get('role', obj.getRole()) 1925 enabled, disabled = self._getEnabledAndDisabledContextRoles() 1926 if not (role in enabled or self._script.utilities.isDetails(obj)): 1927 return [] 1928 1929 count = args.get('count', 1) 1930 1931 result = [] 1932 if self._script.utilities.isDetails(obj): 1933 result.append(messages.LEAVING_DETAILS) 1934 elif role == pyatspi.ROLE_BLOCK_QUOTE: 1935 if count > 1: 1936 result.append(messages.leavingNBlockquotes(count)) 1937 else: 1938 result.append(messages.LEAVING_BLOCKQUOTE) 1939 elif role == pyatspi.ROLE_LIST and self._script.utilities.isDocumentList(obj): 1940 if count > 1: 1941 result.append(messages.leavingNLists(count)) 1942 else: 1943 result.append(messages.LEAVING_LIST) 1944 elif role == pyatspi.ROLE_PANEL: 1945 if self._script.utilities.isFeed(obj): 1946 result.append(messages.LEAVING_FEED) 1947 elif self._script.utilities.isFigure(obj): 1948 result.append(messages.LEAVING_FIGURE) 1949 elif self._script.utilities.isDocumentPanel(obj): 1950 result.append(messages.LEAVING_PANEL) 1951 else: 1952 result = [''] 1953 elif role == pyatspi.ROLE_TABLE and self._script.utilities.isTextDocumentTable(obj): 1954 result.append(messages.LEAVING_TABLE) 1955 elif role == 'ROLE_DPUB_LANDMARK': 1956 if self._script.utilities.isDPubAcknowledgments(obj): 1957 result.append(messages.LEAVING_ACKNOWLEDGMENTS) 1958 elif self._script.utilities.isDPubAfterword(obj): 1959 result.append(messages.LEAVING_AFTERWORD) 1960 elif self._script.utilities.isDPubAppendix(obj): 1961 result.append(messages.LEAVING_APPENDIX) 1962 elif self._script.utilities.isDPubBibliography(obj): 1963 result.append(messages.LEAVING_BIBLIOGRAPHY) 1964 elif self._script.utilities.isDPubChapter(obj): 1965 result.append(messages.LEAVING_CHAPTER) 1966 elif self._script.utilities.isDPubConclusion(obj): 1967 result.append(messages.LEAVING_CONCLUSION) 1968 elif self._script.utilities.isDPubCredits(obj): 1969 result.append(messages.LEAVING_CREDITS) 1970 elif self._script.utilities.isDPubEndnotes(obj): 1971 result.append(messages.LEAVING_ENDNOTES) 1972 elif self._script.utilities.isDPubEpilogue(obj): 1973 result.append(messages.LEAVING_EPILOGUE) 1974 elif self._script.utilities.isDPubErrata(obj): 1975 result.append(messages.LEAVING_ERRATA) 1976 elif self._script.utilities.isDPubForeword(obj): 1977 result.append(messages.LEAVING_FOREWORD) 1978 elif self._script.utilities.isDPubGlossary(obj): 1979 result.append(messages.LEAVING_GLOSSARY) 1980 elif self._script.utilities.isDPubIndex(obj): 1981 result.append(messages.LEAVING_INDEX) 1982 elif self._script.utilities.isDPubIntroduction(obj): 1983 result.append(messages.LEAVING_INTRODUCTION) 1984 elif self._script.utilities.isDPubPagelist(obj): 1985 result.append(messages.LEAVING_PAGELIST) 1986 elif self._script.utilities.isDPubPart(obj): 1987 result.append(messages.LEAVING_PART) 1988 elif self._script.utilities.isDPubPreface(obj): 1989 result.append(messages.LEAVING_PREFACE) 1990 elif self._script.utilities.isDPubPrologue(obj): 1991 result.append(messages.LEAVING_PROLOGUE) 1992 elif self._script.utilities.isDPubToc(obj): 1993 result.append(messages.LEAVING_TOC) 1994 elif role == 'ROLE_DPUB_SECTION': 1995 if self._script.utilities.isDPubAbstract(obj): 1996 result.append(messages.LEAVING_ABSTRACT) 1997 elif self._script.utilities.isDPubColophon(obj): 1998 result.append(messages.LEAVING_COLOPHON) 1999 elif self._script.utilities.isDPubCredit(obj): 2000 result.append(messages.LEAVING_CREDIT) 2001 elif self._script.utilities.isDPubDedication(obj): 2002 result.append(messages.LEAVING_DEDICATION) 2003 elif self._script.utilities.isDPubEpigraph(obj): 2004 result.append(messages.LEAVING_EPIGRAPH) 2005 elif self._script.utilities.isDPubExample(obj): 2006 result.append(messages.LEAVING_EXAMPLE) 2007 elif self._script.utilities.isDPubPullquote(obj): 2008 result.append(messages.LEAVING_PULLQUOTE) 2009 elif self._script.utilities.isDPubQna(obj): 2010 result.append(messages.LEAVING_QNA) 2011 elif self._script.utilities.isLandmark(obj): 2012 if self._script.utilities.isLandmarkBanner(obj): 2013 result.append(messages.LEAVING_LANDMARK_BANNER) 2014 elif self._script.utilities.isLandmarkComplementary(obj): 2015 result.append(messages.LEAVING_LANDMARK_COMPLEMENTARY) 2016 elif self._script.utilities.isLandmarkContentInfo(obj): 2017 result.append(messages.LEAVING_LANDMARK_CONTENTINFO) 2018 elif self._script.utilities.isLandmarkMain(obj): 2019 result.append(messages.LEAVING_LANDMARK_MAIN) 2020 elif self._script.utilities.isLandmarkNavigation(obj): 2021 result.append(messages.LEAVING_LANDMARK_NAVIGATION) 2022 elif self._script.utilities.isLandmarkRegion(obj): 2023 result.append(messages.LEAVING_LANDMARK_REGION) 2024 elif self._script.utilities.isLandmarkSearch(obj): 2025 result.append(messages.LEAVING_LANDMARK_SEARCH) 2026 elif self._script.utilities.isLandmarkForm(obj): 2027 result.append(messages.LEAVING_FORM) 2028 else: 2029 result = [''] 2030 elif role == pyatspi.ROLE_FORM: 2031 result.append(messages.LEAVING_FORM) 2032 elif role == pyatspi.ROLE_TOOL_TIP: 2033 result.append(messages.LEAVING_TOOL_TIP) 2034 elif role == 'ROLE_CONTENT_DELETION': 2035 result.append(messages.CONTENT_DELETION_END) 2036 elif role == 'ROLE_CONTENT_INSERTION': 2037 result.append(messages.CONTENT_INSERTION_END) 2038 elif role == 'ROLE_CONTENT_MARK': 2039 result.append(messages.CONTENT_MARK_END) 2040 elif role == 'ROLE_CONTENT_SUGGESTION' \ 2041 and not self._script.utilities.isInlineSuggestion(obj): 2042 result.append(messages.LEAVING_SUGGESTION) 2043 else: 2044 result = [''] 2045 if result: 2046 result.extend(self.voice(SYSTEM)) 2047 2048 return result 2049 2050 def _generateAncestors(self, obj, **args): 2051 """Returns an array of strings (and possibly voice and audio 2052 specifications) that represent the text of the ancestors for 2053 the object. This is typically used to present the context for 2054 an object (e.g., the names of the window, the panels, etc., 2055 that the object is contained in). If the 'priorObj' attribute 2056 of the args dictionary is set, only the differences in 2057 ancestry between the 'priorObj' and the current obj will be 2058 computed. The 'priorObj' is typically set by Orca to be the 2059 previous object with focus. 2060 """ 2061 result = [] 2062 2063 leaving = args.get('leaving') 2064 if leaving and args.get('priorObj'): 2065 priorObj = obj 2066 obj = args.get('priorObj') 2067 else: 2068 priorObj = args.get('priorObj') 2069 2070 if priorObj and self._script.utilities.isDead(priorObj): 2071 return [] 2072 2073 if priorObj and priorObj.getRole() == pyatspi.ROLE_TOOL_TIP: 2074 return [] 2075 2076 if priorObj and priorObj.parent == obj.parent: 2077 return [] 2078 2079 if self._script.utilities.isTypeahead(priorObj): 2080 return [] 2081 2082 commonAncestor = self._script.utilities.commonAncestor(priorObj, obj) 2083 if obj == commonAncestor: 2084 return [] 2085 2086 includeOnly = args.get('includeOnly', []) 2087 2088 skipRoles = args.get('skipRoles', []) 2089 skipRoles.append(pyatspi.ROLE_TREE_ITEM) 2090 enabled, disabled = self._getEnabledAndDisabledContextRoles() 2091 skipRoles.extend(disabled) 2092 2093 stopAtRoles = args.get('stopAtRoles', []) 2094 stopAtRoles.extend([pyatspi.ROLE_APPLICATION, pyatspi.ROLE_MENU_BAR]) 2095 2096 stopAfterRoles = args.get('stopAfterRoles', []) 2097 stopAfterRoles.extend([pyatspi.ROLE_TOOL_TIP]) 2098 2099 presentOnce = [pyatspi.ROLE_BLOCK_QUOTE, pyatspi.ROLE_LIST] 2100 2101 presentCommonAncestor = False 2102 if commonAncestor and not leaving: 2103 commonRole = self._getAlternativeRole(commonAncestor) 2104 if commonRole in presentOnce: 2105 pred = lambda x: x and self._getAlternativeRole(x) == commonRole 2106 objAncestor = pyatspi.findAncestor(obj, pred) 2107 priorAncestor = pyatspi.findAncestor(priorObj, pred) 2108 objLevel = self._script.utilities.nestingLevel(objAncestor) 2109 priorLevel = self._script.utilities.nestingLevel(priorAncestor) 2110 presentCommonAncestor = objLevel != priorLevel 2111 2112 ancestors, ancestorRoles = [], [] 2113 parent = obj.parent 2114 while parent and parent != parent.parent: 2115 parentRole = self._getAlternativeRole(parent) 2116 if parentRole in stopAtRoles: 2117 break 2118 if parentRole in skipRoles: 2119 pass 2120 elif includeOnly and parentRole not in includeOnly: 2121 pass 2122 elif self._script.utilities.isLayoutOnly(parent): 2123 pass 2124 elif self._script.utilities.isButtonWithPopup(parent): 2125 pass 2126 elif parent != commonAncestor or presentCommonAncestor: 2127 ancestors.append(parent) 2128 ancestorRoles.append(parentRole) 2129 2130 if parent == commonAncestor or parentRole in stopAfterRoles: 2131 break 2132 2133 parent = parent.parent 2134 2135 presentedRoles = [] 2136 for i, x in enumerate(ancestors): 2137 altRole = ancestorRoles[i] 2138 if altRole in presentOnce and altRole in presentedRoles: 2139 continue 2140 2141 presentedRoles.append(altRole) 2142 count = ancestorRoles.count(altRole) 2143 self._overrideRole(altRole, args) 2144 result.append(self.generate(x, formatType='focused', role=altRole, leaving=leaving, count=count, 2145 ancestorOf=obj)) 2146 self._restoreRole(altRole, args) 2147 2148 if not leaving: 2149 result.reverse() 2150 return result 2151 2152 def _generateOldAncestors(self, obj, **args): 2153 """Returns an array of strings (and possibly voice and audio 2154 specifications) that represent the text of the ancestors for 2155 the object being left.""" 2156 2157 if _settingsManager.getSetting('onlySpeakDisplayedText'): 2158 return [] 2159 2160 if self._script.utilities.inFindContainer(): 2161 return [] 2162 2163 priorObj = args.get('priorObj') 2164 if not priorObj or obj == priorObj or self._script.utilities.isZombie(priorObj): 2165 return [] 2166 2167 if obj.getRole() == pyatspi.ROLE_PAGE_TAB: 2168 return [] 2169 2170 if obj.getApplication() != priorObj.getApplication() \ 2171 or pyatspi.findAncestor(obj, lambda x: x == priorObj): 2172 return [] 2173 2174 frame, dialog = self._script.utilities.frameAndDialog(obj) 2175 if dialog: 2176 return [] 2177 2178 args['leaving'] = True 2179 args['includeOnly'] = [pyatspi.ROLE_BLOCK_QUOTE, 2180 pyatspi.ROLE_FORM, 2181 pyatspi.ROLE_LANDMARK, 2182 'ROLE_CONTENT_DELETION', 2183 'ROLE_CONTENT_INSERTION', 2184 'ROLE_CONTENT_MARK', 2185 'ROLE_CONTENT_SUGGESTION', 2186 'ROLE_DPUB_LANDMARK', 2187 'ROLE_DPUB_SECTION', 2188 pyatspi.ROLE_LIST, 2189 pyatspi.ROLE_PANEL, 2190 'ROLE_REGION', 2191 pyatspi.ROLE_TABLE, 2192 pyatspi.ROLE_TOOL_TIP] 2193 2194 result = [] 2195 if self._script.utilities.isBlockquote(priorObj): 2196 oldRole = self._getAlternativeRole(priorObj) 2197 self._overrideRole(oldRole, args) 2198 result.extend(self.generate( 2199 priorObj, role=oldRole, formatType='focused', leaving=True)) 2200 self._restoreRole(oldRole, args) 2201 2202 result.extend(self._generateAncestors(obj, **args)) 2203 args.pop('leaving') 2204 args.pop('includeOnly') 2205 2206 return result 2207 2208 def _generateNewAncestors(self, obj, **args): 2209 """Returns an array of strings (and possibly voice and audio 2210 specifications) that represent the text of the ancestors for 2211 the object. This is typically used to present the context for 2212 an object (e.g., the names of the window, the panels, etc., 2213 that the object is contained in). If the 'priorObj' attribute 2214 of the args dictionary is set, only the differences in 2215 ancestry between the 'priorObj' and the current obj will be 2216 computed. Otherwise, no ancestry will be computed. The 2217 'priorObj' is typically set by Orca to be the previous object 2218 with focus. 2219 """ 2220 2221 if _settingsManager.getSetting('onlySpeakDisplayedText'): 2222 return [] 2223 2224 if self._script.utilities.inFindContainer(): 2225 return [] 2226 2227 priorObj = args.get('priorObj') 2228 if priorObj == obj: 2229 return [] 2230 2231 role = args.get('role', obj.getRole()) 2232 if role in [pyatspi.ROLE_FRAME, pyatspi.ROLE_WINDOW]: 2233 return [] 2234 2235 result = [] 2236 if role == pyatspi.ROLE_MENU_ITEM \ 2237 and (not priorObj or priorObj.getRole() == pyatspi.ROLE_WINDOW): 2238 return result 2239 2240 topLevelObj = self._script.utilities.topLevelObject(obj) 2241 if priorObj \ 2242 or (topLevelObj and topLevelObj.getRole() == pyatspi.ROLE_DIALOG): 2243 result = self._generateAncestors(obj, **args) 2244 return result 2245 2246 def generateContext(self, obj, **args): 2247 if args.get('priorObj') == obj: 2248 return [] 2249 2250 result = self._generateOldAncestors(obj, **args) 2251 result.append(self._generateNewAncestors(obj, **args)) 2252 return result 2253 2254 def _generateParentRoleName(self, obj, **args): 2255 """Returns an array of strings (and possibly voice and audio 2256 specifications) containing the role name of the parent of obj. 2257 """ 2258 if args.get('role', obj.getRole()) == pyatspi.ROLE_ICON \ 2259 and args.get('formatType', None) \ 2260 in ['basicWhereAmI', 'detailedWhereAmI']: 2261 return [object_properties.ROLE_ICON_PANEL] 2262 if obj.parent.getRole() in [pyatspi.ROLE_TABLE_CELL, pyatspi.ROLE_MENU]: 2263 obj = obj.parent 2264 return self._generateRoleName(obj.parent) 2265 2266 def _generateToolbar(self, obj, **args): 2267 """Returns an array of strings (and possibly voice and audio 2268 specifications) containing the name and role of the toolbar 2269 which contains obj. 2270 """ 2271 result = [] 2272 ancestor = self._script.utilities.ancestorWithRole( 2273 obj, [pyatspi.ROLE_TOOL_BAR], [pyatspi.ROLE_FRAME]) 2274 if ancestor: 2275 result.extend(self._generateLabelAndName(ancestor)) 2276 result.extend(self._generateRoleName(ancestor)) 2277 return result 2278 2279 def _generatePositionInGroup(self, obj, **args): 2280 """Returns an array of strings (and possibly voice and audio 2281 specifications) that represent the relative position of an 2282 object in a group. 2283 """ 2284 if _settingsManager.getSetting('onlySpeakDisplayedText'): 2285 return [] 2286 2287 result = [] 2288 acss = self.voice(SYSTEM) 2289 position = -1 2290 total = -1 2291 2292 try: 2293 relations = obj.getRelationSet() 2294 except: 2295 relations = [] 2296 for relation in relations: 2297 if relation.getRelationType() == pyatspi.RELATION_MEMBER_OF: 2298 total = 0 2299 for i in range(0, relation.getNTargets()): 2300 target = relation.getTarget(i) 2301 if target.getState().contains(pyatspi.STATE_SHOWING): 2302 total += 1 2303 if target == obj: 2304 position = total 2305 2306 if position >= 0: 2307 # Adjust the position because the relations tend to be given 2308 # in the reverse order. 2309 position = total - position + 1 2310 result.append(self._script.formatting.getString( 2311 mode='speech', 2312 stringType='groupindex') \ 2313 % {"index" : position, 2314 "total" : total}) 2315 result.extend(acss) 2316 return result 2317 2318 def _generatePositionInList(self, obj, **args): 2319 """Returns an array of strings (and possibly voice and audio 2320 specifications) that represent the relative position of an 2321 object in a list. 2322 """ 2323 2324 if _settingsManager.getSetting('onlySpeakDisplayedText') \ 2325 or not (_settingsManager.getSetting('enablePositionSpeaking') \ 2326 or args.get('forceList', False)): 2327 return [] 2328 2329 if self._script.utilities.isTopLevelMenu(obj): 2330 return [] 2331 2332 if self._script.utilities.isEditableComboBox(obj): 2333 return [] 2334 2335 result = [] 2336 acss = self.voice(SYSTEM) 2337 position, total = self._script.utilities.getPositionAndSetSize(obj, **args) 2338 if position < 0 or total < 0: 2339 return [] 2340 2341 position += 1 2342 result.append(self._script.formatting.getString( 2343 mode='speech', 2344 stringType='groupindex') \ 2345 % {"index" : position, 2346 "total" : total}) 2347 result.extend(acss) 2348 return result 2349 2350 def _generateProgressBarIndex(self, obj, **args): 2351 if not args.get('isProgressBarUpdate') \ 2352 or not self._shouldPresentProgressBarUpdate(obj, **args): 2353 return [] 2354 2355 result = [] 2356 acc, updateTime, updateValue = self._getMostRecentProgressBarUpdate() 2357 if acc != obj: 2358 number, count = self.getProgressBarNumberAndCount(obj) 2359 result = [messages.PROGRESS_BAR_NUMBER % (number)] 2360 result.extend(self.voice(SYSTEM)) 2361 2362 return result 2363 2364 def _generateProgressBarValue(self, obj, **args): 2365 if args.get('isProgressBarUpdate') \ 2366 and not self._shouldPresentProgressBarUpdate(obj, **args): 2367 return [''] 2368 2369 result = [] 2370 percent = self._script.utilities.getValueAsPercent(obj) 2371 if percent is not None: 2372 result.append(messages.percentage(percent)) 2373 result.extend(self.voice(SYSTEM)) 2374 2375 return result 2376 2377 def _getProgressBarUpdateInterval(self): 2378 interval = _settingsManager.getSetting('progressBarSpeechInterval') 2379 if interval is None: 2380 interval = super()._getProgressBarUpdateInterval() 2381 2382 return int(interval) 2383 2384 def _shouldPresentProgressBarUpdate(self, obj, **args): 2385 if not _settingsManager.getSetting('speakProgressBarUpdates'): 2386 return False 2387 2388 return super()._shouldPresentProgressBarUpdate(obj, **args) 2389 2390 def _generateDefaultButton(self, obj, **args): 2391 """Returns an array of strings (and possibly voice and audio 2392 specifications) that represent the default button in a dialog. 2393 This method should initially be called with a top-level window. 2394 """ 2395 result = [] 2396 button = self._script.utilities.defaultButton(obj) 2397 if button and button.getState().contains(pyatspi.STATE_SENSITIVE): 2398 name = self._generateName(button) 2399 if name: 2400 result.append(messages.DEFAULT_BUTTON_IS % name[0]) 2401 result.extend(self.voice(SYSTEM)) 2402 2403 return result 2404 2405 def generateDefaultButton(self, obj, **args): 2406 """Returns an array of strings (and possibly voice and audio 2407 specifications) that represent the default button of the window 2408 containing the object. 2409 """ 2410 return self._generateDefaultButton(obj, **args) 2411 2412 def _generateStatusBar(self, obj, **args): 2413 """Returns an array of strings (and possibly voice and audio 2414 specifications) that represent the status bar of a window. 2415 """ 2416 2417 statusBar = self._script.utilities.statusBar(obj) 2418 if not statusBar: 2419 return [] 2420 2421 items = self._script.utilities.statusBarItems(statusBar) 2422 if not items or items == [statusBar]: 2423 return [] 2424 2425 result = [] 2426 for child in items: 2427 if child == statusBar: 2428 continue 2429 2430 childResult = self.generate(child, includeContext=False) 2431 if childResult: 2432 result.extend(childResult) 2433 if not isinstance(childResult[-1], Pause): 2434 result.extend(self._generatePause(child, **args)) 2435 2436 return result 2437 2438 def generateTitle(self, obj, **args): 2439 """Returns an array of strings (and possibly voice and audio 2440 specifications) that represent the title of the window, obj. 2441 containing the object, along with information associated with 2442 any unfocused dialog boxes. 2443 """ 2444 result = [] 2445 acss = self.voice(DEFAULT) 2446 frame, dialog = self._script.utilities.frameAndDialog(obj) 2447 if frame: 2448 frameResult = self._generateLabelAndName(frame) 2449 if not frameResult: 2450 frameResult = self._generateRoleName(frame) 2451 result.append(frameResult) 2452 2453 if dialog: 2454 result.append(self._generateLabelAndName(dialog)) 2455 2456 alertAndDialogCount = self._script.utilities.unfocusedAlertAndDialogCount(obj) 2457 if alertAndDialogCount > 0: 2458 dialogs = [messages.dialogCountSpeech(alertAndDialogCount)] 2459 dialogs.extend(acss) 2460 result.append(dialogs) 2461 return result 2462 2463 def _generateListBoxItemWidgets(self, obj, **args): 2464 widgetRoles = [pyatspi.ROLE_CHECK_BOX, 2465 pyatspi.ROLE_COMBO_BOX, 2466 pyatspi.ROLE_PUSH_BUTTON, 2467 pyatspi.ROLE_RADIO_BUTTON, 2468 pyatspi.ROLE_SLIDER, 2469 pyatspi.ROLE_TEXT, 2470 pyatspi.ROLE_TOGGLE_BUTTON] 2471 isWidget = lambda x: x and x.getRole() in widgetRoles 2472 result = [] 2473 if obj.parent and obj.parent.getRole() == pyatspi.ROLE_LIST_BOX: 2474 widgets = self._script.utilities.findAllDescendants(obj, isWidget) 2475 for widget in widgets: 2476 if self._script.utilities.isShowingAndVisible(widget): 2477 result.append(self.generate(widget, includeContext=False)) 2478 2479 return result 2480 2481 ##################################################################### 2482 # # 2483 # Keyboard shortcut information # 2484 # # 2485 ##################################################################### 2486 2487 def _generateAccelerator(self, obj, **args): 2488 """Returns an array of strings (and possibly voice and audio 2489 specifications) that represent the accelerator for the object, 2490 or an empty array if no accelerator can be found. 2491 """ 2492 if _settingsManager.getSetting('onlySpeakDisplayedText'): 2493 return [] 2494 2495 result = [] 2496 acss = self.voice(SYSTEM) 2497 [mnemonic, shortcut, accelerator] = \ 2498 self._script.utilities.mnemonicShortcutAccelerator(obj) 2499 if accelerator: 2500 result.append(accelerator) 2501 result.extend(acss) 2502 2503 return result 2504 2505 def _generateMnemonic(self, obj, **args): 2506 """Returns an array of strings (and possibly voice and audio 2507 specifications) that represent the mnemonic for the object, or 2508 an empty array if no mnemonic can be found. 2509 """ 2510 if _settingsManager.getSetting('onlySpeakDisplayedText'): 2511 return [] 2512 2513 result = [] 2514 acss = self.voice(SYSTEM) 2515 if _settingsManager.getSetting('enableMnemonicSpeaking') \ 2516 or args.get('forceMnemonic', False): 2517 [mnemonic, shortcut, accelerator] = \ 2518 self._script.utilities.mnemonicShortcutAccelerator(obj) 2519 if mnemonic: 2520 mnemonic = mnemonic[-1] # we just want a single character 2521 if not mnemonic and shortcut: 2522 mnemonic = shortcut 2523 if mnemonic: 2524 result = [mnemonic] 2525 result.extend(acss) 2526 2527 return result 2528 2529 ##################################################################### 2530 # # 2531 # Tutorial information # 2532 # # 2533 ##################################################################### 2534 2535 def _generateTutorial(self, obj, **args): 2536 """Returns an array of strings (and possibly voice and audio 2537 specifications) that represent the tutorial for the object. 2538 The tutorial will only be generated if the user has requested 2539 tutorials, and will then be generated according to the 2540 tutorial generator. A tutorial can be forced by setting the 2541 'forceTutorial' attribute of the args dictionary to True. 2542 """ 2543 if _settingsManager.getSetting('onlySpeakDisplayedText'): 2544 return [] 2545 2546 result = [] 2547 acss = self.voice(SYSTEM) 2548 alreadyFocused = args.get('alreadyFocused', False) 2549 forceTutorial = args.get('forceTutorial', False) 2550 role = args.get('role', obj.getRole()) 2551 result.extend(self._script.tutorialGenerator.getTutorial( 2552 obj, 2553 alreadyFocused, 2554 forceTutorial, 2555 role)) 2556 if args.get('role', obj.getRole()) == pyatspi.ROLE_ICON \ 2557 and args.get('formatType', 'unfocused') == 'basicWhereAmI': 2558 frame, dialog = self._script.utilities.frameAndDialog(obj) 2559 if frame: 2560 result.extend(self._script.tutorialGenerator.getTutorial( 2561 frame, 2562 alreadyFocused, 2563 forceTutorial)) 2564 if result and result[0]: 2565 result.extend(acss) 2566 return result 2567 2568 # Math 2569 2570 def _generateMath(self, obj, **args): 2571 result = [] 2572 children = [child for child in obj] 2573 if not children and not self._script.utilities.isMathTopLevel(obj): 2574 children = [obj] 2575 2576 for child in children: 2577 if self._script.utilities.isMathLayoutOnly(child) and child.childCount: 2578 result.extend(self._generateMath(child)) 2579 continue 2580 2581 oldRole = self._getAlternativeRole(child) 2582 self._overrideRole(oldRole, args) 2583 result.extend(self.generate(child, role=oldRole)) 2584 self._restoreRole(oldRole, args) 2585 2586 return result 2587 2588 def _generateEnclosedBase(self, obj, **args): 2589 return self._generateMath(obj, **args) 2590 2591 def _generateEnclosedEnclosures(self, obj, **args): 2592 strings = [] 2593 enclosures = self._script.utilities.getMathEnclosures(obj) 2594 if 'actuarial' in enclosures: 2595 strings.append(messages.MATH_ENCLOSURE_ACTUARIAL) 2596 if 'box' in enclosures: 2597 strings.append(messages.MATH_ENCLOSURE_BOX) 2598 if 'circle' in enclosures: 2599 strings.append(messages.MATH_ENCLOSURE_CIRCLE) 2600 if 'longdiv' in enclosures: 2601 strings.append(messages.MATH_ENCLOSURE_LONGDIV) 2602 if 'radical' in enclosures: 2603 strings.append(messages.MATH_ENCLOSURE_RADICAL) 2604 if 'roundedbox' in enclosures: 2605 strings.append(messages.MATH_ENCLOSURE_ROUNDEDBOX) 2606 if 'horizontalstrike' in enclosures: 2607 strings.append(messages.MATH_ENCLOSURE_HORIZONTALSTRIKE) 2608 if 'verticalstrike' in enclosures: 2609 strings.append(messages.MATH_ENCLOSURE_VERTICALSTRIKE) 2610 if 'downdiagonalstrike' in enclosures: 2611 strings.append(messages.MATH_ENCLOSURE_DOWNDIAGONALSTRIKE) 2612 if 'updiagonalstrike' in enclosures: 2613 strings.append(messages.MATH_ENCLOSURE_UPDIAGONALSTRIKE) 2614 if 'northeastarrow' in enclosures: 2615 strings.append(messages.MATH_ENCLOSURE_NORTHEASTARROW) 2616 if 'bottom' in enclosures: 2617 strings.append(messages.MATH_ENCLOSURE_BOTTOM) 2618 if 'left' in enclosures: 2619 strings.append(messages.MATH_ENCLOSURE_LEFT) 2620 if 'right' in enclosures: 2621 strings.append(messages.MATH_ENCLOSURE_RIGHT) 2622 if 'top' in enclosures: 2623 strings.append(messages.MATH_ENCLOSURE_TOP) 2624 if 'phasorangle' in enclosures: 2625 strings.append(messages.MATH_ENCLOSURE_PHASOR_ANGLE) 2626 if 'madruwb' in enclosures: 2627 strings.append(messages.MATH_ENCLOSURE_MADRUWB) 2628 if not strings: 2629 msg = 'INFO: Could not get enclosure message for %s' % enclosures 2630 debug.println(debug.LEVEL_INFO, msg) 2631 return [] 2632 2633 if len(strings) == 1: 2634 result = [messages.MATH_ENCLOSURE_ENCLOSED_BY % strings[0]] 2635 else: 2636 strings.insert(-1, messages.MATH_ENCLOSURE_AND) 2637 if len(strings) == 3: 2638 result = [messages.MATH_ENCLOSURE_ENCLOSED_BY % " ".join(strings)] 2639 else: 2640 result = [messages.MATH_ENCLOSURE_ENCLOSED_BY % ", ".join(strings)] 2641 2642 result.extend(self.voice(SYSTEM)) 2643 return result 2644 2645 def _generateFencedStart(self, obj, **args): 2646 fenceStart, fenceEnd = self._script.utilities.getMathFences(obj) 2647 if fenceStart: 2648 result = [chnames.getCharacterName(fenceStart)] 2649 result.extend(self.voice(DEFAULT)) 2650 return result 2651 2652 return [] 2653 2654 def _generateFencedContents(self, obj, **args): 2655 result = [] 2656 separators = self._script.utilities.getMathFencedSeparators(obj) 2657 for x in range(len(separators), obj.childCount-1): 2658 separators.append(separators[-1]) 2659 separators.append('') 2660 2661 for i, child in enumerate(obj): 2662 result.extend(self._generateMath(child, **args)) 2663 separatorName = chnames.getCharacterName(separators[i]) 2664 result.append(separatorName) 2665 result.extend(self.voice(DEFAULT)) 2666 if separatorName: 2667 result.extend(self._generatePause(obj, **args)) 2668 2669 return result 2670 2671 def _generateFencedEnd(self, obj, **args): 2672 fenceStart, fenceEnd = self._script.utilities.getMathFences(obj) 2673 if fenceEnd: 2674 result = [chnames.getCharacterName(fenceEnd)] 2675 result.extend(self.voice(DEFAULT)) 2676 return result 2677 2678 return [] 2679 2680 def _generateFractionStart(self, obj, **args): 2681 if self._script.utilities.isMathFractionWithoutBar(obj): 2682 result = [messages.MATH_FRACTION_WITHOUT_BAR_START] 2683 else: 2684 result = [messages.MATH_FRACTION_START] 2685 result.extend(self.voice(SYSTEM)) 2686 return result 2687 2688 def _generateFractionNumerator(self, obj, **args): 2689 numerator = self._script.utilities.getMathNumerator(obj) 2690 if self._script.utilities.isMathLayoutOnly(numerator): 2691 return self._generateMath(numerator) 2692 2693 oldRole = self._getAlternativeRole(numerator) 2694 self._overrideRole(oldRole, args) 2695 result = self.generate(numerator, role=oldRole) 2696 self._restoreRole(oldRole, args) 2697 return result 2698 2699 def _generateFractionDenominator(self, obj, **args): 2700 denominator = self._script.utilities.getMathDenominator(obj) 2701 if self._script.utilities.isMathLayoutOnly(denominator): 2702 return self._generateMath(denominator) 2703 2704 oldRole = self._getAlternativeRole(denominator) 2705 self._overrideRole(oldRole, args) 2706 result = self.generate(denominator, role=oldRole) 2707 self._restoreRole(oldRole, args) 2708 return result 2709 2710 def _generateFractionLine(self, obj, **args): 2711 result = [messages.MATH_FRACTION_LINE] 2712 result.extend(self.voice(SYSTEM)) 2713 return result 2714 2715 def _generateFractionEnd(self, obj, **args): 2716 result = [messages.MATH_FRACTION_END] 2717 result.extend(self.voice(SYSTEM)) 2718 return result 2719 2720 def _generateRootStart(self, obj, **args): 2721 result = [] 2722 if self._script.utilities.isMathSquareRoot(obj): 2723 result = [messages.MATH_SQUARE_ROOT_OF] 2724 else: 2725 index = self._script.utilities.getMathRootIndex(obj) 2726 string = self._script.utilities.displayedText(index) 2727 if string == "2": 2728 result = [messages.MATH_SQUARE_ROOT_OF] 2729 elif string == "3": 2730 result = [messages.MATH_CUBE_ROOT_OF] 2731 elif string: 2732 result = [string] 2733 result.extend([messages.MATH_ROOT_OF]) 2734 elif self._script.utilities.isMathLayoutOnly(index): 2735 result = self._generateMath(index) 2736 result.extend([messages.MATH_ROOT_OF]) 2737 else: 2738 oldRole = self._getAlternativeRole(index) 2739 self._overrideRole(oldRole, args) 2740 result.extend(self.generate(index, role=oldRole)) 2741 self._restoreRole(oldRole, args) 2742 result.extend([messages.MATH_ROOT_OF]) 2743 2744 if result: 2745 result.extend(self.voice(SYSTEM)) 2746 2747 return result 2748 2749 def _generateRootBase(self, obj, **args): 2750 base = self._script.utilities.getMathRootBase(obj) 2751 if not base: 2752 return [] 2753 2754 if self._script.utilities.isMathSquareRoot(obj) \ 2755 or self._script.utilities.isMathToken(base) \ 2756 or self._script.utilities.isMathLayoutOnly(base): 2757 return self._generateMath(base) 2758 2759 result = [self._generatePause(obj, **args)] 2760 oldRole = self._getAlternativeRole(base) 2761 self._overrideRole(oldRole, args) 2762 result.extend(self.generate(base, role=oldRole)) 2763 self._restoreRole(oldRole, args) 2764 2765 return result 2766 2767 def _generateRootEnd(self, obj, **args): 2768 result = [messages.MATH_ROOT_END] 2769 result.extend(self.voice(SYSTEM)) 2770 return result 2771 2772 def _generateScriptBase(self, obj, **args): 2773 base = self._script.utilities.getMathScriptBase(obj) 2774 if not base: 2775 return [] 2776 2777 return self._generateMath(base) 2778 2779 def _generateScriptScript(self, obj, **args): 2780 if self._script.utilities.isMathLayoutOnly(obj): 2781 return self._generateMath(obj) 2782 2783 oldRole = self._getAlternativeRole(obj) 2784 self._overrideRole(oldRole, args) 2785 result = self.generate(obj, role=oldRole) 2786 self._restoreRole(oldRole, args) 2787 2788 return result 2789 2790 def _generateScriptSubscript(self, obj, **args): 2791 subscript = self._script.utilities.getMathScriptSubscript(obj) 2792 if not subscript: 2793 return [] 2794 2795 result = [messages.MATH_SUBSCRIPT] 2796 result.extend(self.voice(SYSTEM)) 2797 result.extend(self._generateScriptScript(subscript)) 2798 2799 return result 2800 2801 def _generateScriptSuperscript(self, obj, **args): 2802 superscript = self._script.utilities.getMathScriptSuperscript(obj) 2803 if not superscript: 2804 return [] 2805 2806 result = [messages.MATH_SUPERSCRIPT] 2807 result.extend(self.voice(SYSTEM)) 2808 result.extend(self._generateScriptScript(superscript)) 2809 2810 return result 2811 2812 def _generateScriptUnderscript(self, obj, **args): 2813 underscript = self._script.utilities.getMathScriptUnderscript(obj) 2814 if not underscript: 2815 return [] 2816 2817 result = [messages.MATH_UNDERSCRIPT] 2818 result.extend(self.voice(SYSTEM)) 2819 result.extend(self._generateScriptScript(underscript)) 2820 2821 return result 2822 2823 def _generateScriptOverscript(self, obj, **args): 2824 overscript = self._script.utilities.getMathScriptOverscript(obj) 2825 if not overscript: 2826 return [] 2827 2828 result = [messages.MATH_OVERSCRIPT] 2829 result.extend(self.voice(SYSTEM)) 2830 result.extend(self._generateScriptScript(overscript)) 2831 2832 return result 2833 2834 def _generateScriptPrescripts(self, obj, **args): 2835 result = [] 2836 prescripts = self._script.utilities.getMathPrescripts(obj) 2837 for i, script in enumerate(prescripts): 2838 if self._script.utilities.isNoneElement(script): 2839 continue 2840 if i % 2: 2841 rv = [messages.MATH_PRE_SUPERSCRIPT] 2842 else: 2843 rv = [messages.MATH_PRE_SUBSCRIPT] 2844 rv.extend(self.voice(SYSTEM)) 2845 rv.extend(self._generateScriptScript(script)) 2846 result.append(rv) 2847 2848 return result 2849 2850 def _generateScriptPostscripts(self, obj, **args): 2851 result = [] 2852 postscripts = self._script.utilities.getMathPostscripts(obj) 2853 for i, script in enumerate(postscripts): 2854 if self._script.utilities.isNoneElement(script): 2855 continue 2856 if i % 2: 2857 rv = [messages.MATH_SUPERSCRIPT] 2858 else: 2859 rv = [messages.MATH_SUBSCRIPT] 2860 rv.extend(self.voice(SYSTEM)) 2861 rv.extend(self._generateScriptScript(script)) 2862 result.append(rv) 2863 2864 return result 2865 2866 def _generateMathTableStart(self, obj, **args): 2867 try: 2868 table = obj.queryTable() 2869 except: 2870 return [] 2871 2872 nestingLevel = self._script.utilities.getMathNestingLevel(obj) 2873 if nestingLevel > 0: 2874 result = [messages.mathNestedTableSize(table.nRows, table.nColumns)] 2875 else: 2876 result = [messages.mathTableSize(table.nRows, table.nColumns)] 2877 result.extend(self.voice(SYSTEM)) 2878 return result 2879 2880 def _generateMathTableRows(self, obj, **args): 2881 result = [] 2882 for row in obj: 2883 oldRole = self._getAlternativeRole(row) 2884 self._overrideRole(oldRole, args) 2885 result.extend(self.generate(row, role=oldRole)) 2886 self._restoreRole(oldRole, args) 2887 2888 return result 2889 2890 def _generateMathRow(self, obj, **args): 2891 result = [] 2892 2893 result.append(messages.TABLE_ROW % (obj.getIndexInParent() + 1)) 2894 result.extend(self.voice(SYSTEM)) 2895 result.extend(self._generatePause(obj, **args)) 2896 2897 for child in obj: 2898 result.extend(self._generateMath(child)) 2899 result.extend(self._generatePause(child, **args)) 2900 2901 return result 2902 2903 def _generateMathTableEnd(self, obj, **args): 2904 nestingLevel = self._script.utilities.getMathNestingLevel(obj) 2905 if nestingLevel > 0: 2906 result = [messages.MATH_NESTED_TABLE_END] 2907 else: 2908 result = [messages.MATH_TABLE_END] 2909 result.extend(self.voice(SYSTEM)) 2910 return result 2911 2912 ##################################################################### 2913 # # 2914 # Other things for prosody and voice selection # 2915 # # 2916 ##################################################################### 2917 2918 def _generatePause(self, obj, **args): 2919 if not _settingsManager.getSetting('enablePauseBreaks') \ 2920 or args.get('eliminatePauses', False): 2921 return [] 2922 2923 if _settingsManager.getSetting('verbalizePunctuationStyle') == \ 2924 settings.PUNCTUATION_STYLE_ALL: 2925 return [] 2926 2927 return PAUSE 2928 2929 def _generateLineBreak(self, obj, **args): 2930 return LINE_BREAK 2931 2932 def voice(self, key=None, **args): 2933 """Returns an array containing a voice. The key is a value 2934 to be used to look up the voice in the settings.py:voices 2935 dictionary. Other arguments can be passed in for future 2936 decision making. 2937 """ 2938 2939 voicename = voiceType.get(key) or voiceType.get(DEFAULT) 2940 voices = _settingsManager.getSetting('voices') 2941 voice = acss.ACSS(voices.get(voiceType.get(DEFAULT))) 2942 2943 if key in [None, DEFAULT]: 2944 string = args.get('string', '') 2945 if isinstance(string, str) and string.isupper(): 2946 voice.update(voices.get(voiceType.get(UPPERCASE))) 2947 else: 2948 override = voices.get(voicename) 2949 if override and override.get('established', True): 2950 voice.update(override) 2951 2952 return [voice] 2953 2954 def utterancesToString(self, utterances): 2955 string = "" 2956 for u in utterances: 2957 if isinstance(u, str): 2958 string += " %s" % u 2959 elif isinstance(u, Pause) and string and string[-1].isalnum(): 2960 string += "." 2961 2962 return string.strip() 2963