1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5/* global Components, XPCOMUtils, Utils, PrefCache, States, Roles, Logger */ 6/* exported UtteranceGenerator, BrailleGenerator */ 7 8'use strict'; 9 10const {utils: Cu, interfaces: Ci} = Components; 11 12const INCLUDE_DESC = 0x01; 13const INCLUDE_NAME = 0x02; 14const INCLUDE_VALUE = 0x04; 15const NAME_FROM_SUBTREE_RULE = 0x10; 16const IGNORE_EXPLICIT_NAME = 0x20; 17 18const OUTPUT_DESC_FIRST = 0; 19const OUTPUT_DESC_LAST = 1; 20 21Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 22XPCOMUtils.defineLazyModuleGetter(this, 'Utils', // jshint ignore:line 23 'resource://gre/modules/accessibility/Utils.jsm'); 24XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache', // jshint ignore:line 25 'resource://gre/modules/accessibility/Utils.jsm'); 26XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line 27 'resource://gre/modules/accessibility/Utils.jsm'); 28XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line 29 'resource://gre/modules/accessibility/Constants.jsm'); 30XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line 31 'resource://gre/modules/accessibility/Constants.jsm'); 32 33this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator']; // jshint ignore:line 34 35var OutputGenerator = { 36 37 defaultOutputOrder: OUTPUT_DESC_LAST, 38 39 /** 40 * Generates output for a PivotContext. 41 * @param {PivotContext} aContext object that generates and caches 42 * context information for a given accessible and its relationship with 43 * another accessible. 44 * @return {Object} An array of speech data. Depending on the utterance order, 45 * the data describes the context for an accessible object either 46 * starting from the accessible's ancestry or accessible's subtree. 47 */ 48 genForContext: function genForContext(aContext) { 49 let output = []; 50 let self = this; 51 let addOutput = function addOutput(aAccessible) { 52 output.push.apply(output, self.genForObject(aAccessible, aContext)); 53 }; 54 let ignoreSubtree = function ignoreSubtree(aAccessible) { 55 let roleString = Utils.AccService.getStringRole(aAccessible.role); 56 let nameRule = self.roleRuleMap[roleString] || 0; 57 // Ignore subtree if the name is explicit and the role's name rule is the 58 // NAME_FROM_SUBTREE_RULE. 59 return (((nameRule & INCLUDE_VALUE) && aAccessible.value) || 60 ((nameRule & NAME_FROM_SUBTREE_RULE) && 61 (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' && 62 !(nameRule & IGNORE_EXPLICIT_NAME)))); 63 }; 64 65 let contextStart = this._getContextStart(aContext); 66 67 if (this.outputOrder === OUTPUT_DESC_FIRST) { 68 contextStart.forEach(addOutput); 69 addOutput(aContext.accessible); 70 for (let node of aContext.subtreeGenerator(true, ignoreSubtree)) { 71 addOutput(node); 72 } 73 } else { 74 for (let node of aContext.subtreeGenerator(false, ignoreSubtree)) { 75 addOutput(node); 76 } 77 addOutput(aContext.accessible); 78 79 // If there are any documents in new ancestry, find a first one and place 80 // it in the beginning of the utterance. 81 let doc, docIndex = contextStart.findIndex( 82 ancestor => ancestor.role === Roles.DOCUMENT); 83 84 if (docIndex > -1) { 85 doc = contextStart.splice(docIndex, 1)[0]; 86 } 87 88 contextStart.reverse().forEach(addOutput); 89 if (doc) { 90 output.unshift.apply(output, self.genForObject(doc, aContext)); 91 } 92 } 93 94 return output; 95 }, 96 97 98 /** 99 * Generates output for an object. 100 * @param {nsIAccessible} aAccessible accessible object to generate output 101 * for. 102 * @param {PivotContext} aContext object that generates and caches 103 * context information for a given accessible and its relationship with 104 * another accessible. 105 * @return {Array} A 2 element array of speech data. The first element 106 * describes the object and its state. The second element is the object's 107 * name. Whether the object's description or it's role is included is 108 * determined by {@link roleRuleMap}. 109 */ 110 genForObject: function genForObject(aAccessible, aContext) { 111 let roleString = Utils.AccService.getStringRole(aAccessible.role); 112 let func = this.objectOutputFunctions[ 113 OutputGenerator._getOutputName(roleString)] || 114 this.objectOutputFunctions.defaultFunc; 115 116 let flags = this.roleRuleMap[roleString] || 0; 117 118 if (aAccessible.childCount === 0) { 119 flags |= INCLUDE_NAME; 120 } 121 122 return func.apply(this, [aAccessible, roleString, 123 Utils.getState(aAccessible), flags, aContext]); 124 }, 125 126 /** 127 * Generates output for an action performed. 128 * @param {nsIAccessible} aAccessible accessible object that the action was 129 * invoked in. 130 * @param {string} aActionName the name of the action, one of the keys in 131 * {@link gActionMap}. 132 * @return {Array} A one element array with action data. 133 */ 134 genForAction: function genForAction(aObject, aActionName) {}, // jshint ignore:line 135 136 /** 137 * Generates output for an announcement. 138 * @param {string} aAnnouncement unlocalized announcement. 139 * @return {Array} An announcement speech data to be localized. 140 */ 141 genForAnnouncement: function genForAnnouncement(aAnnouncement) {}, // jshint ignore:line 142 143 /** 144 * Generates output for a tab state change. 145 * @param {nsIAccessible} aAccessible accessible object of the tab's attached 146 * document. 147 * @param {string} aTabState the tab state name, see 148 * {@link Presenter.tabStateChanged}. 149 * @return {Array} The tab state utterace. 150 */ 151 genForTabStateChange: function genForTabStateChange(aObject, aTabState) {}, // jshint ignore:line 152 153 /** 154 * Generates output for announcing entering and leaving editing mode. 155 * @param {aIsEditing} boolean true if we are in editing mode 156 * @return {Array} The mode utterance 157 */ 158 genForEditingMode: function genForEditingMode(aIsEditing) {}, // jshint ignore:line 159 160 _getContextStart: function getContextStart(aContext) {}, // jshint ignore:line 161 162 /** 163 * Adds an accessible name and description to the output if available. 164 * @param {Array} aOutput Output array. 165 * @param {nsIAccessible} aAccessible current accessible object. 166 * @param {Number} aFlags output flags. 167 */ 168 _addName: function _addName(aOutput, aAccessible, aFlags) { 169 let name; 170 if ((Utils.getAttributes(aAccessible)['explicit-name'] === 'true' && 171 !(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) { 172 name = aAccessible.name; 173 } 174 175 let description = aAccessible.description; 176 if (description) { 177 // Compare against the calculated name unconditionally, regardless of name rule, 178 // so we can make sure we don't speak duplicated descriptions 179 let tmpName = name || aAccessible.name; 180 if (tmpName && (description !== tmpName)) { 181 name = name || ''; 182 name = this.outputOrder === OUTPUT_DESC_FIRST ? 183 description + ' - ' + name : 184 name + ' - ' + description; 185 } 186 } 187 188 if (!name || !name.trim()) { 189 return; 190 } 191 aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'](name); 192 }, 193 194 /** 195 * Adds a landmark role to the output if available. 196 * @param {Array} aOutput Output array. 197 * @param {nsIAccessible} aAccessible current accessible object. 198 */ 199 _addLandmark: function _addLandmark(aOutput, aAccessible) { 200 let landmarkName = Utils.getLandmarkName(aAccessible); 201 if (!landmarkName) { 202 return; 203 } 204 aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push']({ 205 string: landmarkName 206 }); 207 }, 208 209 /** 210 * Adds math roles to the output, for a MathML accessible. 211 * @param {Array} aOutput Output array. 212 * @param {nsIAccessible} aAccessible current accessible object. 213 * @param {String} aRoleStr aAccessible's role string. 214 */ 215 _addMathRoles: function _addMathRoles(aOutput, aAccessible, aRoleStr) { 216 // First, determine the actual role to use (e.g. mathmlfraction). 217 let roleStr = aRoleStr; 218 switch(aAccessible.role) { 219 case Roles.MATHML_CELL: 220 case Roles.MATHML_ENCLOSED: 221 case Roles.MATHML_LABELED_ROW: 222 case Roles.MATHML_ROOT: 223 case Roles.MATHML_SQUARE_ROOT: 224 case Roles.MATHML_TABLE: 225 case Roles.MATHML_TABLE_ROW: 226 // Use the default role string. 227 break; 228 case Roles.MATHML_MULTISCRIPTS: 229 case Roles.MATHML_OVER: 230 case Roles.MATHML_SUB: 231 case Roles.MATHML_SUB_SUP: 232 case Roles.MATHML_SUP: 233 case Roles.MATHML_UNDER: 234 case Roles.MATHML_UNDER_OVER: 235 // For scripted accessibles, use the string 'mathmlscripted'. 236 roleStr = 'mathmlscripted'; 237 break; 238 case Roles.MATHML_FRACTION: 239 // From a semantic point of view, the only important point is to 240 // distinguish between fractions that have a bar and those that do not. 241 // Per the MathML 3 spec, the latter happens iff the linethickness 242 // attribute is of the form [zero-float][optional-unit]. In that case, 243 // we use the string 'mathmlfractionwithoutbar'. 244 let linethickness = Utils.getAttributes(aAccessible).linethickness; 245 if (linethickness) { 246 let numberMatch = linethickness.match(/^(?:\d|\.)+/); 247 if (numberMatch && !parseFloat(numberMatch[0])) { 248 roleStr += 'withoutbar'; 249 } 250 } 251 break; 252 default: 253 // Otherwise, do not output the actual role. 254 roleStr = null; 255 break; 256 } 257 258 // Get the math role based on the position in the parent accessible 259 // (e.g. numerator for the first child of a mathmlfraction). 260 let mathRole = Utils.getMathRole(aAccessible); 261 if (mathRole) { 262 aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'] 263 ({string: this._getOutputName(mathRole)}); 264 } 265 if (roleStr) { 266 aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'] 267 ({string: this._getOutputName(roleStr)}); 268 } 269 }, 270 271 /** 272 * Adds MathML menclose notations to the output. 273 * @param {Array} aOutput Output array. 274 * @param {nsIAccessible} aAccessible current accessible object. 275 */ 276 _addMencloseNotations: function _addMencloseNotations(aOutput, aAccessible) { 277 let notations = Utils.getAttributes(aAccessible).notation || 'longdiv'; 278 aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'].apply( 279 aOutput, notations.split(' ').map(notation => { 280 return { string: this._getOutputName('notation-' + notation) }; 281 })); 282 }, 283 284 /** 285 * Adds an entry type attribute to the description if available. 286 * @param {Array} aOutput Output array. 287 * @param {nsIAccessible} aAccessible current accessible object. 288 * @param {String} aRoleStr aAccessible's role string. 289 */ 290 _addType: function _addType(aOutput, aAccessible, aRoleStr) { 291 if (aRoleStr !== 'entry') { 292 return; 293 } 294 295 let typeName = Utils.getAttributes(aAccessible)['text-input-type']; 296 // Ignore the the input type="text" case. 297 if (!typeName || typeName === 'text') { 298 return; 299 } 300 aOutput.push({string: 'textInputType_' + typeName}); 301 }, 302 303 _addState: function _addState(aOutput, aState, aRoleStr) {}, // jshint ignore:line 304 305 _addRole: function _addRole(aOutput, aAccessible, aRoleStr) {}, // jshint ignore:line 306 307 get outputOrder() { 308 if (!this._utteranceOrder) { 309 this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance'); 310 } 311 return typeof this._utteranceOrder.value === 'number' ? 312 this._utteranceOrder.value : this.defaultOutputOrder; 313 }, 314 315 _getOutputName: function _getOutputName(aName) { 316 return aName.replace(/\s/g, ''); 317 }, 318 319 roleRuleMap: { 320 'menubar': INCLUDE_DESC, 321 'scrollbar': INCLUDE_DESC, 322 'grip': INCLUDE_DESC, 323 'alert': INCLUDE_DESC | INCLUDE_NAME, 324 'menupopup': INCLUDE_DESC, 325 'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 326 'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 327 'columnheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 328 'rowheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 329 'column': NAME_FROM_SUBTREE_RULE, 330 'row': NAME_FROM_SUBTREE_RULE, 331 'cell': INCLUDE_DESC | INCLUDE_NAME, 332 'application': INCLUDE_NAME, 333 'document': INCLUDE_NAME, 334 'grouping': INCLUDE_DESC | INCLUDE_NAME, 335 'toolbar': INCLUDE_DESC, 336 'table': INCLUDE_DESC | INCLUDE_NAME, 337 'link': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 338 'helpballoon': NAME_FROM_SUBTREE_RULE, 339 'list': INCLUDE_DESC | INCLUDE_NAME, 340 'listitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 341 'outline': INCLUDE_DESC, 342 'outlineitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 343 'pagetab': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 344 'graphic': INCLUDE_DESC, 345 'switch': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 346 'pushbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 347 'checkbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 348 'radiobutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 349 'buttondropdown': NAME_FROM_SUBTREE_RULE, 350 'combobox': INCLUDE_DESC | INCLUDE_VALUE, 351 'droplist': INCLUDE_DESC, 352 'progressbar': INCLUDE_DESC | INCLUDE_VALUE, 353 'slider': INCLUDE_DESC | INCLUDE_VALUE, 354 'spinbutton': INCLUDE_DESC | INCLUDE_VALUE, 355 'diagram': INCLUDE_DESC, 356 'animation': INCLUDE_DESC, 357 'equation': INCLUDE_DESC, 358 'buttonmenu': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 359 'buttondropdowngrid': NAME_FROM_SUBTREE_RULE, 360 'pagetablist': INCLUDE_DESC, 361 'canvas': INCLUDE_DESC, 362 'check menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 363 'label': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 364 'password text': INCLUDE_DESC, 365 'popup menu': INCLUDE_DESC, 366 'radio menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 367 'table column header': NAME_FROM_SUBTREE_RULE, 368 'table row header': NAME_FROM_SUBTREE_RULE, 369 'tear off menu item': NAME_FROM_SUBTREE_RULE, 370 'toggle button': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 371 'parent menuitem': NAME_FROM_SUBTREE_RULE, 372 'header': INCLUDE_DESC, 373 'footer': INCLUDE_DESC, 374 'entry': INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE, 375 'caption': INCLUDE_DESC, 376 'document frame': INCLUDE_DESC, 377 'heading': INCLUDE_DESC, 378 'calendar': INCLUDE_DESC | INCLUDE_NAME, 379 'combobox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 380 'listbox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, 381 'listbox rich option': NAME_FROM_SUBTREE_RULE, 382 'gridcell': NAME_FROM_SUBTREE_RULE, 383 'check rich option': NAME_FROM_SUBTREE_RULE, 384 'term': NAME_FROM_SUBTREE_RULE, 385 'definition': NAME_FROM_SUBTREE_RULE, 386 'key': NAME_FROM_SUBTREE_RULE, 387 'image map': INCLUDE_DESC, 388 'option': INCLUDE_DESC, 389 'listbox': INCLUDE_DESC, 390 'definitionlist': INCLUDE_DESC | INCLUDE_NAME, 391 'dialog': INCLUDE_DESC | INCLUDE_NAME, 392 'chrome window': IGNORE_EXPLICIT_NAME, 393 'app root': IGNORE_EXPLICIT_NAME, 394 'statusbar': NAME_FROM_SUBTREE_RULE, 395 'mathml table': INCLUDE_DESC | INCLUDE_NAME, 396 'mathml labeled row': NAME_FROM_SUBTREE_RULE, 397 'mathml table row': NAME_FROM_SUBTREE_RULE, 398 'mathml cell': INCLUDE_DESC | INCLUDE_NAME, 399 'mathml fraction': INCLUDE_DESC, 400 'mathml square root': INCLUDE_DESC, 401 'mathml root': INCLUDE_DESC, 402 'mathml enclosed': INCLUDE_DESC, 403 'mathml sub': INCLUDE_DESC, 404 'mathml sup': INCLUDE_DESC, 405 'mathml sub sup': INCLUDE_DESC, 406 'mathml under': INCLUDE_DESC, 407 'mathml over': INCLUDE_DESC, 408 'mathml under over': INCLUDE_DESC, 409 'mathml multiscripts': INCLUDE_DESC, 410 'mathml identifier': INCLUDE_DESC, 411 'mathml number': INCLUDE_DESC, 412 'mathml operator': INCLUDE_DESC, 413 'mathml text': INCLUDE_DESC, 414 'mathml string literal': INCLUDE_DESC, 415 'mathml row': INCLUDE_DESC, 416 'mathml style': INCLUDE_DESC, 417 'mathml error': INCLUDE_DESC }, 418 419 mathmlRolesSet: new Set([ 420 Roles.MATHML_MATH, 421 Roles.MATHML_IDENTIFIER, 422 Roles.MATHML_NUMBER, 423 Roles.MATHML_OPERATOR, 424 Roles.MATHML_TEXT, 425 Roles.MATHML_STRING_LITERAL, 426 Roles.MATHML_GLYPH, 427 Roles.MATHML_ROW, 428 Roles.MATHML_FRACTION, 429 Roles.MATHML_SQUARE_ROOT, 430 Roles.MATHML_ROOT, 431 Roles.MATHML_FENCED, 432 Roles.MATHML_ENCLOSED, 433 Roles.MATHML_STYLE, 434 Roles.MATHML_SUB, 435 Roles.MATHML_SUP, 436 Roles.MATHML_SUB_SUP, 437 Roles.MATHML_UNDER, 438 Roles.MATHML_OVER, 439 Roles.MATHML_UNDER_OVER, 440 Roles.MATHML_MULTISCRIPTS, 441 Roles.MATHML_TABLE, 442 Roles.LABELED_ROW, 443 Roles.MATHML_TABLE_ROW, 444 Roles.MATHML_CELL, 445 Roles.MATHML_ACTION, 446 Roles.MATHML_ERROR, 447 Roles.MATHML_STACK, 448 Roles.MATHML_LONG_DIVISION, 449 Roles.MATHML_STACK_GROUP, 450 Roles.MATHML_STACK_ROW, 451 Roles.MATHML_STACK_CARRIES, 452 Roles.MATHML_STACK_CARRY, 453 Roles.MATHML_STACK_LINE 454 ]), 455 456 objectOutputFunctions: { 457 _generateBaseOutput: 458 function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) { 459 let output = []; 460 461 if (aFlags & INCLUDE_DESC) { 462 this._addState(output, aState, aRoleStr); 463 this._addType(output, aAccessible, aRoleStr); 464 this._addRole(output, aAccessible, aRoleStr); 465 } 466 467 if (aFlags & INCLUDE_VALUE && aAccessible.value.trim()) { 468 output[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift']( 469 aAccessible.value); 470 } 471 472 this._addName(output, aAccessible, aFlags); 473 this._addLandmark(output, aAccessible); 474 475 return output; 476 }, 477 478 label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) { 479 if (aContext.isNestedControl || 480 aContext.accessible == Utils.getEmbeddedControl(aAccessible)) { 481 // If we are on a nested control, or a nesting label, 482 // we don't need the context. 483 return []; 484 } 485 486 return this.objectOutputFunctions.defaultFunc.apply(this, arguments); 487 }, 488 489 entry: function entry(aAccessible, aRoleStr, aState, aFlags) { 490 let rolestr = aState.contains(States.MULTI_LINE) ? 'textarea' : 'entry'; 491 return this.objectOutputFunctions.defaultFunc.apply( 492 this, [aAccessible, rolestr, aState, aFlags]); 493 }, 494 495 pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) { 496 let itemno = {}; 497 let itemof = {}; 498 aAccessible.groupPosition({}, itemof, itemno); 499 let output = []; 500 this._addState(output, aState); 501 this._addRole(output, aAccessible, aRoleStr); 502 output.push({ 503 string: 'objItemOfN', 504 args: [itemno.value, itemof.value] 505 }); 506 507 this._addName(output, aAccessible, aFlags); 508 this._addLandmark(output, aAccessible); 509 510 return output; 511 }, 512 513 table: function table(aAccessible, aRoleStr, aState, aFlags) { 514 let output = []; 515 let table; 516 try { 517 table = aAccessible.QueryInterface(Ci.nsIAccessibleTable); 518 } catch (x) { 519 Logger.logException(x); 520 return output; 521 } finally { 522 // Check if it's a layout table, and bail out if true. 523 // We don't want to speak any table information for layout tables. 524 if (table.isProbablyForLayout()) { 525 return output; 526 } 527 this._addRole(output, aAccessible, aRoleStr); 528 output.push.call(output, { 529 string: this._getOutputName('tblColumnInfo'), 530 count: table.columnCount 531 }, { 532 string: this._getOutputName('tblRowInfo'), 533 count: table.rowCount 534 }); 535 this._addName(output, aAccessible, aFlags); 536 this._addLandmark(output, aAccessible); 537 return output; 538 } 539 }, 540 541 gridcell: function gridcell(aAccessible, aRoleStr, aState, aFlags) { 542 let output = []; 543 this._addState(output, aState); 544 this._addName(output, aAccessible, aFlags); 545 this._addLandmark(output, aAccessible); 546 return output; 547 }, 548 549 // Use the table output functions for MathML tabular elements. 550 mathmltable: function mathmltable() { 551 return this.objectOutputFunctions.table.apply(this, arguments); 552 }, 553 554 mathmlcell: function mathmlcell() { 555 return this.objectOutputFunctions.cell.apply(this, arguments); 556 }, 557 558 mathmlenclosed: function mathmlenclosed(aAccessible, aRoleStr, aState, 559 aFlags, aContext) { 560 let output = this.objectOutputFunctions.defaultFunc. 561 apply(this, [aAccessible, aRoleStr, aState, aFlags, aContext]); 562 this._addMencloseNotations(output, aAccessible); 563 return output; 564 } 565 } 566}; 567 568/** 569 * Generates speech utterances from objects, actions and state changes. 570 * An utterance is an array of speech data. 571 * 572 * It should not be assumed that flattening an utterance array would create a 573 * gramatically correct sentence. For example, {@link genForObject} might 574 * return: ['graphic', 'Welcome to my home page']. 575 * Each string element in an utterance should be gramatically correct in itself. 576 * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama']. 577 * 578 * An utterance is ordered from the least to the most important. Speaking the 579 * last string usually makes sense, but speaking the first often won't. 580 * For example {@link genForAction} might return ['button', 'clicked'] for a 581 * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does 582 * not. 583 */ 584this.UtteranceGenerator = { // jshint ignore:line 585 __proto__: OutputGenerator, // jshint ignore:line 586 587 gActionMap: { 588 jump: 'jumpAction', 589 press: 'pressAction', 590 check: 'checkAction', 591 uncheck: 'uncheckAction', 592 on: 'onAction', 593 off: 'offAction', 594 select: 'selectAction', 595 unselect: 'unselectAction', 596 open: 'openAction', 597 close: 'closeAction', 598 switch: 'switchAction', 599 click: 'clickAction', 600 collapse: 'collapseAction', 601 expand: 'expandAction', 602 activate: 'activateAction', 603 cycle: 'cycleAction' 604 }, 605 606 //TODO: May become more verbose in the future. 607 genForAction: function genForAction(aObject, aActionName) { 608 return [{string: this.gActionMap[aActionName]}]; 609 }, 610 611 genForLiveRegion: 612 function genForLiveRegion(aContext, aIsHide, aModifiedText) { 613 let utterance = []; 614 if (aIsHide) { 615 utterance.push({string: 'hidden'}); 616 } 617 return utterance.concat(aModifiedText || this.genForContext(aContext)); 618 }, 619 620 genForAnnouncement: function genForAnnouncement(aAnnouncement) { 621 return [{ 622 string: aAnnouncement 623 }]; 624 }, 625 626 genForTabStateChange: function genForTabStateChange(aObject, aTabState) { 627 switch (aTabState) { 628 case 'newtab': 629 return [{string: 'tabNew'}]; 630 case 'loading': 631 return [{string: 'tabLoading'}]; 632 case 'loaded': 633 return [aObject.name, {string: 'tabLoaded'}]; 634 case 'loadstopped': 635 return [{string: 'tabLoadStopped'}]; 636 case 'reload': 637 return [{string: 'tabReload'}]; 638 default: 639 return []; 640 } 641 }, 642 643 genForEditingMode: function genForEditingMode(aIsEditing) { 644 return [{string: aIsEditing ? 'editingMode' : 'navigationMode'}]; 645 }, 646 647 objectOutputFunctions: { 648 649 __proto__: OutputGenerator.objectOutputFunctions, // jshint ignore:line 650 651 defaultFunc: function defaultFunc() { 652 return this.objectOutputFunctions._generateBaseOutput.apply( 653 this, arguments); 654 }, 655 656 heading: function heading(aAccessible, aRoleStr, aState, aFlags) { 657 let level = {}; 658 aAccessible.groupPosition(level, {}, {}); 659 let utterance = [{string: 'headingLevel', args: [level.value]}]; 660 661 this._addName(utterance, aAccessible, aFlags); 662 this._addLandmark(utterance, aAccessible); 663 664 return utterance; 665 }, 666 667 listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) { 668 let itemno = {}; 669 let itemof = {}; 670 aAccessible.groupPosition({}, itemof, itemno); 671 let utterance = []; 672 if (itemno.value == 1) { 673 // Start of list 674 utterance.push({string: 'listStart'}); 675 } 676 else if (itemno.value == itemof.value) { 677 // last item 678 utterance.push({string: 'listEnd'}); 679 } 680 681 this._addName(utterance, aAccessible, aFlags); 682 this._addLandmark(utterance, aAccessible); 683 684 return utterance; 685 }, 686 687 list: function list(aAccessible, aRoleStr, aState, aFlags) { 688 return this._getListUtterance 689 (aAccessible, aRoleStr, aFlags, aAccessible.childCount); 690 }, 691 692 definitionlist: 693 function definitionlist(aAccessible, aRoleStr, aState, aFlags) { 694 return this._getListUtterance 695 (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2); 696 }, 697 698 application: function application(aAccessible, aRoleStr, aState, aFlags) { 699 // Don't utter location of applications, it gets tiring. 700 if (aAccessible.name != aAccessible.DOMNode.location) { 701 return this.objectOutputFunctions.defaultFunc.apply(this, 702 [aAccessible, aRoleStr, aState, aFlags]); 703 } 704 705 return []; 706 }, 707 708 cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) { 709 let utterance = []; 710 let cell = aContext.getCellInfo(aAccessible); 711 if (cell) { 712 let addCellChanged = 713 function addCellChanged(aUtterance, aChanged, aString, aIndex) { 714 if (aChanged) { 715 aUtterance.push({string: aString, args: [aIndex + 1]}); 716 } 717 }; 718 let addExtent = function addExtent(aUtterance, aExtent, aString) { 719 if (aExtent > 1) { 720 aUtterance.push({string: aString, args: [aExtent]}); 721 } 722 }; 723 let addHeaders = function addHeaders(aUtterance, aHeaders) { 724 if (aHeaders.length > 0) { 725 aUtterance.push.apply(aUtterance, aHeaders); 726 } 727 }; 728 729 addCellChanged(utterance, cell.columnChanged, 'columnInfo', 730 cell.columnIndex); 731 addCellChanged(utterance, cell.rowChanged, 'rowInfo', cell.rowIndex); 732 733 addExtent(utterance, cell.columnExtent, 'spansColumns'); 734 addExtent(utterance, cell.rowExtent, 'spansRows'); 735 736 addHeaders(utterance, cell.columnHeaders); 737 addHeaders(utterance, cell.rowHeaders); 738 } 739 740 this._addName(utterance, aAccessible, aFlags); 741 this._addLandmark(utterance, aAccessible); 742 743 return utterance; 744 }, 745 746 columnheader: function columnheader() { 747 return this.objectOutputFunctions.cell.apply(this, arguments); 748 }, 749 750 rowheader: function rowheader() { 751 return this.objectOutputFunctions.cell.apply(this, arguments); 752 }, 753 754 statictext: function statictext(aAccessible) { 755 if (Utils.isListItemDecorator(aAccessible, true)) { 756 return []; 757 } 758 759 return this.objectOutputFunctions.defaultFunc.apply(this, arguments); 760 } 761 }, 762 763 _getContextStart: function _getContextStart(aContext) { 764 return aContext.newAncestry; 765 }, 766 767 _addRole: function _addRole(aOutput, aAccessible, aRoleStr) { 768 if (this.mathmlRolesSet.has(aAccessible.role)) { 769 this._addMathRoles(aOutput, aAccessible, aRoleStr); 770 } else { 771 aOutput.push({string: this._getOutputName(aRoleStr)}); 772 } 773 }, 774 775 _addState: function _addState(aOutput, aState, aRoleStr) { 776 777 if (aState.contains(States.UNAVAILABLE)) { 778 aOutput.push({string: 'stateUnavailable'}); 779 } 780 781 if (aState.contains(States.READONLY)) { 782 aOutput.push({string: 'stateReadonly'}); 783 } 784 785 // Don't utter this in Jelly Bean, we let TalkBack do it for us there. 786 // This is because we expose the checked information on the node itself. 787 // XXX: this means the checked state is always appended to the end, 788 // regardless of the utterance ordering preference. 789 if ((Utils.AndroidSdkVersion < 16 || Utils.MozBuildApp === 'browser') && 790 aState.contains(States.CHECKABLE)) { 791 let checked = aState.contains(States.CHECKED); 792 let statetr; 793 if (aRoleStr === 'switch') { 794 statetr = checked ? 'stateOn' : 'stateOff'; 795 } else { 796 statetr = checked ? 'stateChecked' : 'stateNotChecked'; 797 } 798 aOutput.push({string: statetr}); 799 } 800 801 if (aState.contains(States.PRESSED)) { 802 aOutput.push({string: 'statePressed'}); 803 } 804 805 if (aState.contains(States.EXPANDABLE)) { 806 let statetr = aState.contains(States.EXPANDED) ? 807 'stateExpanded' : 'stateCollapsed'; 808 aOutput.push({string: statetr}); 809 } 810 811 if (aState.contains(States.REQUIRED)) { 812 aOutput.push({string: 'stateRequired'}); 813 } 814 815 if (aState.contains(States.TRAVERSED)) { 816 aOutput.push({string: 'stateTraversed'}); 817 } 818 819 if (aState.contains(States.HASPOPUP)) { 820 aOutput.push({string: 'stateHasPopup'}); 821 } 822 823 if (aState.contains(States.SELECTED)) { 824 aOutput.push({string: 'stateSelected'}); 825 } 826 }, 827 828 _getListUtterance: 829 function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) { 830 let utterance = []; 831 this._addRole(utterance, aAccessible, aRoleStr); 832 utterance.push({ 833 string: this._getOutputName('listItemsCount'), 834 count: aItemCount 835 }); 836 837 this._addName(utterance, aAccessible, aFlags); 838 this._addLandmark(utterance, aAccessible); 839 840 return utterance; 841 } 842}; 843 844this.BrailleGenerator = { // jshint ignore:line 845 __proto__: OutputGenerator, // jshint ignore:line 846 847 genForContext: function genForContext(aContext) { 848 let output = OutputGenerator.genForContext.apply(this, arguments); 849 850 let acc = aContext.accessible; 851 852 // add the static text indicating a list item; do this for both listitems or 853 // direct first children of listitems, because these are both common 854 // browsing scenarios 855 let addListitemIndicator = function addListitemIndicator(indicator = '*') { 856 output.unshift(indicator); 857 }; 858 859 if (acc.indexInParent === 1 && 860 acc.parent.role == Roles.LISTITEM && 861 acc.previousSibling.role == Roles.STATICTEXT) { 862 if (acc.parent.parent && acc.parent.parent.DOMNode && 863 acc.parent.parent.DOMNode.nodeName == 'UL') { 864 addListitemIndicator(); 865 } else { 866 addListitemIndicator(acc.previousSibling.name.trim()); 867 } 868 } else if (acc.role == Roles.LISTITEM && acc.firstChild && 869 acc.firstChild.role == Roles.STATICTEXT) { 870 if (acc.parent.DOMNode.nodeName == 'UL') { 871 addListitemIndicator(); 872 } else { 873 addListitemIndicator(acc.firstChild.name.trim()); 874 } 875 } 876 877 return output; 878 }, 879 880 objectOutputFunctions: { 881 882 __proto__: OutputGenerator.objectOutputFunctions, // jshint ignore:line 883 884 defaultFunc: function defaultFunc() { 885 return this.objectOutputFunctions._generateBaseOutput.apply( 886 this, arguments); 887 }, 888 889 listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) { 890 let braille = []; 891 892 this._addName(braille, aAccessible, aFlags); 893 this._addLandmark(braille, aAccessible); 894 895 return braille; 896 }, 897 898 cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) { 899 let braille = []; 900 let cell = aContext.getCellInfo(aAccessible); 901 if (cell) { 902 let addHeaders = function addHeaders(aBraille, aHeaders) { 903 if (aHeaders.length > 0) { 904 aBraille.push.apply(aBraille, aHeaders); 905 } 906 }; 907 908 braille.push({ 909 string: this._getOutputName('cellInfo'), 910 args: [cell.columnIndex + 1, cell.rowIndex + 1] 911 }); 912 913 addHeaders(braille, cell.columnHeaders); 914 addHeaders(braille, cell.rowHeaders); 915 } 916 917 this._addName(braille, aAccessible, aFlags); 918 this._addLandmark(braille, aAccessible); 919 return braille; 920 }, 921 922 columnheader: function columnheader() { 923 return this.objectOutputFunctions.cell.apply(this, arguments); 924 }, 925 926 rowheader: function rowheader() { 927 return this.objectOutputFunctions.cell.apply(this, arguments); 928 }, 929 930 statictext: function statictext(aAccessible) { 931 // Since we customize the list bullet's output, we add the static 932 // text from the first node in each listitem, so skip it here. 933 if (Utils.isListItemDecorator(aAccessible)) { 934 return []; 935 } 936 937 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); 938 }, 939 940 _useStateNotRole: 941 function _useStateNotRole(aAccessible, aRoleStr, aState, aFlags) { 942 let braille = []; 943 this._addState(braille, aState, aRoleStr); 944 this._addName(braille, aAccessible, aFlags); 945 this._addLandmark(braille, aAccessible); 946 947 return braille; 948 }, 949 950 switch: function braille_generator_object_output_functions_switch() { 951 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); 952 }, 953 954 checkbutton: function checkbutton() { 955 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); 956 }, 957 958 radiobutton: function radiobutton() { 959 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); 960 }, 961 962 togglebutton: function togglebutton() { 963 return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); 964 } 965 }, 966 967 _getContextStart: function _getContextStart(aContext) { 968 if (aContext.accessible.parent.role == Roles.LINK) { 969 return [aContext.accessible.parent]; 970 } 971 972 return []; 973 }, 974 975 _getOutputName: function _getOutputName(aName) { 976 return OutputGenerator._getOutputName(aName) + 'Abbr'; 977 }, 978 979 _addRole: function _addRole(aBraille, aAccessible, aRoleStr) { 980 if (this.mathmlRolesSet.has(aAccessible.role)) { 981 this._addMathRoles(aBraille, aAccessible, aRoleStr); 982 } else { 983 aBraille.push({string: this._getOutputName(aRoleStr)}); 984 } 985 }, 986 987 _addState: function _addState(aBraille, aState, aRoleStr) { 988 if (aState.contains(States.CHECKABLE)) { 989 aBraille.push({ 990 string: aState.contains(States.CHECKED) ? 991 this._getOutputName('stateChecked') : 992 this._getOutputName('stateUnchecked') 993 }); 994 } 995 if (aRoleStr === 'toggle button') { 996 aBraille.push({ 997 string: aState.contains(States.PRESSED) ? 998 this._getOutputName('statePressed') : 999 this._getOutputName('stateUnpressed') 1000 }); 1001 } 1002 } 1003}; 1004