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