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"""Custom script for StarOffice and OpenOffice."""
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
29
30import orca.messages as messages
31import orca.settings_manager as settings_manager
32import orca.speech_generator as speech_generator
33
34_settingsManager = settings_manager.getManager()
35
36class SpeechGenerator(speech_generator.SpeechGenerator):
37    def __init__(self, script):
38        speech_generator.SpeechGenerator.__init__(self, script)
39
40    def __overrideParagraph(self, obj, **args):
41        # Treat a paragraph which is serving as a text entry in a dialog
42        # as a text object.
43        #
44        role = args.get('role', obj.getRole())
45        override = \
46            role == "text frame" \
47            or (role == pyatspi.ROLE_PARAGRAPH \
48                and self._script.utilities.ancestorWithRole(
49                      obj, [pyatspi.ROLE_DIALOG], [pyatspi.ROLE_APPLICATION]))
50        return override
51
52    def _generateRoleName(self, obj, **args):
53        result = []
54        role = args.get('role', obj.getRole())
55        if role == pyatspi.ROLE_TOGGLE_BUTTON \
56           and obj.parent.getRole() == pyatspi.ROLE_TOOL_BAR:
57            pass
58        else:
59            # Treat a paragraph which is serving as a text entry in a dialog
60            # as a text object.
61            #
62            override = self.__overrideParagraph(obj, **args)
63            if override:
64                oldRole = self._overrideRole(pyatspi.ROLE_TEXT, args)
65            # Treat a paragraph which is inside of a spreadsheet cell as
66            # a spreadsheet cell.
67            #
68            elif role == 'ROLE_SPREADSHEET_CELL':
69                oldRole = self._overrideRole(pyatspi.ROLE_TABLE_CELL, args)
70                override = True
71            result.extend(speech_generator.SpeechGenerator._generateRoleName(
72                          self, obj, **args))
73            if override:
74                self._restoreRole(oldRole, args)
75        return result
76
77    def _generateTextRole(self, obj, **args):
78        result = []
79        role = args.get('role', obj.getRole())
80        if role == pyatspi.ROLE_TEXT and obj.parent.getRole() == pyatspi.ROLE_COMBO_BOX:
81            return []
82
83        if role != pyatspi.ROLE_PARAGRAPH \
84           or self.__overrideParagraph(obj, **args):
85            result.extend(self._generateRoleName(obj, **args))
86        return result
87
88    def _generateLabel(self, obj, **args):
89        """Returns the label for an object as an array of strings (and
90        possibly voice and audio specifications).  The label is
91        determined by the displayedLabel method of the script utility,
92        and an empty array will be returned if no label can be found.
93        """
94        result = []
95        acss = self.voice(speech_generator.DEFAULT)
96        override = self.__overrideParagraph(obj, **args)
97        label = self._script.utilities.displayedLabel(obj) or ""
98        if not label and override:
99            label = self._script.utilities.displayedLabel(obj.parent) or ""
100        if label:
101            result.append(label.strip())
102            result.extend(acss)
103        return result
104
105    def _generateName(self, obj, **args):
106        """Returns an array of strings for use by speech and braille that
107        represent the name of the object.  If the object is directly
108        displaying any text, that text will be treated as the name.
109        Otherwise, the accessible name of the object will be used.  If
110        there is no accessible name, then the description of the
111        object will be used.  This method will return an empty array
112        if nothing can be found.
113        """
114
115        # TODO - JD: This should be the behavior by default. But the default
116        # generators call displayedText(). Once that is corrected, this method
117        # can be removed.
118        if obj.name:
119            result = [obj.name]
120            result.extend(self.voice(speech_generator.DEFAULT))
121            return result
122
123        return super()._generateName(obj, **args)
124
125    def _generateLabelAndName(self, obj, **args):
126        if obj.getRole() != pyatspi.ROLE_COMBO_BOX:
127            return super()._generateLabelAndName(obj, **args)
128
129        # TODO - JD: This should be the behavior by default because many
130        # toolkits use the label for the name.
131        result = []
132        label = self._script.utilities.displayedLabel(obj) or obj.name
133        if label:
134            result.append(label)
135            result.extend(self.voice(speech_generator.DEFAULT))
136
137        name = obj.name
138        if label == name or not name:
139            selected = self._script.utilities.selectedChildren(obj)
140            if selected:
141                name = selected[0].name
142
143        if name:
144            result.append(name)
145            result.extend(self.voice(speech_generator.DEFAULT))
146
147        return result
148
149    def _generateLabelOrName(self, obj, **args):
150        """Gets the label or the name if the label is not preset."""
151
152        result = []
153        acss = self.voice(speech_generator.DEFAULT)
154        override = self.__overrideParagraph(obj, **args)
155        # Treat a paragraph which is serving as a text entry in a dialog
156        # as a text object.
157        #
158        if override:
159            result.extend(self._generateLabel(obj, **args))
160            if len(result) == 0 and obj.parent:
161                parentLabel = self._generateLabel(obj.parent, **args)
162                # If we aren't already focused, we will have spoken the
163                # parent as part of the speech context and do not want
164                # to repeat it.
165                #
166                alreadyFocused = args.get('alreadyFocused', False)
167                if alreadyFocused:
168                    result.extend(parentLabel)
169                # If we still don't have a label, look to the name.
170                #
171                if not parentLabel and obj.name and len(obj.name):
172                    result.append(obj.name)
173                if result:
174                    result.extend(acss)
175        else:
176            result.extend(speech_generator.SpeechGenerator._generateLabelOrName(
177                self, obj, **args))
178        return result
179
180    def _generateAnyTextSelection(self, obj, **args):
181        comboBoxEntry = self._script.utilities.getEntryForEditableComboBox(obj)
182        if comboBoxEntry:
183            return super()._generateAnyTextSelection(comboBoxEntry)
184
185        return super()._generateAnyTextSelection(obj, **args)
186
187    def _generateAvailability(self, obj, **args):
188        """Returns an array of strings for use by speech and braille that
189        represent the grayed/sensitivity/availability state of the
190        object, but only if it is insensitive (i.e., grayed out and
191        inactive).  Otherwise, and empty array will be returned.
192        """
193
194        result = []
195        if not self._script.utilities.isSpreadSheetCell(obj):
196            result.extend(speech_generator.SpeechGenerator.\
197                _generateAvailability(self, obj, **args))
198
199        return result
200
201    def _generateDescription(self, obj, **args):
202        """Returns an array of strings (and possibly voice and audio
203        specifications) that represent the description of the object,
204        if that description is different from that of the name and
205        label.
206        """
207        if _settingsManager.getSetting('onlySpeakDisplayedText'):
208            return []
209
210        if not _settingsManager.getSetting('speakDescription'):
211            return []
212
213        if not args.get('formatType', '').endswith('WhereAmI'):
214            return []
215
216        result = []
217        acss = self.voice(speech_generator.SYSTEM)
218        if obj.description:
219            # The description of some OOo paragraphs consists of the name
220            # and the displayed text, with punctuation added. Try to spot
221            # this and, if found, ignore the description.
222            #
223            text = self._script.utilities.displayedText(obj) or ""
224            desc = obj.description.replace(text, "")
225            for item in obj.name.split():
226                desc = desc.replace(item, "")
227            for char in desc.strip():
228                if char.isalnum():
229                    result.append(obj.description)
230                    break
231
232        if result:
233            result.extend(acss)
234        return result
235
236    def _generateCurrentLineText(self, obj, **args):
237        if self._script.utilities.isTextDocumentCell(obj.parent):
238            priorObj = args.get('priorObj', None)
239            if priorObj and priorObj.parent != obj.parent:
240                return []
241
242        if obj.getRole() == pyatspi.ROLE_COMBO_BOX:
243            entry = self._script.utilities.getEntryForEditableComboBox(obj)
244            if entry:
245                return super()._generateCurrentLineText(entry)
246            return []
247
248        # TODO - JD: The SayLine, etc. code should be generated and not put
249        # together in the scripts. In addition, the voice crap needs to go
250        # here. Then it needs to be removed from the scripts.
251        [text, caretOffset, startOffset] = self._script.getTextLineAtCaret(obj)
252        voice = self.voice(string=text)
253        text = self._script.utilities.adjustForLinks(obj, text, startOffset)
254        text = self._script.utilities.adjustForRepeats(text)
255        if not text:
256            result = [messages.BLANK]
257        else:
258            result = [text]
259        result.extend(voice)
260
261        return result
262
263    def _generateToggleState(self, obj, **args):
264        """Treat toggle buttons in the toolbar specially. This is so we can
265        have more natural sounding speech such as "bold on", "bold off", etc."""
266        acss = self.voice(speech_generator.SYSTEM)
267        result = []
268        role = args.get('role', obj.getRole())
269        if role == pyatspi.ROLE_TOGGLE_BUTTON \
270           and obj.parent.getRole() == pyatspi.ROLE_TOOL_BAR:
271            if obj.getState().contains(pyatspi.STATE_CHECKED):
272                result.append(messages.ON)
273            else:
274                result.append(messages.OFF)
275            result.extend(acss)
276        elif role == pyatspi.ROLE_TOGGLE_BUTTON:
277            result.extend(speech_generator.SpeechGenerator._generateToggleState(
278                self, obj, **args))
279        return result
280
281    def _generateRowHeader(self, obj, **args):
282        """Returns an array of strings (and possibly voice and audio
283        specifications) that represent the row header for an object
284        that is in a table, if it exists.  Otherwise, an empty array
285        is returned. Overridden here so that we can get the dynamic
286        row header(s).
287        """
288
289        if self._script.utilities.shouldReadFullRow(obj):
290            return []
291
292        newOnly = args.get('newOnly', False)
293        rowHeader, columnHeader = \
294            self._script.utilities.getDynamicHeadersForCell(obj, newOnly)
295        if not rowHeader:
296            return super()._generateRowHeader(obj, **args)
297
298        result = []
299        text = self._script.utilities.displayedText(rowHeader)
300        if text:
301            result.append(text)
302            result.extend(self.voice(speech_generator.DEFAULT))
303
304        return result
305
306    def _generateColumnHeader(self, obj, **args):
307        """Returns an array of strings (and possibly voice and audio
308        specifications) that represent the column header for an object
309        that is in a table, if it exists.  Otherwise, an empty array
310        is returned. Overridden here so that we can get the dynamic
311        column header(s).
312        """
313
314        newOnly = args.get('newOnly', False)
315        rowHeader, columnHeader = \
316            self._script.utilities.getDynamicHeadersForCell(obj, newOnly)
317        if not columnHeader:
318            return super()._generateColumnHeader(obj, **args)
319
320        result = []
321        text = self._script.utilities.displayedText(columnHeader)
322        if text:
323            result.append(text)
324            result.extend(self.voice(speech_generator.DEFAULT))
325
326        return result
327
328    def _generateTooLong(self, obj, **args):
329        """If there is text in this spread sheet cell, compare the size of
330        the text within the table cell with the size of the actual table
331        cell and report back to the user if it is larger.
332
333        Returns an indication of how many characters are greater than the size
334        of the spread sheet cell, or None if the message fits.
335        """
336        if _settingsManager.getSetting('onlySpeakDisplayedText'):
337            return []
338
339        result = []
340        acss = self.voice(speech_generator.SYSTEM)
341        try:
342            text = obj.queryText()
343            objectText = \
344                self._script.utilities.substring(obj, 0, -1)
345            extents = obj.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
346        except NotImplementedError:
347            pass
348        else:
349            tooLongCount = 0
350            for i in range(0, len(objectText)):
351                [x, y, width, height] = text.getRangeExtents(i, i + 1, 0)
352                if x < extents.x:
353                    tooLongCount += 1
354                elif (x + width) > extents.x + extents.width:
355                    tooLongCount += len(objectText) - i
356                    break
357            if tooLongCount > 0:
358                result = [messages.charactersTooLong(tooLongCount)]
359        if result:
360            result.extend(acss)
361        return result
362
363    def _generateHasFormula(self, obj, **args):
364        inputLine = self._script.utilities.locateInputLine(obj)
365        if not inputLine:
366            return []
367
368        text = self._script.utilities.displayedText(inputLine)
369        if text and text.startswith("="):
370            result = [messages.HAS_FORMULA]
371            result.extend(self.voice(speech_generator.SYSTEM))
372            return result
373
374        return []
375
376    def _generateRealTableCell(self, obj, **args):
377        """Get the speech for a table cell. If this isn't inside a
378        spread sheet, just return the utterances returned by the default
379        table cell speech handler.
380
381        Arguments:
382        - obj: the table cell
383
384        Returns a list of utterances to be spoken for the object.
385        """
386
387        if self._script.inSayAll():
388            return []
389
390        result = super()._generateRealTableCell(obj, **args)
391
392        if not self._script.utilities.isSpreadSheetCell(obj):
393            if self._script._lastCommandWasStructNav:
394                return result
395
396            if _settingsManager.getSetting('speakCellCoordinates'):
397                result.append(obj.name)
398            return result
399
400        isBasicWhereAmI = args.get('formatType') == 'basicWhereAmI'
401        speakCoordinates = _settingsManager.getSetting('speakSpreadsheetCoordinates')
402        if speakCoordinates and not isBasicWhereAmI:
403            result.append(self._script.utilities.spreadSheetCellName(obj))
404
405        if self._script.utilities.shouldReadFullRow(obj):
406            row, col, table = self._script.utilities.getRowColumnAndTable(obj)
407            lastRow = self._script.pointOfReference.get("lastRow")
408            if row != lastRow:
409                return result
410
411        tooLong = self._generateTooLong(obj, **args)
412        if tooLong:
413            result.extend(self._generatePause(obj, **args))
414            result.extend(tooLong)
415
416        hasFormula = self._generateHasFormula(obj, **args)
417        if hasFormula:
418            result.extend(self._generatePause(obj, **args))
419            result.extend(hasFormula)
420
421        return result
422
423    def _generateTableCellRow(self, obj, **args):
424        if not self._script.utilities.shouldReadFullRow(obj):
425            return self._generateRealTableCell(obj, **args)
426
427        if not self._script.utilities.isSpreadSheetCell(obj):
428            return super()._generateTableCellRow(obj, **args)
429
430        cells = self._script.utilities.getShowingCellsInSameRow(obj)
431        if not cells:
432            return []
433
434        result = []
435        for cell in cells:
436            result.extend(self._generateRealTableCell(cell, **args))
437
438        return result
439
440    def _generateEndOfTableIndicator(self, obj, **args):
441        """Returns an array of strings (and possibly voice and audio
442        specifications) indicating that this cell is the last cell
443        in the table. Overridden here because Orca keeps saying "end
444        of table" in certain lists (e.g. the Templates and Documents
445        dialog).
446        """
447
448        if self._script._lastCommandWasStructNav or self._script.inSayAll():
449            return []
450
451        topLevel = self._script.utilities.topLevelObject(obj)
452        if topLevel and topLevel.getRole() == pyatspi.ROLE_DIALOG:
453            return []
454
455        return super()._generateEndOfTableIndicator(obj, **args)
456
457    def _generateNewAncestors(self, obj, **args):
458        priorObj = args.get('priorObj', None)
459        if not priorObj or priorObj.getRoleName() == 'text frame':
460            return []
461
462        if self._script.utilities.isSpreadSheetCell(obj) \
463           and self._script.utilities.isDocumentPanel(priorObj.parent):
464            return []
465
466        return super()._generateNewAncestors(obj, **args)
467
468    def _generateOldAncestors(self, obj, **args):
469        """Returns an array of strings (and possibly voice and audio
470        specifications) that represent the text of the ancestors for
471        the object being left."""
472
473        if obj.getRoleName() == 'text frame':
474            return []
475
476        priorObj = args.get('priorObj', None)
477        if self._script.utilities.isSpreadSheetCell(priorObj):
478            return []
479
480        return super()._generateOldAncestors(obj, **args)
481
482    def _generateUnselectedCell(self, obj, **args):
483        if self._script.utilities.isSpreadSheetCell(obj):
484            return []
485
486        if self._script._lastCommandWasStructNav:
487            return []
488
489        return super()._generateUnselectedCell(obj, **args)
490
491    def generateSpeech(self, obj, **args):
492        result = []
493        if args.get('formatType', 'unfocused') == 'basicWhereAmI' \
494           and self._script.utilities.isSpreadSheetCell(obj):
495            oldRole = self._overrideRole('ROLE_SPREADSHEET_CELL', args)
496            # In addition, if focus is in a cell being edited, we cannot
497            # query the accessible table interface for coordinates and the
498            # like because we're temporarily in an entirely different object
499            # which is outside of the table. This makes things difficult.
500            # However, odds are that if we're doing a whereAmI in a cell
501            # which we are editing, we have some pointOfReference info
502            # we can use to guess the coordinates.
503            #
504            args['guessCoordinates'] = obj.getRole() == pyatspi.ROLE_PARAGRAPH
505            result.extend(super().generateSpeech(obj, **args))
506            del args['guessCoordinates']
507            self._restoreRole(oldRole, args)
508        else:
509            oldRole = self._overrideRole(self._getAlternativeRole(obj, **args), args)
510            result.extend(super().generateSpeech(obj, **args))
511            self._restoreRole(oldRole, args)
512
513        return result
514