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