accessible/src/jsat/OutputGenerator.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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/. */
     5 'use strict';
     7 const Cc = Components.classes;
     8 const Ci = Components.interfaces;
     9 const Cu = Components.utils;
    10 const Cr = Components.results;
    12 const INCLUDE_DESC = 0x01;
    13 const INCLUDE_NAME = 0x02;
    14 const INCLUDE_VALUE = 0x04;
    15 const INCLUDE_CUSTOM = 0x08;
    16 const NAME_FROM_SUBTREE_RULE = 0x10;
    17 const IGNORE_EXPLICIT_NAME = 0x20;
    19 const OUTPUT_DESC_FIRST = 0;
    20 const OUTPUT_DESC_LAST = 1;
    22 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    23 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
    24   'resource://gre/modules/accessibility/Utils.jsm');
    25 XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache',
    26   'resource://gre/modules/accessibility/Utils.jsm');
    27 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
    28   'resource://gre/modules/accessibility/Utils.jsm');
    29 XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm',
    30   'resource://gre/modules/PluralForm.jsm');
    31 XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
    32   'resource://gre/modules/accessibility/Constants.jsm');
    33 XPCOMUtils.defineLazyModuleGetter(this, 'States',
    34   'resource://gre/modules/accessibility/Constants.jsm');
    36 this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator'];
    38 this.OutputGenerator = {
    40   defaultOutputOrder: OUTPUT_DESC_LAST,
    42   /**
    43    * Generates output for a PivotContext.
    44    * @param {PivotContext} aContext object that generates and caches
    45    *    context information for a given accessible and its relationship with
    46    *    another accessible.
    47    * @return {Object} An object that neccessarily has an output property which
    48    *    is an array of strings. Depending on the utterance order,
    49    *    the strings describe the context for an accessible object either
    50    *    starting from the accessible's ancestry or accessible's subtree.
    51    *    The object may also have properties specific to the type of output
    52    *    generated.
    53    */
    54   genForContext: function genForContext(aContext) {
    55     let output = [];
    56     let self = this;
    57     let addOutput = function addOutput(aAccessible) {
    58       output.push.apply(output, self.genForObject(aAccessible, aContext));
    59     };
    60     let ignoreSubtree = function ignoreSubtree(aAccessible) {
    61       let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
    62       let nameRule = self.roleRuleMap[roleString] || 0;
    63       // Ignore subtree if the name is explicit and the role's name rule is the
    64       // NAME_FROM_SUBTREE_RULE.
    65       return (((nameRule & INCLUDE_VALUE) && aAccessible.value) ||
    66               ((nameRule & NAME_FROM_SUBTREE_RULE) &&
    67                (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' &&
    68                !(nameRule & IGNORE_EXPLICIT_NAME))));
    69     };
    71     let contextStart = this._getContextStart(aContext);
    73     if (this.outputOrder === OUTPUT_DESC_FIRST) {
    74       contextStart.forEach(addOutput);
    75       addOutput(aContext.accessible);
    76       [addOutput(node) for
    77         (node of aContext.subtreeGenerator(true, ignoreSubtree))];
    78     } else {
    79       [addOutput(node) for
    80         (node of aContext.subtreeGenerator(false, ignoreSubtree))];
    81       addOutput(aContext.accessible);
    82       contextStart.reverse().forEach(addOutput);
    83     }
    85     // Clean up the white space.
    86     let trimmed;
    87     output = [trimmed for (word of output) if (trimmed = word.trim())];
    88     return {output: output};
    89   },
    92   /**
    93    * Generates output for an object.
    94    * @param {nsIAccessible} aAccessible accessible object to generate output
    95    *    for.
    96    * @param {PivotContext} aContext object that generates and caches
    97    *    context information for a given accessible and its relationship with
    98    *    another accessible.
    99    * @return {Array} Two string array. The first string describes the object
   100    *    and its state. The second string is the object's name. Whether the
   101    *    object's description or it's role is included is determined by
   102    *    {@link roleRuleMap}.
   103    */
   104   genForObject: function genForObject(aAccessible, aContext) {
   105     let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
   106     let func = this.objectOutputFunctions[
   107       OutputGenerator._getOutputName(roleString)] ||
   108       this.objectOutputFunctions.defaultFunc;
   110     let flags = this.roleRuleMap[roleString] || 0;
   112     if (aAccessible.childCount == 0)
   113       flags |= INCLUDE_NAME;
   115     return func.apply(this, [aAccessible, roleString,
   116                              Utils.getState(aAccessible), flags, aContext]);
   117   },
   119   /**
   120    * Generates output for an action performed.
   121    * @param {nsIAccessible} aAccessible accessible object that the action was
   122    *    invoked in.
   123    * @param {string} aActionName the name of the action, one of the keys in
   124    *    {@link gActionMap}.
   125    * @return {Array} A one string array with the action.
   126    */
   127   genForAction: function genForAction(aObject, aActionName) {},
   129   /**
   130    * Generates output for an announcement. Basically attempts to localize
   131    * the announcement string.
   132    * @param {string} aAnnouncement unlocalized announcement.
   133    * @return {Array} A one string array with the announcement.
   134    */
   135   genForAnnouncement: function genForAnnouncement(aAnnouncement) {},
   137   /**
   138    * Generates output for a tab state change.
   139    * @param {nsIAccessible} aAccessible accessible object of the tab's attached
   140    *    document.
   141    * @param {string} aTabState the tab state name, see
   142    *    {@link Presenter.tabStateChanged}.
   143    * @return {Array} The tab state utterace.
   144    */
   145   genForTabStateChange: function genForTabStateChange(aObject, aTabState) {},
   147   /**
   148    * Generates output for announcing entering and leaving editing mode.
   149    * @param {aIsEditing} boolean true if we are in editing mode
   150    * @return {Array} The mode utterance
   151    */
   152   genForEditingMode: function genForEditingMode(aIsEditing) {},
   154   _getContextStart: function getContextStart(aContext) {},
   156   _addName: function _addName(aOutput, aAccessible, aFlags) {
   157     let name;
   158     if ((Utils.getAttributes(aAccessible)['explicit-name'] === 'true' &&
   159          !(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) {
   160       name = aAccessible.name;
   161     }
   163     let description = aAccessible.description;
   164     if (description) {
   165       // Compare against the calculated name unconditionally, regardless of name rule,
   166       // so we can make sure we don't speak duplicated descriptions
   167       let tmpName = name || aAccessible.name;
   168       if (tmpName && (description !== tmpName)) {
   169         name = name || '';
   170         name = this.outputOrder === OUTPUT_DESC_FIRST ?
   171           description + ' - ' + name :
   172           name + ' - ' + description;
   173       }
   174     }
   176     if (name) {
   177       aOutput[this.outputOrder === OUTPUT_DESC_FIRST ?
   178         'push' : 'unshift'](name);
   179     }
   180   },
   182   /**
   183    * Adds a landmark role to the output if available.
   184    * @param {Array} aOutput Output array.
   185    * @param {nsIAccessible} aAccessible current accessible object.
   186    */
   187   _addLandmark: function _addLandmark(aOutput, aAccessible) {
   188     let landmarkName = Utils.getLandmarkName(aAccessible);
   189     if (!landmarkName) {
   190       return;
   191     }
   193     let landmark = Utils.stringBundle.GetStringFromName(landmarkName);
   194     if (!landmark) {
   195       return;
   196     }
   198     aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push'](
   199       landmark);
   200   },
   202   /**
   203    * Adds an entry type attribute to the description if available.
   204    * @param {Array} aDesc Description array.
   205    * @param {nsIAccessible} aAccessible current accessible object.
   206    * @param {String} aRoleStr aAccessible's role string.
   207    */
   208   _addType: function _addType(aDesc, aAccessible, aRoleStr) {
   209     if (aRoleStr !== 'entry') {
   210       return;
   211     }
   213     let typeName = Utils.getAttributes(aAccessible)['text-input-type'];
   214     // Ignore the the input type="text" case.
   215     if (!typeName || typeName === 'text') {
   216       return;
   217     }
   218     typeName = 'textInputType_' + typeName;
   219     try {
   220       aDesc.push(Utils.stringBundle.GetStringFromName(typeName));
   221     } catch (x) {
   222       Logger.warning('Failed to get a string from a bundle for', typeName);
   223     }
   224   },
   226   get outputOrder() {
   227     if (!this._utteranceOrder) {
   228       this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance');
   229     }
   230     return typeof this._utteranceOrder.value === 'number' ?
   231       this._utteranceOrder.value : this.defaultOutputOrder;
   232   },
   234   _getOutputName: function _getOutputName(aName) {
   235     return aName.replace(' ', '');
   236   },
   238   _getLocalizedRole: function _getLocalizedRole(aRoleStr) {},
   240   _getLocalizedState: function _getLocalizedState(aState) {},
   242   _getPluralFormString: function _getPluralFormString(aString, aCount) {
   243     let str = Utils.stringBundle.GetStringFromName(this._getOutputName(aString));
   244     str = PluralForm.get(aCount, str);
   245     return str.replace('#1', aCount);
   246   },
   248   roleRuleMap: {
   249     'menubar': INCLUDE_DESC,
   250     'scrollbar': INCLUDE_DESC,
   251     'grip': INCLUDE_DESC,
   252     'alert': INCLUDE_DESC | INCLUDE_NAME,
   253     'menupopup': INCLUDE_DESC,
   254     'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   255     'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   256     'columnheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   257     'rowheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   258     'column': NAME_FROM_SUBTREE_RULE,
   259     'row': NAME_FROM_SUBTREE_RULE,
   260     'cell': INCLUDE_DESC | INCLUDE_NAME,
   261     'application': INCLUDE_NAME,
   262     'document': INCLUDE_NAME,
   263     'grouping': INCLUDE_DESC | INCLUDE_NAME,
   264     'toolbar': INCLUDE_DESC,
   265     'table': INCLUDE_DESC | INCLUDE_NAME,
   266     'link': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   267     'helpballoon': NAME_FROM_SUBTREE_RULE,
   268     'list': INCLUDE_DESC | INCLUDE_NAME,
   269     'listitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   270     'outline': INCLUDE_DESC,
   271     'outlineitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   272     'pagetab': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   273     'graphic': INCLUDE_DESC,
   274     'pushbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   275     'checkbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   276     'radiobutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   277     'buttondropdown': NAME_FROM_SUBTREE_RULE,
   278     'combobox': INCLUDE_DESC,
   279     'droplist': INCLUDE_DESC,
   280     'progressbar': INCLUDE_DESC | INCLUDE_VALUE,
   281     'slider': INCLUDE_DESC | INCLUDE_VALUE,
   282     'spinbutton': INCLUDE_DESC | INCLUDE_VALUE,
   283     'diagram': INCLUDE_DESC,
   284     'animation': INCLUDE_DESC,
   285     'equation': INCLUDE_DESC,
   286     'buttonmenu': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   287     'buttondropdowngrid': NAME_FROM_SUBTREE_RULE,
   288     'pagetablist': INCLUDE_DESC,
   289     'canvas': INCLUDE_DESC,
   290     'check menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   291     'label': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   292     'password text': INCLUDE_DESC,
   293     'popup menu': INCLUDE_DESC,
   294     'radio menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   295     'table column header': NAME_FROM_SUBTREE_RULE,
   296     'table row header': NAME_FROM_SUBTREE_RULE,
   297     'tear off menu item': NAME_FROM_SUBTREE_RULE,
   298     'toggle button': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   299     'parent menuitem': NAME_FROM_SUBTREE_RULE,
   300     'header': INCLUDE_DESC,
   301     'footer': INCLUDE_DESC,
   302     'entry': INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE,
   303     'caption': INCLUDE_DESC,
   304     'document frame': INCLUDE_DESC,
   305     'heading': INCLUDE_DESC,
   306     'calendar': INCLUDE_DESC | INCLUDE_NAME,
   307     'combobox list': INCLUDE_DESC,
   308     'combobox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   309     'listbox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   310     'listbox rich option': NAME_FROM_SUBTREE_RULE,
   311     'gridcell': NAME_FROM_SUBTREE_RULE,
   312     'check rich option': NAME_FROM_SUBTREE_RULE,
   313     'term': NAME_FROM_SUBTREE_RULE,
   314     'definition': NAME_FROM_SUBTREE_RULE,
   315     'key': NAME_FROM_SUBTREE_RULE,
   316     'image map': INCLUDE_DESC,
   317     'option': INCLUDE_DESC,
   318     'listbox': INCLUDE_DESC,
   319     'definitionlist': INCLUDE_DESC | INCLUDE_NAME,
   320     'dialog': INCLUDE_DESC | INCLUDE_NAME,
   321     'chrome window': IGNORE_EXPLICIT_NAME,
   322     'app root': IGNORE_EXPLICIT_NAME },
   324   objectOutputFunctions: {
   325     _generateBaseOutput: function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) {
   326       let output = [];
   328       if (aFlags & INCLUDE_DESC) {
   329         let desc = this._getLocalizedState(aState);
   330         let roleStr = this._getLocalizedRole(aRoleStr);
   331         if (roleStr) {
   332           this._addType(desc, aAccessible, aRoleStr);
   333           desc.push(roleStr);
   334         }
   335         output.push(desc.join(' '));
   336       }
   338       if (aFlags & INCLUDE_VALUE) {
   339         let value = aAccessible.value;
   340         if (value) {
   341           output[this.outputOrder === OUTPUT_DESC_FIRST ?
   342                  'push' : 'unshift'](value);
   343         }
   344       }
   346       this._addName(output, aAccessible, aFlags);
   347       this._addLandmark(output, aAccessible);
   349       return output;
   350     },
   352     label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) {
   353       if (aContext.isNestedControl ||
   354           aContext.accessible == Utils.getEmbeddedControl(aAccessible)) {
   355         // If we are on a nested control, or a nesting label,
   356         // we don't need the context.
   357         return [];
   358       }
   360       return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
   361     },
   363     entry: function entry(aAccessible, aRoleStr, aState, aFlags) {
   364       let rolestr = aState.contains(States.MULTI_LINE) ? 'textarea' : 'entry';
   365       return this.objectOutputFunctions.defaultFunc.apply(
   366         this, [aAccessible, rolestr, aState, aFlags]);
   367     },
   369     pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) {
   370       let localizedRole = this._getLocalizedRole(aRoleStr);
   371       let itemno = {};
   372       let itemof = {};
   373       aAccessible.groupPosition({}, itemof, itemno);
   374       let output = [];
   375       let desc = this._getLocalizedState(aState);
   376       desc.push(
   377         Utils.stringBundle.formatStringFromName(
   378           'objItemOf', [localizedRole, itemno.value, itemof.value], 3));
   379       output.push(desc.join(' '));
   381       this._addName(output, aAccessible, aFlags);
   382       this._addLandmark(output, aAccessible);
   384       return output;
   385     },
   387     table: function table(aAccessible, aRoleStr, aState, aFlags) {
   388       let output = [];
   389       let table;
   390       try {
   391         table = aAccessible.QueryInterface(Ci.nsIAccessibleTable);
   392       } catch (x) {
   393         Logger.logException(x);
   394         return output;
   395       } finally {
   396         // Check if it's a layout table, and bail out if true.
   397         // We don't want to speak any table information for layout tables.
   398         if (table.isProbablyForLayout()) {
   399           return output;
   400         }
   401         let tableColumnInfo = this._getPluralFormString('tableColumnInfo',
   402           table.columnCount);
   403         let tableRowInfo = this._getPluralFormString('tableRowInfo',
   404           table.rowCount);
   405         output.push(Utils.stringBundle.formatStringFromName(
   406           this._getOutputName('tableInfo'), [this._getLocalizedRole(aRoleStr),
   407             tableColumnInfo, tableRowInfo], 3));
   408         this._addName(output, aAccessible, aFlags);
   409         this._addLandmark(output, aAccessible);
   410         return output;
   411       }
   412     }
   413   }
   414 };
   416 /**
   417  * Generates speech utterances from objects, actions and state changes.
   418  * An utterance is an array of strings.
   419  *
   420  * It should not be assumed that flattening an utterance array would create a
   421  * gramatically correct sentence. For example, {@link genForObject} might
   422  * return: ['graphic', 'Welcome to my home page'].
   423  * Each string element in an utterance should be gramatically correct in itself.
   424  * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
   425  *
   426  * An utterance is ordered from the least to the most important. Speaking the
   427  * last string usually makes sense, but speaking the first often won't.
   428  * For example {@link genForAction} might return ['button', 'clicked'] for a
   429  * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
   430  * not.
   431  */
   432 this.UtteranceGenerator = {
   433   __proto__: OutputGenerator,
   435   gActionMap: {
   436     jump: 'jumpAction',
   437     press: 'pressAction',
   438     check: 'checkAction',
   439     uncheck: 'uncheckAction',
   440     select: 'selectAction',
   441     unselect: 'unselectAction',
   442     open: 'openAction',
   443     close: 'closeAction',
   444     switch: 'switchAction',
   445     click: 'clickAction',
   446     collapse: 'collapseAction',
   447     expand: 'expandAction',
   448     activate: 'activateAction',
   449     cycle: 'cycleAction'
   450   },
   452   //TODO: May become more verbose in the future.
   453   genForAction: function genForAction(aObject, aActionName) {
   454     return [Utils.stringBundle.GetStringFromName(this.gActionMap[aActionName])];
   455   },
   457   genForLiveRegion: function genForLiveRegion(aContext, aIsHide, aModifiedText) {
   458     let utterance = [];
   459     if (aIsHide) {
   460       utterance.push(Utils.stringBundle.GetStringFromName('hidden'));
   461     }
   462     return utterance.concat(
   463       aModifiedText || this.genForContext(aContext).output);
   464   },
   466   genForAnnouncement: function genForAnnouncement(aAnnouncement) {
   467     try {
   468       return [Utils.stringBundle.GetStringFromName(aAnnouncement)];
   469     } catch (x) {
   470       return [aAnnouncement];
   471     }
   472   },
   474   genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
   475     switch (aTabState) {
   476       case 'newtab':
   477         return [Utils.stringBundle.GetStringFromName('tabNew')];
   478       case 'loading':
   479         return [Utils.stringBundle.GetStringFromName('tabLoading')];
   480       case 'loaded':
   481         return [aObject.name || '',
   482                 Utils.stringBundle.GetStringFromName('tabLoaded')];
   483       case 'loadstopped':
   484         return [Utils.stringBundle.GetStringFromName('tabLoadStopped')];
   485       case 'reload':
   486         return [Utils.stringBundle.GetStringFromName('tabReload')];
   487       default:
   488         return [];
   489     }
   490   },
   492   genForEditingMode: function genForEditingMode(aIsEditing) {
   493     return [Utils.stringBundle.GetStringFromName(
   494       aIsEditing ? 'editingMode' : 'navigationMode')];
   495   },
   497   objectOutputFunctions: {
   499     __proto__: OutputGenerator.objectOutputFunctions,
   501     defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) {
   502       return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
   503     },
   505     heading: function heading(aAccessible, aRoleStr, aState, aFlags) {
   506       let level = {};
   507       aAccessible.groupPosition(level, {}, {});
   508       let utterance =
   509         [Utils.stringBundle.formatStringFromName(
   510           'headingLevel', [level.value], 1)];
   512       this._addName(utterance, aAccessible, aFlags);
   513       this._addLandmark(utterance, aAccessible);
   515       return utterance;
   516     },
   518     listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
   519       let itemno = {};
   520       let itemof = {};
   521       aAccessible.groupPosition({}, itemof, itemno);
   522       let utterance = [];
   523       if (itemno.value == 1) // Start of list
   524         utterance.push(Utils.stringBundle.GetStringFromName('listStart'));
   525       else if (itemno.value == itemof.value) // last item
   526         utterance.push(Utils.stringBundle.GetStringFromName('listEnd'));
   528       this._addName(utterance, aAccessible, aFlags);
   529       this._addLandmark(utterance, aAccessible);
   531       return utterance;
   532     },
   534     list: function list(aAccessible, aRoleStr, aState, aFlags) {
   535       return this._getListUtterance
   536         (aAccessible, aRoleStr, aFlags, aAccessible.childCount);
   537     },
   539     definitionlist: function definitionlist(aAccessible, aRoleStr, aState, aFlags) {
   540       return this._getListUtterance
   541         (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2);
   542     },
   544     application: function application(aAccessible, aRoleStr, aState, aFlags) {
   545       // Don't utter location of applications, it gets tiring.
   546       if (aAccessible.name != aAccessible.DOMNode.location)
   547         return this.objectOutputFunctions.defaultFunc.apply(this,
   548           [aAccessible, aRoleStr, aState, aFlags]);
   550       return [];
   551     },
   553     cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
   554       let utterance = [];
   555       let cell = aContext.getCellInfo(aAccessible);
   556       if (cell) {
   557         let desc = [];
   558         let addCellChanged = function addCellChanged(aDesc, aChanged, aString, aIndex) {
   559           if (aChanged) {
   560             aDesc.push(Utils.stringBundle.formatStringFromName(aString,
   561               [aIndex + 1], 1));
   562           }
   563         };
   564         let addExtent = function addExtent(aDesc, aExtent, aString) {
   565           if (aExtent > 1) {
   566             aDesc.push(Utils.stringBundle.formatStringFromName(aString,
   567               [aExtent], 1));
   568           }
   569         };
   570         let addHeaders = function addHeaders(aDesc, aHeaders) {
   571           if (aHeaders.length > 0) {
   572             aDesc.push.apply(aDesc, aHeaders);
   573           }
   574         };
   576         addCellChanged(desc, cell.columnChanged, 'columnInfo', cell.columnIndex);
   577         addCellChanged(desc, cell.rowChanged, 'rowInfo', cell.rowIndex);
   579         addExtent(desc, cell.columnExtent, 'spansColumns');
   580         addExtent(desc, cell.rowExtent, 'spansRows');
   582         addHeaders(desc, cell.columnHeaders);
   583         addHeaders(desc, cell.rowHeaders);
   585         utterance.push(desc.join(' '));
   586       }
   588       this._addName(utterance, aAccessible, aFlags);
   589       this._addLandmark(utterance, aAccessible);
   591       return utterance;
   592     },
   594     columnheader: function columnheader() {
   595       return this.objectOutputFunctions.cell.apply(this, arguments);
   596     },
   598     rowheader: function rowheader() {
   599       return this.objectOutputFunctions.cell.apply(this, arguments);
   600     }
   601   },
   603   _getContextStart: function _getContextStart(aContext) {
   604     return aContext.newAncestry;
   605   },
   607   _getLocalizedRole: function _getLocalizedRole(aRoleStr) {
   608     try {
   609       return Utils.stringBundle.GetStringFromName(
   610         this._getOutputName(aRoleStr));
   611     } catch (x) {
   612       return '';
   613     }
   614   },
   616   _getLocalizedState: function _getLocalizedState(aState) {
   617     let stateUtterances = [];
   619     if (aState.contains(States.UNAVAILABLE)) {
   620       stateUtterances.push(
   621         Utils.stringBundle.GetStringFromName('stateUnavailable'));
   622     }
   624     // Don't utter this in Jelly Bean, we let TalkBack do it for us there.
   625     // This is because we expose the checked information on the node itself.
   626     // XXX: this means the checked state is always appended to the end, regardless
   627     // of the utterance ordering preference.
   628     if ((Utils.AndroidSdkVersion < 16 || Utils.MozBuildApp === 'browser') &&
   629       aState.contains(States.CHECKABLE)) {
   630       let statetr = aState.contains(States.CHECKED) ?
   631         'stateChecked' : 'stateNotChecked';
   632       stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr));
   633     }
   635     if (aState.contains(States.PRESSED)) {
   636       stateUtterances.push(
   637         Utils.stringBundle.GetStringFromName('statePressed'));
   638     }
   640     if (aState.contains(States.EXPANDABLE)) {
   641       let statetr = aState.contains(States.EXPANDED) ?
   642         'stateExpanded' : 'stateCollapsed';
   643       stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr));
   644     }
   646     if (aState.contains(States.REQUIRED)) {
   647       stateUtterances.push(
   648         Utils.stringBundle.GetStringFromName('stateRequired'));
   649     }
   651     if (aState.contains(States.TRAVERSED)) {
   652       stateUtterances.push(
   653         Utils.stringBundle.GetStringFromName('stateTraversed'));
   654     }
   656     if (aState.contains(States.HASPOPUP)) {
   657       stateUtterances.push(
   658         Utils.stringBundle.GetStringFromName('stateHasPopup'));
   659     }
   661     if (aState.contains(States.SELECTED)) {
   662       stateUtterances.push(
   663         Utils.stringBundle.GetStringFromName('stateSelected'));
   664     }
   666     return stateUtterances;
   667   },
   669   _getListUtterance: function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) {
   670     let desc = [];
   671     let roleStr = this._getLocalizedRole(aRoleStr);
   672     if (roleStr) {
   673       desc.push(roleStr);
   674     }
   675     desc.push(this._getPluralFormString('listItemsCount', aItemCount));
   676     let utterance = [desc.join(' ')];
   678     this._addName(utterance, aAccessible, aFlags);
   679     this._addLandmark(utterance, aAccessible);
   681     return utterance;
   682   }
   683 };
   686 this.BrailleGenerator = {
   687   __proto__: OutputGenerator,
   689   genForContext: function genForContext(aContext) {
   690     let output = OutputGenerator.genForContext.apply(this, arguments);
   692     let acc = aContext.accessible;
   694     // add the static text indicating a list item; do this for both listitems or
   695     // direct first children of listitems, because these are both common browsing
   696     // scenarios
   697     let addListitemIndicator = function addListitemIndicator(indicator = '*') {
   698       output.output.unshift(indicator);
   699     };
   701     if (acc.indexInParent === 1 &&
   702         acc.parent.role == Roles.LISTITEM &&
   703         acc.previousSibling.role == Roles.STATICTEXT) {
   704       if (acc.parent.parent && acc.parent.parent.DOMNode &&
   705           acc.parent.parent.DOMNode.nodeName == 'UL') {
   706         addListitemIndicator();
   707       } else {
   708         addListitemIndicator(acc.previousSibling.name.trim());
   709       }
   710     } else if (acc.role == Roles.LISTITEM && acc.firstChild &&
   711                acc.firstChild.role == Roles.STATICTEXT) {
   712       if (acc.parent.DOMNode.nodeName == 'UL') {
   713         addListitemIndicator();
   714       } else {
   715         addListitemIndicator(acc.firstChild.name.trim());
   716       }
   717     }
   719     if (acc instanceof Ci.nsIAccessibleText) {
   720       output.endOffset = this.outputOrder === OUTPUT_DESC_FIRST ?
   721                          output.output.join(' ').length : acc.characterCount;
   722       output.startOffset = output.endOffset - acc.characterCount;
   723     }
   725     return output;
   726   },
   728   objectOutputFunctions: {
   730     __proto__: OutputGenerator.objectOutputFunctions,
   732     defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) {
   733       return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
   734     },
   736     listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
   737       let braille = [];
   739       this._addName(braille, aAccessible, aFlags);
   740       this._addLandmark(braille, aAccessible);
   742       return braille;
   743     },
   745     cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
   746       let braille = [];
   747       let cell = aContext.getCellInfo(aAccessible);
   748       if (cell) {
   749         let desc = [];
   750         let addHeaders = function addHeaders(aDesc, aHeaders) {
   751           if (aHeaders.length > 0) {
   752             aDesc.push.apply(aDesc, aHeaders);
   753           }
   754         };
   756         desc.push(Utils.stringBundle.formatStringFromName(
   757           this._getOutputName('cellInfo'), [cell.columnIndex + 1,
   758             cell.rowIndex + 1], 2));
   760         addHeaders(desc, cell.columnHeaders);
   761         addHeaders(desc, cell.rowHeaders);
   762         braille.push(desc.join(' '));
   763       }
   765       this._addName(braille, aAccessible, aFlags);
   766       this._addLandmark(braille, aAccessible);
   767       return braille;
   768     },
   770     columnheader: function columnheader() {
   771       return this.objectOutputFunctions.cell.apply(this, arguments);
   772     },
   774     rowheader: function rowheader() {
   775       return this.objectOutputFunctions.cell.apply(this, arguments);
   776     },
   778     statictext: function statictext(aAccessible, aRoleStr, aState, aFlags) {
   779       // Since we customize the list bullet's output, we add the static
   780       // text from the first node in each listitem, so skip it here.
   781       if (aAccessible.parent.role == Roles.LISTITEM) {
   782         return [];
   783       }
   785       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   786     },
   788     _useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aState, aFlags) {
   789       let braille = [];
   791       let desc = this._getLocalizedState(aState, aAccessible.role);
   792       braille.push(desc.join(' '));
   794       this._addName(braille, aAccessible, aFlags);
   795       this._addLandmark(braille, aAccessible);
   797       return braille;
   798     },
   800     checkbutton: function checkbutton(aAccessible, aRoleStr, aState, aFlags) {
   801       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   802     },
   804     radiobutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) {
   805       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   806     },
   808     togglebutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) {
   809       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   810     }
   811   },
   813   _getContextStart: function _getContextStart(aContext) {
   814     if (aContext.accessible.parent.role == Roles.LINK) {
   815       return [aContext.accessible.parent];
   816     }
   818     return [];
   819   },
   821   _getOutputName: function _getOutputName(aName) {
   822     return OutputGenerator._getOutputName(aName) + 'Abbr';
   823   },
   825   _getLocalizedRole: function _getLocalizedRole(aRoleStr) {
   826     try {
   827       return Utils.stringBundle.GetStringFromName(
   828         this._getOutputName(aRoleStr));
   829     } catch (x) {
   830       try {
   831         return Utils.stringBundle.GetStringFromName(
   832           OutputGenerator._getOutputName(aRoleStr));
   833       } catch (y) {
   834         return '';
   835       }
   836     }
   837   },
   839   _getLocalizedState: function _getLocalizedState(aState, aRole) {
   840     let stateBraille = [];
   842     let getResultMarker = function getResultMarker(aMarker) {
   843       // aMarker is a simple boolean.
   844       let resultMarker = [];
   845       resultMarker.push('(');
   846       resultMarker.push(aMarker ? 'x' : ' ');
   847       resultMarker.push(')');
   849       return resultMarker.join('');
   850     };
   852     if (aState.contains(States.CHECKABLE)) {
   853       stateBraille.push(getResultMarker(aState.contains(States.CHECKED)));
   854     }
   856     if (aRole === Roles.TOGGLE_BUTTON) {
   857       stateBraille.push(getResultMarker(aState.contains(States.PRESSED)));
   858     }
   860     return stateBraille;
   861   }
   863 };

mercurial