Backed out changeset 0b84a9bee07a (bug 937429) for Android test_videocontrols_standal...
[gecko.git] / accessible / src / jsat / OutputGenerator.jsm
blob3cab3c00f12e581955af870f54025076a34feab0
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;
18 const OUTPUT_DESC_FIRST = 0;
19 const OUTPUT_DESC_LAST = 1;
21 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
22 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
23   'resource://gre/modules/accessibility/Utils.jsm');
24 XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache',
25   'resource://gre/modules/accessibility/Utils.jsm');
26 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
27   'resource://gre/modules/accessibility/Utils.jsm');
28 XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm',
29   'resource://gre/modules/PluralForm.jsm');
30 XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
31   'resource://gre/modules/accessibility/Constants.jsm');
33 var gStringBundle = Cc['@mozilla.org/intl/stringbundle;1'].
34   getService(Ci.nsIStringBundleService).
35   createBundle('chrome://global/locale/AccessFu.properties');
37 this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator'];
39 this.OutputGenerator = {
41   defaultOutputOrder: OUTPUT_DESC_LAST,
43   /**
44    * Generates output for a PivotContext.
45    * @param {PivotContext} aContext object that generates and caches
46    *    context information for a given accessible and its relationship with
47    *    another accessible.
48    * @return {Object} An object that neccessarily has an output property which
49    *    is an array of strings. Depending on the utterance order,
50    *    the strings describe the context for an accessible object either
51    *    starting from the accessible's ancestry or accessible's subtree.
52    *    The object may also have properties specific to the type of output
53    *    generated.
54    */
55   genForContext: function genForContext(aContext) {
56     let output = [];
57     let self = this;
58     let addOutput = function addOutput(aAccessible) {
59       output.push.apply(output, self.genForObject(aAccessible, aContext));
60     };
61     let ignoreSubtree = function ignoreSubtree(aAccessible) {
62       let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
63       let nameRule = self.roleRuleMap[roleString] || 0;
64       // Ignore subtree if the name is explicit and the role's name rule is the
65       // NAME_FROM_SUBTREE_RULE.
66       return (((nameRule & INCLUDE_VALUE) && aAccessible.value) ||
67               ((nameRule & NAME_FROM_SUBTREE_RULE) &&
68                Utils.getAttributes(aAccessible)['explicit-name'] === 'true'));
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 states. 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     let state = {};
116     let extState = {};
117     aAccessible.getState(state, extState);
118     let states = {base: state.value, ext: extState.value};
119     return func.apply(this, [aAccessible, roleString, states, flags, aContext]);
120   },
122   /**
123    * Generates output for an action performed.
124    * @param {nsIAccessible} aAccessible accessible object that the action was
125    *    invoked in.
126    * @param {string} aActionName the name of the action, one of the keys in
127    *    {@link gActionMap}.
128    * @return {Array} A one string array with the action.
129    */
130   genForAction: function genForAction(aObject, aActionName) {},
132   /**
133    * Generates output for an announcement. Basically attempts to localize
134    * the announcement string.
135    * @param {string} aAnnouncement unlocalized announcement.
136    * @return {Array} A one string array with the announcement.
137    */
138   genForAnnouncement: function genForAnnouncement(aAnnouncement) {},
140   /**
141    * Generates output for a tab state change.
142    * @param {nsIAccessible} aAccessible accessible object of the tab's attached
143    *    document.
144    * @param {string} aTabState the tab state name, see
145    *    {@link Presenter.tabStateChanged}.
146    * @return {Array} The tab state utterace.
147    */
148   genForTabStateChange: function genForTabStateChange(aObject, aTabState) {},
150   /**
151    * Generates output for announcing entering and leaving editing mode.
152    * @param {aIsEditing} boolean true if we are in editing mode
153    * @return {Array} The mode utterance
154    */
155   genForEditingMode: function genForEditingMode(aIsEditing) {},
157   _getContextStart: function getContextStart(aContext) {},
159   _addName: function _addName(aOutput, aAccessible, aFlags) {
160     let name;
161     if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' ||
162       (aFlags & INCLUDE_NAME)) {
163       name = aAccessible.name;
164     }
166     let description = aAccessible.description;
167     if (description) {
168       // Compare against the calculated name unconditionally, regardless of name rule,
169       // so we can make sure we don't speak duplicated descriptions
170       let tmpName = name || aAccessible.name;
171       if (tmpName && (description !== tmpName)) {
172         name = name || '';
173         name = this.outputOrder === OUTPUT_DESC_FIRST ?
174           description + ' - ' + name :
175           name + ' - ' + description;
176       }
177     }
179     if (name) {
180       aOutput[this.outputOrder === OUTPUT_DESC_FIRST ?
181         'push' : 'unshift'](name);
182     }
183   },
185   /**
186    * Adds a landmark role to the output if available.
187    * @param {Array} aOutput Output array.
188    * @param {nsIAccessible} aAccessible current accessible object.
189    */
190   _addLandmark: function _addLandmark(aOutput, aAccessible) {
191     let landmarkName = Utils.getLandmarkName(aAccessible);
192     if (!landmarkName) {
193       return;
194     }
196     let landmark = gStringBundle.GetStringFromName(landmarkName);
197     if (!landmark) {
198       return;
199     }
201     aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push'](
202       landmark);
203   },
205   /**
206    * Adds an entry type attribute to the description if available.
207    * @param {Array} aDesc Description array.
208    * @param {nsIAccessible} aAccessible current accessible object.
209    * @param {String} aRoleStr aAccessible's role string.
210    */
211   _addType: function _addType(aDesc, aAccessible, aRoleStr) {
212     if (aRoleStr !== 'entry') {
213       return;
214     }
216     let typeName = Utils.getAttributes(aAccessible)['text-input-type'];
217     // Ignore the the input type="text" case.
218     if (!typeName || typeName === 'text') {
219       return;
220     }
221     typeName = 'textInputType_' + typeName;
222     try {
223       aDesc.push(gStringBundle.GetStringFromName(typeName));
224     } catch (x) {
225       Logger.warning('Failed to get a string from a bundle for', typeName);
226     }
227   },
229   get outputOrder() {
230     if (!this._utteranceOrder) {
231       this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance');
232     }
233     return typeof this._utteranceOrder.value === 'number' ?
234       this._utteranceOrder.value : this.defaultOutputOrder;
235   },
237   _getOutputName: function _getOutputName(aName) {
238     return aName.replace(' ', '');
239   },
241   _getLocalizedRole: function _getLocalizedRole(aRoleStr) {},
243   _getLocalizedStates: function _getLocalizedStates(aStates) {},
245   _getPluralFormString: function _getPluralFormString(aString, aCount) {
246     let str = gStringBundle.GetStringFromName(this._getOutputName(aString));
247     str = PluralForm.get(aCount, str);
248     return str.replace('#1', aCount);
249   },
251   roleRuleMap: {
252     'menubar': INCLUDE_DESC,
253     'scrollbar': INCLUDE_DESC,
254     'grip': INCLUDE_DESC,
255     'alert': INCLUDE_DESC | INCLUDE_NAME,
256     'menupopup': INCLUDE_DESC,
257     'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
258     'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
259     'columnheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
260     'rowheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
261     'column': NAME_FROM_SUBTREE_RULE,
262     'row': NAME_FROM_SUBTREE_RULE,
263     'cell': INCLUDE_DESC | INCLUDE_NAME,
264     'application': INCLUDE_NAME,
265     'document': INCLUDE_NAME,
266     'grouping': INCLUDE_DESC | INCLUDE_NAME,
267     'toolbar': INCLUDE_DESC,
268     'table': INCLUDE_DESC | INCLUDE_NAME,
269     'link': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
270     'helpballoon': NAME_FROM_SUBTREE_RULE,
271     'list': INCLUDE_DESC | INCLUDE_NAME,
272     'listitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
273     'outline': INCLUDE_DESC,
274     'outlineitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
275     'pagetab': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
276     'graphic': INCLUDE_DESC,
277     'pushbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
278     'checkbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
279     'radiobutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
280     'buttondropdown': NAME_FROM_SUBTREE_RULE,
281     'combobox': INCLUDE_DESC,
282     'droplist': INCLUDE_DESC,
283     'progressbar': INCLUDE_DESC | INCLUDE_VALUE,
284     'slider': INCLUDE_DESC | INCLUDE_VALUE,
285     'spinbutton': INCLUDE_DESC | INCLUDE_VALUE,
286     'diagram': INCLUDE_DESC,
287     'animation': INCLUDE_DESC,
288     'equation': INCLUDE_DESC,
289     'buttonmenu': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
290     'buttondropdowngrid': NAME_FROM_SUBTREE_RULE,
291     'pagetablist': INCLUDE_DESC,
292     'canvas': INCLUDE_DESC,
293     'check menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
294     'label': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
295     'password text': INCLUDE_DESC,
296     'popup menu': INCLUDE_DESC,
297     'radio menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
298     'table column header': NAME_FROM_SUBTREE_RULE,
299     'table row header': NAME_FROM_SUBTREE_RULE,
300     'tear off menu item': NAME_FROM_SUBTREE_RULE,
301     'toggle button': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
302     'parent menuitem': NAME_FROM_SUBTREE_RULE,
303     'header': INCLUDE_DESC,
304     'footer': INCLUDE_DESC,
305     'entry': INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE,
306     'caption': INCLUDE_DESC,
307     'document frame': INCLUDE_DESC,
308     'heading': INCLUDE_DESC,
309     'calendar': INCLUDE_DESC | INCLUDE_NAME,
310     'combobox list': INCLUDE_DESC,
311     'combobox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
312     'listbox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
313     'listbox rich option': NAME_FROM_SUBTREE_RULE,
314     'gridcell': NAME_FROM_SUBTREE_RULE,
315     'check rich option': NAME_FROM_SUBTREE_RULE,
316     'term': NAME_FROM_SUBTREE_RULE,
317     'definition': NAME_FROM_SUBTREE_RULE,
318     'key': NAME_FROM_SUBTREE_RULE,
319     'image map': INCLUDE_DESC,
320     'option': INCLUDE_DESC,
321     'listbox': INCLUDE_DESC,
322     'definitionlist': INCLUDE_DESC | INCLUDE_NAME},
324   objectOutputFunctions: {
325     _generateBaseOutput: function _generateBaseOutput(aAccessible, aRoleStr, aStates, aFlags) {
326       let output = [];
328       if (aFlags & INCLUDE_DESC) {
329         let desc = this._getLocalizedStates(aStates);
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, aStates, 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, aStates, aFlags) {
364       let rolestr = (aStates.ext & Ci.nsIAccessibleStates.EXT_STATE_MULTI_LINE) ?
365             'textarea' : 'entry';
366       return this.objectOutputFunctions.defaultFunc.apply(
367         this, [aAccessible, rolestr, aStates, aFlags]);
368     },
370     pagetab: function pagetab(aAccessible, aRoleStr, aStates, aFlags) {
371       let localizedRole = this._getLocalizedRole(aRoleStr);
372       let itemno = {};
373       let itemof = {};
374       aAccessible.groupPosition({}, itemof, itemno);
375       let output = [];
376       let desc = this._getLocalizedStates(aStates);
377       desc.push(
378         gStringBundle.formatStringFromName(
379           'objItemOf', [localizedRole, itemno.value, itemof.value], 3));
380       output.push(desc.join(' '));
382       this._addName(output, aAccessible, aFlags);
383       this._addLandmark(output, aAccessible);
385       return output;
386     },
388     table: function table(aAccessible, aRoleStr, aStates, aFlags) {
389       let output = [];
390       let table;
391       try {
392         table = aAccessible.QueryInterface(Ci.nsIAccessibleTable);
393       } catch (x) {
394         Logger.logException(x);
395         return output;
396       } finally {
397         // Check if it's a layout table, and bail out if true.
398         // We don't want to speak any table information for layout tables.
399         if (table.isProbablyForLayout()) {
400           return output;
401         }
402         let tableColumnInfo = this._getPluralFormString('tableColumnInfo',
403           table.columnCount);
404         let tableRowInfo = this._getPluralFormString('tableRowInfo',
405           table.rowCount);
406         output.push(gStringBundle.formatStringFromName(
407           this._getOutputName('tableInfo'), [this._getLocalizedRole(aRoleStr),
408             tableColumnInfo, tableRowInfo], 3));
409         this._addName(output, aAccessible, aFlags);
410         this._addLandmark(output, aAccessible);
411         return output;
412       }
413     }
414   }
418  * Generates speech utterances from objects, actions and state changes.
419  * An utterance is an array of strings.
421  * It should not be assumed that flattening an utterance array would create a
422  * gramatically correct sentence. For example, {@link genForObject} might
423  * return: ['graphic', 'Welcome to my home page'].
424  * Each string element in an utterance should be gramatically correct in itself.
425  * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
427  * An utterance is ordered from the least to the most important. Speaking the
428  * last string usually makes sense, but speaking the first often won't.
429  * For example {@link genForAction} might return ['button', 'clicked'] for a
430  * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
431  * not.
432  */
433 this.UtteranceGenerator = {
434   __proto__: OutputGenerator,
436   gActionMap: {
437     jump: 'jumpAction',
438     press: 'pressAction',
439     check: 'checkAction',
440     uncheck: 'uncheckAction',
441     select: 'selectAction',
442     unselect: 'unselectAction',
443     open: 'openAction',
444     close: 'closeAction',
445     switch: 'switchAction',
446     click: 'clickAction',
447     collapse: 'collapseAction',
448     expand: 'expandAction',
449     activate: 'activateAction',
450     cycle: 'cycleAction'
451   },
453   //TODO: May become more verbose in the future.
454   genForAction: function genForAction(aObject, aActionName) {
455     return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])];
456   },
458   genForLiveRegion: function genForLiveRegion(aContext, aIsHide, aModifiedText) {
459     let utterance = [];
460     if (aIsHide) {
461       utterance.push(gStringBundle.GetStringFromName('hidden'));
462     }
463     return utterance.concat(
464       aModifiedText || this.genForContext(aContext).output);
465   },
467   genForAnnouncement: function genForAnnouncement(aAnnouncement) {
468     try {
469       return [gStringBundle.GetStringFromName(aAnnouncement)];
470     } catch (x) {
471       return [aAnnouncement];
472     }
473   },
475   genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
476     switch (aTabState) {
477       case 'newtab':
478         return [gStringBundle.GetStringFromName('tabNew')];
479       case 'loading':
480         return [gStringBundle.GetStringFromName('tabLoading')];
481       case 'loaded':
482         return [aObject.name || '',
483                 gStringBundle.GetStringFromName('tabLoaded')];
484       case 'loadstopped':
485         return [gStringBundle.GetStringFromName('tabLoadStopped')];
486       case 'reload':
487         return [gStringBundle.GetStringFromName('tabReload')];
488       default:
489         return [];
490     }
491   },
493   genForEditingMode: function genForEditingMode(aIsEditing) {
494     return [gStringBundle.GetStringFromName(
495               aIsEditing ? 'editingMode' : 'navigationMode')];
496   },
498   objectOutputFunctions: {
500     __proto__: OutputGenerator.objectOutputFunctions,
502     defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
503       return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
504     },
506     heading: function heading(aAccessible, aRoleStr, aStates, aFlags) {
507       let level = {};
508       aAccessible.groupPosition(level, {}, {});
509       let utterance =
510         [gStringBundle.formatStringFromName('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, aStates, 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(gStringBundle.GetStringFromName('listStart'));
525       else if (itemno.value == itemof.value) // last item
526         utterance.push(gStringBundle.GetStringFromName('listEnd'));
528       this._addName(utterance, aAccessible, aFlags);
529       this._addLandmark(utterance, aAccessible);
531       return utterance;
532     },
534     list: function list(aAccessible, aRoleStr, aStates, aFlags) {
535       return this._getListUtterance
536         (aAccessible, aRoleStr, aFlags, aAccessible.childCount);
537     },
539     definitionlist: function definitionlist(aAccessible, aRoleStr, aStates, aFlags) {
540       return this._getListUtterance
541         (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2);
542     },
544     application: function application(aAccessible, aRoleStr, aStates, 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, aStates, aFlags]);
550       return [];
551     },
553     cell: function cell(aAccessible, aRoleStr, aStates, 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(gStringBundle.formatStringFromName(aString,
561               [aIndex + 1], 1));
562           }
563         };
564         let addExtent = function addExtent(aDesc, aExtent, aString) {
565           if (aExtent > 1) {
566             aDesc.push(gStringBundle.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 gStringBundle.GetStringFromName(this._getOutputName(aRoleStr));
610     } catch (x) {
611       return '';
612     }
613   },
615   _getLocalizedStates: function _getLocalizedStates(aStates) {
616     let stateUtterances = [];
618     if (aStates.base & Ci.nsIAccessibleStates.STATE_UNAVAILABLE) {
619       stateUtterances.push(gStringBundle.GetStringFromName('stateUnavailable'));
620     }
622     // Don't utter this in Jelly Bean, we let TalkBack do it for us there.
623     // This is because we expose the checked information on the node itself.
624     // XXX: this means the checked state is always appended to the end, regardless
625     // of the utterance ordering preference.
626     if (Utils.AndroidSdkVersion < 16 && aStates.base & Ci.nsIAccessibleStates.STATE_CHECKABLE) {
627       let stateStr = (aStates.base & Ci.nsIAccessibleStates.STATE_CHECKED) ?
628         'stateChecked' : 'stateNotChecked';
629       stateUtterances.push(gStringBundle.GetStringFromName(stateStr));
630     }
632     if (aStates.ext & Ci.nsIAccessibleStates.EXT_STATE_EXPANDABLE) {
633       let stateStr = (aStates.base & Ci.nsIAccessibleStates.STATE_EXPANDED) ?
634         'stateExpanded' : 'stateCollapsed';
635       stateUtterances.push(gStringBundle.GetStringFromName(stateStr));
636     }
638     if (aStates.base & Ci.nsIAccessibleStates.STATE_REQUIRED) {
639       stateUtterances.push(gStringBundle.GetStringFromName('stateRequired'));
640     }
642     if (aStates.base & Ci.nsIAccessibleStates.STATE_TRAVERSED) {
643       stateUtterances.push(gStringBundle.GetStringFromName('stateTraversed'));
644     }
646     if (aStates.base & Ci.nsIAccessibleStates.STATE_HASPOPUP) {
647       stateUtterances.push(gStringBundle.GetStringFromName('stateHasPopup'));
648     }
650     if (aStates.base & Ci.nsIAccessibleStates.STATE_SELECTED) {
651       stateUtterances.push(gStringBundle.GetStringFromName('stateSelected'));
652     }
654     return stateUtterances;
655   },
657   _getListUtterance: function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) {
658     let desc = [];
659     let roleStr = this._getLocalizedRole(aRoleStr);
660     if (roleStr) {
661       desc.push(roleStr);
662     }
663     desc.push(this._getPluralFormString('listItemsCount', aItemCount));
664     let utterance = [desc.join(' ')];
666     this._addName(utterance, aAccessible, aFlags);
667     this._addLandmark(utterance, aAccessible);
669     return utterance;
670   }
674 this.BrailleGenerator = {
675   __proto__: OutputGenerator,
677   genForContext: function genForContext(aContext) {
678     let output = OutputGenerator.genForContext.apply(this, arguments);
680     let acc = aContext.accessible;
681     if (acc instanceof Ci.nsIAccessibleText) {
682       output.endOffset = this.outputOrder === OUTPUT_DESC_FIRST ?
683                          output.output.join(' ').length : acc.characterCount;
684       output.startOffset = output.endOffset - acc.characterCount;
685     }
687     return output;
688   },
690   objectOutputFunctions: {
692     __proto__: OutputGenerator.objectOutputFunctions,
694     defaultFunc: function defaultFunc(aAccessible, aRoleStr, aStates, aFlags) {
695       let braille = this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
697       if (aAccessible.indexInParent === 1 &&
698           aAccessible.parent.role == Roles.LISTITEM &&
699           aAccessible.previousSibling.role == Roles.STATICTEXT) {
700         if (aAccessible.parent.parent && aAccessible.parent.parent.DOMNode &&
701             aAccessible.parent.parent.DOMNode.nodeName == 'UL') {
702           braille.unshift('*');
703         } else {
704           braille.unshift(aAccessible.previousSibling.name);
705         }
706       }
708       return braille;
709     },
711     listitem: function listitem(aAccessible, aRoleStr, aStates, aFlags) {
712       let braille = [];
714       this._addName(braille, aAccessible, aFlags);
715       this._addLandmark(braille, aAccessible);
717       return braille;
718     },
720     cell: function cell(aAccessible, aRoleStr, aStates, aFlags, aContext) {
721       let braille = [];
722       let cell = aContext.getCellInfo(aAccessible);
723       if (cell) {
724         let desc = [];
725         let addHeaders = function addHeaders(aDesc, aHeaders) {
726           if (aHeaders.length > 0) {
727             aDesc.push.apply(aDesc, aHeaders);
728           }
729         };
731         desc.push(gStringBundle.formatStringFromName(
732           this._getOutputName('cellInfo'), [cell.columnIndex + 1,
733             cell.rowIndex + 1], 2));
735         addHeaders(desc, cell.columnHeaders);
736         addHeaders(desc, cell.rowHeaders);
737         braille.push(desc.join(' '));
738       }
740       this._addName(braille, aAccessible, aFlags);
741       this._addLandmark(braille, aAccessible);
742       return braille;
743     },
745     columnheader: function columnheader() {
746       return this.objectOutputFunctions.cell.apply(this, arguments);
747     },
749     rowheader: function rowheader() {
750       return this.objectOutputFunctions.cell.apply(this, arguments);
751     },
753     statictext: function statictext(aAccessible, aRoleStr, aStates, aFlags) {
754       // Since we customize the list bullet's output, we add the static
755       // text from the first node in each listitem, so skip it here.
756       if (aAccessible.parent.role == Roles.LISTITEM) {
757         return [];
758       }
760       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
761     },
763     _useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aStates, aFlags) {
764       let braille = [];
766       let desc = this._getLocalizedStates(aStates);
767       braille.push(desc.join(' '));
769       this._addName(braille, aAccessible, aFlags);
770       this._addLandmark(braille, aAccessible);
772       return braille;
773     },
775     checkbutton: function checkbutton(aAccessible, aRoleStr, aStates, aFlags) {
776       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
777     },
779     radiobutton: function radiobutton(aAccessible, aRoleStr, aStates, aFlags) {
780       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
781     },
783     togglebutton: function radiobutton(aAccessible, aRoleStr, aStates, aFlags) {
784       return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
785     }
786   },
788   _getContextStart: function _getContextStart(aContext) {
789     if (aContext.accessible.parent.role == Roles.LINK) {
790       return [aContext.accessible.parent];
791     }
793     return [];
794   },
796   _getOutputName: function _getOutputName(aName) {
797     return OutputGenerator._getOutputName(aName) + 'Abbr';
798   },
800   _getLocalizedRole: function _getLocalizedRole(aRoleStr) {
801     try {
802       return gStringBundle.GetStringFromName(this._getOutputName(aRoleStr));
803     } catch (x) {
804       try {
805         return gStringBundle.GetStringFromName(
806           OutputGenerator._getOutputName(aRoleStr));
807       } catch (y) {
808         return '';
809       }
810     }
811   },
813   _getLocalizedStates: function _getLocalizedStates(aStates) {
814     let stateBraille = [];
816     let getCheckedState = function getCheckedState() {
817       let resultMarker = [];
818       let state = aStates.base;
819       let fill = !!(state & Ci.nsIAccessibleStates.STATE_CHECKED) ||
820                  !!(state & Ci.nsIAccessibleStates.STATE_PRESSED);
822       resultMarker.push('(');
823       resultMarker.push(fill ? 'x' : ' ');
824       resultMarker.push(')');
826       return resultMarker.join('');
827     };
829     if (aStates.base & Ci.nsIAccessibleStates.STATE_CHECKABLE) {
830       stateBraille.push(getCheckedState());
831     }
833     return stateBraille;
834   }