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