michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: 'use strict'; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: const INCLUDE_DESC = 0x01; michael@0: const INCLUDE_NAME = 0x02; michael@0: const INCLUDE_VALUE = 0x04; michael@0: const INCLUDE_CUSTOM = 0x08; michael@0: const NAME_FROM_SUBTREE_RULE = 0x10; michael@0: const IGNORE_EXPLICIT_NAME = 0x20; michael@0: michael@0: const OUTPUT_DESC_FIRST = 0; michael@0: const OUTPUT_DESC_LAST = 1; michael@0: michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Utils', michael@0: 'resource://gre/modules/accessibility/Utils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache', michael@0: 'resource://gre/modules/accessibility/Utils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Logger', michael@0: 'resource://gre/modules/accessibility/Utils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm', michael@0: 'resource://gre/modules/PluralForm.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Roles', michael@0: 'resource://gre/modules/accessibility/Constants.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'States', michael@0: 'resource://gre/modules/accessibility/Constants.jsm'); michael@0: michael@0: this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator']; michael@0: michael@0: this.OutputGenerator = { michael@0: michael@0: defaultOutputOrder: OUTPUT_DESC_LAST, michael@0: michael@0: /** michael@0: * Generates output for a PivotContext. michael@0: * @param {PivotContext} aContext object that generates and caches michael@0: * context information for a given accessible and its relationship with michael@0: * another accessible. michael@0: * @return {Object} An object that neccessarily has an output property which michael@0: * is an array of strings. Depending on the utterance order, michael@0: * the strings describe the context for an accessible object either michael@0: * starting from the accessible's ancestry or accessible's subtree. michael@0: * The object may also have properties specific to the type of output michael@0: * generated. michael@0: */ michael@0: genForContext: function genForContext(aContext) { michael@0: let output = []; michael@0: let self = this; michael@0: let addOutput = function addOutput(aAccessible) { michael@0: output.push.apply(output, self.genForObject(aAccessible, aContext)); michael@0: }; michael@0: let ignoreSubtree = function ignoreSubtree(aAccessible) { michael@0: let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role); michael@0: let nameRule = self.roleRuleMap[roleString] || 0; michael@0: // Ignore subtree if the name is explicit and the role's name rule is the michael@0: // NAME_FROM_SUBTREE_RULE. michael@0: return (((nameRule & INCLUDE_VALUE) && aAccessible.value) || michael@0: ((nameRule & NAME_FROM_SUBTREE_RULE) && michael@0: (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' && michael@0: !(nameRule & IGNORE_EXPLICIT_NAME)))); michael@0: }; michael@0: michael@0: let contextStart = this._getContextStart(aContext); michael@0: michael@0: if (this.outputOrder === OUTPUT_DESC_FIRST) { michael@0: contextStart.forEach(addOutput); michael@0: addOutput(aContext.accessible); michael@0: [addOutput(node) for michael@0: (node of aContext.subtreeGenerator(true, ignoreSubtree))]; michael@0: } else { michael@0: [addOutput(node) for michael@0: (node of aContext.subtreeGenerator(false, ignoreSubtree))]; michael@0: addOutput(aContext.accessible); michael@0: contextStart.reverse().forEach(addOutput); michael@0: } michael@0: michael@0: // Clean up the white space. michael@0: let trimmed; michael@0: output = [trimmed for (word of output) if (trimmed = word.trim())]; michael@0: return {output: output}; michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Generates output for an object. michael@0: * @param {nsIAccessible} aAccessible accessible object to generate output michael@0: * for. michael@0: * @param {PivotContext} aContext object that generates and caches michael@0: * context information for a given accessible and its relationship with michael@0: * another accessible. michael@0: * @return {Array} Two string array. The first string describes the object michael@0: * and its state. The second string is the object's name. Whether the michael@0: * object's description or it's role is included is determined by michael@0: * {@link roleRuleMap}. michael@0: */ michael@0: genForObject: function genForObject(aAccessible, aContext) { michael@0: let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role); michael@0: let func = this.objectOutputFunctions[ michael@0: OutputGenerator._getOutputName(roleString)] || michael@0: this.objectOutputFunctions.defaultFunc; michael@0: michael@0: let flags = this.roleRuleMap[roleString] || 0; michael@0: michael@0: if (aAccessible.childCount == 0) michael@0: flags |= INCLUDE_NAME; michael@0: michael@0: return func.apply(this, [aAccessible, roleString, michael@0: Utils.getState(aAccessible), flags, aContext]); michael@0: }, michael@0: michael@0: /** michael@0: * Generates output for an action performed. michael@0: * @param {nsIAccessible} aAccessible accessible object that the action was michael@0: * invoked in. michael@0: * @param {string} aActionName the name of the action, one of the keys in michael@0: * {@link gActionMap}. michael@0: * @return {Array} A one string array with the action. michael@0: */ michael@0: genForAction: function genForAction(aObject, aActionName) {}, michael@0: michael@0: /** michael@0: * Generates output for an announcement. Basically attempts to localize michael@0: * the announcement string. michael@0: * @param {string} aAnnouncement unlocalized announcement. michael@0: * @return {Array} A one string array with the announcement. michael@0: */ michael@0: genForAnnouncement: function genForAnnouncement(aAnnouncement) {}, michael@0: michael@0: /** michael@0: * Generates output for a tab state change. michael@0: * @param {nsIAccessible} aAccessible accessible object of the tab's attached michael@0: * document. michael@0: * @param {string} aTabState the tab state name, see michael@0: * {@link Presenter.tabStateChanged}. michael@0: * @return {Array} The tab state utterace. michael@0: */ michael@0: genForTabStateChange: function genForTabStateChange(aObject, aTabState) {}, michael@0: michael@0: /** michael@0: * Generates output for announcing entering and leaving editing mode. michael@0: * @param {aIsEditing} boolean true if we are in editing mode michael@0: * @return {Array} The mode utterance michael@0: */ michael@0: genForEditingMode: function genForEditingMode(aIsEditing) {}, michael@0: michael@0: _getContextStart: function getContextStart(aContext) {}, michael@0: michael@0: _addName: function _addName(aOutput, aAccessible, aFlags) { michael@0: let name; michael@0: if ((Utils.getAttributes(aAccessible)['explicit-name'] === 'true' && michael@0: !(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) { michael@0: name = aAccessible.name; michael@0: } michael@0: michael@0: let description = aAccessible.description; michael@0: if (description) { michael@0: // Compare against the calculated name unconditionally, regardless of name rule, michael@0: // so we can make sure we don't speak duplicated descriptions michael@0: let tmpName = name || aAccessible.name; michael@0: if (tmpName && (description !== tmpName)) { michael@0: name = name || ''; michael@0: name = this.outputOrder === OUTPUT_DESC_FIRST ? michael@0: description + ' - ' + name : michael@0: name + ' - ' + description; michael@0: } michael@0: } michael@0: michael@0: if (name) { michael@0: aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? michael@0: 'push' : 'unshift'](name); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds a landmark role to the output if available. michael@0: * @param {Array} aOutput Output array. michael@0: * @param {nsIAccessible} aAccessible current accessible object. michael@0: */ michael@0: _addLandmark: function _addLandmark(aOutput, aAccessible) { michael@0: let landmarkName = Utils.getLandmarkName(aAccessible); michael@0: if (!landmarkName) { michael@0: return; michael@0: } michael@0: michael@0: let landmark = Utils.stringBundle.GetStringFromName(landmarkName); michael@0: if (!landmark) { michael@0: return; michael@0: } michael@0: michael@0: aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push']( michael@0: landmark); michael@0: }, michael@0: michael@0: /** michael@0: * Adds an entry type attribute to the description if available. michael@0: * @param {Array} aDesc Description array. michael@0: * @param {nsIAccessible} aAccessible current accessible object. michael@0: * @param {String} aRoleStr aAccessible's role string. michael@0: */ michael@0: _addType: function _addType(aDesc, aAccessible, aRoleStr) { michael@0: if (aRoleStr !== 'entry') { michael@0: return; michael@0: } michael@0: michael@0: let typeName = Utils.getAttributes(aAccessible)['text-input-type']; michael@0: // Ignore the the input type="text" case. michael@0: if (!typeName || typeName === 'text') { michael@0: return; michael@0: } michael@0: typeName = 'textInputType_' + typeName; michael@0: try { michael@0: aDesc.push(Utils.stringBundle.GetStringFromName(typeName)); michael@0: } catch (x) { michael@0: Logger.warning('Failed to get a string from a bundle for', typeName); michael@0: } michael@0: }, michael@0: michael@0: get outputOrder() { michael@0: if (!this._utteranceOrder) { michael@0: this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance'); michael@0: } michael@0: return typeof this._utteranceOrder.value === 'number' ? michael@0: this._utteranceOrder.value : this.defaultOutputOrder; michael@0: }, michael@0: michael@0: _getOutputName: function _getOutputName(aName) { michael@0: return aName.replace(' ', ''); michael@0: }, michael@0: michael@0: _getLocalizedRole: function _getLocalizedRole(aRoleStr) {}, michael@0: michael@0: _getLocalizedState: function _getLocalizedState(aState) {}, michael@0: michael@0: _getPluralFormString: function _getPluralFormString(aString, aCount) { michael@0: let str = Utils.stringBundle.GetStringFromName(this._getOutputName(aString)); michael@0: str = PluralForm.get(aCount, str); michael@0: return str.replace('#1', aCount); michael@0: }, michael@0: michael@0: roleRuleMap: { michael@0: 'menubar': INCLUDE_DESC, michael@0: 'scrollbar': INCLUDE_DESC, michael@0: 'grip': INCLUDE_DESC, michael@0: 'alert': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'menupopup': INCLUDE_DESC, michael@0: 'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'columnheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'rowheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'column': NAME_FROM_SUBTREE_RULE, michael@0: 'row': NAME_FROM_SUBTREE_RULE, michael@0: 'cell': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'application': INCLUDE_NAME, michael@0: 'document': INCLUDE_NAME, michael@0: 'grouping': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'toolbar': INCLUDE_DESC, michael@0: 'table': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'link': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'helpballoon': NAME_FROM_SUBTREE_RULE, michael@0: 'list': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'listitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'outline': INCLUDE_DESC, michael@0: 'outlineitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'pagetab': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'graphic': INCLUDE_DESC, michael@0: 'pushbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'checkbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'radiobutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'buttondropdown': NAME_FROM_SUBTREE_RULE, michael@0: 'combobox': INCLUDE_DESC, michael@0: 'droplist': INCLUDE_DESC, michael@0: 'progressbar': INCLUDE_DESC | INCLUDE_VALUE, michael@0: 'slider': INCLUDE_DESC | INCLUDE_VALUE, michael@0: 'spinbutton': INCLUDE_DESC | INCLUDE_VALUE, michael@0: 'diagram': INCLUDE_DESC, michael@0: 'animation': INCLUDE_DESC, michael@0: 'equation': INCLUDE_DESC, michael@0: 'buttonmenu': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'buttondropdowngrid': NAME_FROM_SUBTREE_RULE, michael@0: 'pagetablist': INCLUDE_DESC, michael@0: 'canvas': INCLUDE_DESC, michael@0: 'check menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'label': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'password text': INCLUDE_DESC, michael@0: 'popup menu': INCLUDE_DESC, michael@0: 'radio menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'table column header': NAME_FROM_SUBTREE_RULE, michael@0: 'table row header': NAME_FROM_SUBTREE_RULE, michael@0: 'tear off menu item': NAME_FROM_SUBTREE_RULE, michael@0: 'toggle button': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'parent menuitem': NAME_FROM_SUBTREE_RULE, michael@0: 'header': INCLUDE_DESC, michael@0: 'footer': INCLUDE_DESC, michael@0: 'entry': INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE, michael@0: 'caption': INCLUDE_DESC, michael@0: 'document frame': INCLUDE_DESC, michael@0: 'heading': INCLUDE_DESC, michael@0: 'calendar': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'combobox list': INCLUDE_DESC, michael@0: 'combobox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'listbox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE, michael@0: 'listbox rich option': NAME_FROM_SUBTREE_RULE, michael@0: 'gridcell': NAME_FROM_SUBTREE_RULE, michael@0: 'check rich option': NAME_FROM_SUBTREE_RULE, michael@0: 'term': NAME_FROM_SUBTREE_RULE, michael@0: 'definition': NAME_FROM_SUBTREE_RULE, michael@0: 'key': NAME_FROM_SUBTREE_RULE, michael@0: 'image map': INCLUDE_DESC, michael@0: 'option': INCLUDE_DESC, michael@0: 'listbox': INCLUDE_DESC, michael@0: 'definitionlist': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'dialog': INCLUDE_DESC | INCLUDE_NAME, michael@0: 'chrome window': IGNORE_EXPLICIT_NAME, michael@0: 'app root': IGNORE_EXPLICIT_NAME }, michael@0: michael@0: objectOutputFunctions: { michael@0: _generateBaseOutput: function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) { michael@0: let output = []; michael@0: michael@0: if (aFlags & INCLUDE_DESC) { michael@0: let desc = this._getLocalizedState(aState); michael@0: let roleStr = this._getLocalizedRole(aRoleStr); michael@0: if (roleStr) { michael@0: this._addType(desc, aAccessible, aRoleStr); michael@0: desc.push(roleStr); michael@0: } michael@0: output.push(desc.join(' ')); michael@0: } michael@0: michael@0: if (aFlags & INCLUDE_VALUE) { michael@0: let value = aAccessible.value; michael@0: if (value) { michael@0: output[this.outputOrder === OUTPUT_DESC_FIRST ? michael@0: 'push' : 'unshift'](value); michael@0: } michael@0: } michael@0: michael@0: this._addName(output, aAccessible, aFlags); michael@0: this._addLandmark(output, aAccessible); michael@0: michael@0: return output; michael@0: }, michael@0: michael@0: label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) { michael@0: if (aContext.isNestedControl || michael@0: aContext.accessible == Utils.getEmbeddedControl(aAccessible)) { michael@0: // If we are on a nested control, or a nesting label, michael@0: // we don't need the context. michael@0: return []; michael@0: } michael@0: michael@0: return this.objectOutputFunctions.defaultFunc.apply(this, arguments); michael@0: }, michael@0: michael@0: entry: function entry(aAccessible, aRoleStr, aState, aFlags) { michael@0: let rolestr = aState.contains(States.MULTI_LINE) ? 'textarea' : 'entry'; michael@0: return this.objectOutputFunctions.defaultFunc.apply( michael@0: this, [aAccessible, rolestr, aState, aFlags]); michael@0: }, michael@0: michael@0: pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) { michael@0: let localizedRole = this._getLocalizedRole(aRoleStr); michael@0: let itemno = {}; michael@0: let itemof = {}; michael@0: aAccessible.groupPosition({}, itemof, itemno); michael@0: let output = []; michael@0: let desc = this._getLocalizedState(aState); michael@0: desc.push( michael@0: Utils.stringBundle.formatStringFromName( michael@0: 'objItemOf', [localizedRole, itemno.value, itemof.value], 3)); michael@0: output.push(desc.join(' ')); michael@0: michael@0: this._addName(output, aAccessible, aFlags); michael@0: this._addLandmark(output, aAccessible); michael@0: michael@0: return output; michael@0: }, michael@0: michael@0: table: function table(aAccessible, aRoleStr, aState, aFlags) { michael@0: let output = []; michael@0: let table; michael@0: try { michael@0: table = aAccessible.QueryInterface(Ci.nsIAccessibleTable); michael@0: } catch (x) { michael@0: Logger.logException(x); michael@0: return output; michael@0: } finally { michael@0: // Check if it's a layout table, and bail out if true. michael@0: // We don't want to speak any table information for layout tables. michael@0: if (table.isProbablyForLayout()) { michael@0: return output; michael@0: } michael@0: let tableColumnInfo = this._getPluralFormString('tableColumnInfo', michael@0: table.columnCount); michael@0: let tableRowInfo = this._getPluralFormString('tableRowInfo', michael@0: table.rowCount); michael@0: output.push(Utils.stringBundle.formatStringFromName( michael@0: this._getOutputName('tableInfo'), [this._getLocalizedRole(aRoleStr), michael@0: tableColumnInfo, tableRowInfo], 3)); michael@0: this._addName(output, aAccessible, aFlags); michael@0: this._addLandmark(output, aAccessible); michael@0: return output; michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Generates speech utterances from objects, actions and state changes. michael@0: * An utterance is an array of strings. michael@0: * michael@0: * It should not be assumed that flattening an utterance array would create a michael@0: * gramatically correct sentence. For example, {@link genForObject} might michael@0: * return: ['graphic', 'Welcome to my home page']. michael@0: * Each string element in an utterance should be gramatically correct in itself. michael@0: * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama']. michael@0: * michael@0: * An utterance is ordered from the least to the most important. Speaking the michael@0: * last string usually makes sense, but speaking the first often won't. michael@0: * For example {@link genForAction} might return ['button', 'clicked'] for a michael@0: * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does michael@0: * not. michael@0: */ michael@0: this.UtteranceGenerator = { michael@0: __proto__: OutputGenerator, michael@0: michael@0: gActionMap: { michael@0: jump: 'jumpAction', michael@0: press: 'pressAction', michael@0: check: 'checkAction', michael@0: uncheck: 'uncheckAction', michael@0: select: 'selectAction', michael@0: unselect: 'unselectAction', michael@0: open: 'openAction', michael@0: close: 'closeAction', michael@0: switch: 'switchAction', michael@0: click: 'clickAction', michael@0: collapse: 'collapseAction', michael@0: expand: 'expandAction', michael@0: activate: 'activateAction', michael@0: cycle: 'cycleAction' michael@0: }, michael@0: michael@0: //TODO: May become more verbose in the future. michael@0: genForAction: function genForAction(aObject, aActionName) { michael@0: return [Utils.stringBundle.GetStringFromName(this.gActionMap[aActionName])]; michael@0: }, michael@0: michael@0: genForLiveRegion: function genForLiveRegion(aContext, aIsHide, aModifiedText) { michael@0: let utterance = []; michael@0: if (aIsHide) { michael@0: utterance.push(Utils.stringBundle.GetStringFromName('hidden')); michael@0: } michael@0: return utterance.concat( michael@0: aModifiedText || this.genForContext(aContext).output); michael@0: }, michael@0: michael@0: genForAnnouncement: function genForAnnouncement(aAnnouncement) { michael@0: try { michael@0: return [Utils.stringBundle.GetStringFromName(aAnnouncement)]; michael@0: } catch (x) { michael@0: return [aAnnouncement]; michael@0: } michael@0: }, michael@0: michael@0: genForTabStateChange: function genForTabStateChange(aObject, aTabState) { michael@0: switch (aTabState) { michael@0: case 'newtab': michael@0: return [Utils.stringBundle.GetStringFromName('tabNew')]; michael@0: case 'loading': michael@0: return [Utils.stringBundle.GetStringFromName('tabLoading')]; michael@0: case 'loaded': michael@0: return [aObject.name || '', michael@0: Utils.stringBundle.GetStringFromName('tabLoaded')]; michael@0: case 'loadstopped': michael@0: return [Utils.stringBundle.GetStringFromName('tabLoadStopped')]; michael@0: case 'reload': michael@0: return [Utils.stringBundle.GetStringFromName('tabReload')]; michael@0: default: michael@0: return []; michael@0: } michael@0: }, michael@0: michael@0: genForEditingMode: function genForEditingMode(aIsEditing) { michael@0: return [Utils.stringBundle.GetStringFromName( michael@0: aIsEditing ? 'editingMode' : 'navigationMode')]; michael@0: }, michael@0: michael@0: objectOutputFunctions: { michael@0: michael@0: __proto__: OutputGenerator.objectOutputFunctions, michael@0: michael@0: defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) { michael@0: return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments); michael@0: }, michael@0: michael@0: heading: function heading(aAccessible, aRoleStr, aState, aFlags) { michael@0: let level = {}; michael@0: aAccessible.groupPosition(level, {}, {}); michael@0: let utterance = michael@0: [Utils.stringBundle.formatStringFromName( michael@0: 'headingLevel', [level.value], 1)]; michael@0: michael@0: this._addName(utterance, aAccessible, aFlags); michael@0: this._addLandmark(utterance, aAccessible); michael@0: michael@0: return utterance; michael@0: }, michael@0: michael@0: listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) { michael@0: let itemno = {}; michael@0: let itemof = {}; michael@0: aAccessible.groupPosition({}, itemof, itemno); michael@0: let utterance = []; michael@0: if (itemno.value == 1) // Start of list michael@0: utterance.push(Utils.stringBundle.GetStringFromName('listStart')); michael@0: else if (itemno.value == itemof.value) // last item michael@0: utterance.push(Utils.stringBundle.GetStringFromName('listEnd')); michael@0: michael@0: this._addName(utterance, aAccessible, aFlags); michael@0: this._addLandmark(utterance, aAccessible); michael@0: michael@0: return utterance; michael@0: }, michael@0: michael@0: list: function list(aAccessible, aRoleStr, aState, aFlags) { michael@0: return this._getListUtterance michael@0: (aAccessible, aRoleStr, aFlags, aAccessible.childCount); michael@0: }, michael@0: michael@0: definitionlist: function definitionlist(aAccessible, aRoleStr, aState, aFlags) { michael@0: return this._getListUtterance michael@0: (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2); michael@0: }, michael@0: michael@0: application: function application(aAccessible, aRoleStr, aState, aFlags) { michael@0: // Don't utter location of applications, it gets tiring. michael@0: if (aAccessible.name != aAccessible.DOMNode.location) michael@0: return this.objectOutputFunctions.defaultFunc.apply(this, michael@0: [aAccessible, aRoleStr, aState, aFlags]); michael@0: michael@0: return []; michael@0: }, michael@0: michael@0: cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) { michael@0: let utterance = []; michael@0: let cell = aContext.getCellInfo(aAccessible); michael@0: if (cell) { michael@0: let desc = []; michael@0: let addCellChanged = function addCellChanged(aDesc, aChanged, aString, aIndex) { michael@0: if (aChanged) { michael@0: aDesc.push(Utils.stringBundle.formatStringFromName(aString, michael@0: [aIndex + 1], 1)); michael@0: } michael@0: }; michael@0: let addExtent = function addExtent(aDesc, aExtent, aString) { michael@0: if (aExtent > 1) { michael@0: aDesc.push(Utils.stringBundle.formatStringFromName(aString, michael@0: [aExtent], 1)); michael@0: } michael@0: }; michael@0: let addHeaders = function addHeaders(aDesc, aHeaders) { michael@0: if (aHeaders.length > 0) { michael@0: aDesc.push.apply(aDesc, aHeaders); michael@0: } michael@0: }; michael@0: michael@0: addCellChanged(desc, cell.columnChanged, 'columnInfo', cell.columnIndex); michael@0: addCellChanged(desc, cell.rowChanged, 'rowInfo', cell.rowIndex); michael@0: michael@0: addExtent(desc, cell.columnExtent, 'spansColumns'); michael@0: addExtent(desc, cell.rowExtent, 'spansRows'); michael@0: michael@0: addHeaders(desc, cell.columnHeaders); michael@0: addHeaders(desc, cell.rowHeaders); michael@0: michael@0: utterance.push(desc.join(' ')); michael@0: } michael@0: michael@0: this._addName(utterance, aAccessible, aFlags); michael@0: this._addLandmark(utterance, aAccessible); michael@0: michael@0: return utterance; michael@0: }, michael@0: michael@0: columnheader: function columnheader() { michael@0: return this.objectOutputFunctions.cell.apply(this, arguments); michael@0: }, michael@0: michael@0: rowheader: function rowheader() { michael@0: return this.objectOutputFunctions.cell.apply(this, arguments); michael@0: } michael@0: }, michael@0: michael@0: _getContextStart: function _getContextStart(aContext) { michael@0: return aContext.newAncestry; michael@0: }, michael@0: michael@0: _getLocalizedRole: function _getLocalizedRole(aRoleStr) { michael@0: try { michael@0: return Utils.stringBundle.GetStringFromName( michael@0: this._getOutputName(aRoleStr)); michael@0: } catch (x) { michael@0: return ''; michael@0: } michael@0: }, michael@0: michael@0: _getLocalizedState: function _getLocalizedState(aState) { michael@0: let stateUtterances = []; michael@0: michael@0: if (aState.contains(States.UNAVAILABLE)) { michael@0: stateUtterances.push( michael@0: Utils.stringBundle.GetStringFromName('stateUnavailable')); michael@0: } michael@0: michael@0: // Don't utter this in Jelly Bean, we let TalkBack do it for us there. michael@0: // This is because we expose the checked information on the node itself. michael@0: // XXX: this means the checked state is always appended to the end, regardless michael@0: // of the utterance ordering preference. michael@0: if ((Utils.AndroidSdkVersion < 16 || Utils.MozBuildApp === 'browser') && michael@0: aState.contains(States.CHECKABLE)) { michael@0: let statetr = aState.contains(States.CHECKED) ? michael@0: 'stateChecked' : 'stateNotChecked'; michael@0: stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr)); michael@0: } michael@0: michael@0: if (aState.contains(States.PRESSED)) { michael@0: stateUtterances.push( michael@0: Utils.stringBundle.GetStringFromName('statePressed')); michael@0: } michael@0: michael@0: if (aState.contains(States.EXPANDABLE)) { michael@0: let statetr = aState.contains(States.EXPANDED) ? michael@0: 'stateExpanded' : 'stateCollapsed'; michael@0: stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr)); michael@0: } michael@0: michael@0: if (aState.contains(States.REQUIRED)) { michael@0: stateUtterances.push( michael@0: Utils.stringBundle.GetStringFromName('stateRequired')); michael@0: } michael@0: michael@0: if (aState.contains(States.TRAVERSED)) { michael@0: stateUtterances.push( michael@0: Utils.stringBundle.GetStringFromName('stateTraversed')); michael@0: } michael@0: michael@0: if (aState.contains(States.HASPOPUP)) { michael@0: stateUtterances.push( michael@0: Utils.stringBundle.GetStringFromName('stateHasPopup')); michael@0: } michael@0: michael@0: if (aState.contains(States.SELECTED)) { michael@0: stateUtterances.push( michael@0: Utils.stringBundle.GetStringFromName('stateSelected')); michael@0: } michael@0: michael@0: return stateUtterances; michael@0: }, michael@0: michael@0: _getListUtterance: function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) { michael@0: let desc = []; michael@0: let roleStr = this._getLocalizedRole(aRoleStr); michael@0: if (roleStr) { michael@0: desc.push(roleStr); michael@0: } michael@0: desc.push(this._getPluralFormString('listItemsCount', aItemCount)); michael@0: let utterance = [desc.join(' ')]; michael@0: michael@0: this._addName(utterance, aAccessible, aFlags); michael@0: this._addLandmark(utterance, aAccessible); michael@0: michael@0: return utterance; michael@0: } michael@0: }; michael@0: michael@0: michael@0: this.BrailleGenerator = { michael@0: __proto__: OutputGenerator, michael@0: michael@0: genForContext: function genForContext(aContext) { michael@0: let output = OutputGenerator.genForContext.apply(this, arguments); michael@0: michael@0: let acc = aContext.accessible; michael@0: michael@0: // add the static text indicating a list item; do this for both listitems or michael@0: // direct first children of listitems, because these are both common browsing michael@0: // scenarios michael@0: let addListitemIndicator = function addListitemIndicator(indicator = '*') { michael@0: output.output.unshift(indicator); michael@0: }; michael@0: michael@0: if (acc.indexInParent === 1 && michael@0: acc.parent.role == Roles.LISTITEM && michael@0: acc.previousSibling.role == Roles.STATICTEXT) { michael@0: if (acc.parent.parent && acc.parent.parent.DOMNode && michael@0: acc.parent.parent.DOMNode.nodeName == 'UL') { michael@0: addListitemIndicator(); michael@0: } else { michael@0: addListitemIndicator(acc.previousSibling.name.trim()); michael@0: } michael@0: } else if (acc.role == Roles.LISTITEM && acc.firstChild && michael@0: acc.firstChild.role == Roles.STATICTEXT) { michael@0: if (acc.parent.DOMNode.nodeName == 'UL') { michael@0: addListitemIndicator(); michael@0: } else { michael@0: addListitemIndicator(acc.firstChild.name.trim()); michael@0: } michael@0: } michael@0: michael@0: if (acc instanceof Ci.nsIAccessibleText) { michael@0: output.endOffset = this.outputOrder === OUTPUT_DESC_FIRST ? michael@0: output.output.join(' ').length : acc.characterCount; michael@0: output.startOffset = output.endOffset - acc.characterCount; michael@0: } michael@0: michael@0: return output; michael@0: }, michael@0: michael@0: objectOutputFunctions: { michael@0: michael@0: __proto__: OutputGenerator.objectOutputFunctions, michael@0: michael@0: defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) { michael@0: return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments); michael@0: }, michael@0: michael@0: listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) { michael@0: let braille = []; michael@0: michael@0: this._addName(braille, aAccessible, aFlags); michael@0: this._addLandmark(braille, aAccessible); michael@0: michael@0: return braille; michael@0: }, michael@0: michael@0: cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) { michael@0: let braille = []; michael@0: let cell = aContext.getCellInfo(aAccessible); michael@0: if (cell) { michael@0: let desc = []; michael@0: let addHeaders = function addHeaders(aDesc, aHeaders) { michael@0: if (aHeaders.length > 0) { michael@0: aDesc.push.apply(aDesc, aHeaders); michael@0: } michael@0: }; michael@0: michael@0: desc.push(Utils.stringBundle.formatStringFromName( michael@0: this._getOutputName('cellInfo'), [cell.columnIndex + 1, michael@0: cell.rowIndex + 1], 2)); michael@0: michael@0: addHeaders(desc, cell.columnHeaders); michael@0: addHeaders(desc, cell.rowHeaders); michael@0: braille.push(desc.join(' ')); michael@0: } michael@0: michael@0: this._addName(braille, aAccessible, aFlags); michael@0: this._addLandmark(braille, aAccessible); michael@0: return braille; michael@0: }, michael@0: michael@0: columnheader: function columnheader() { michael@0: return this.objectOutputFunctions.cell.apply(this, arguments); michael@0: }, michael@0: michael@0: rowheader: function rowheader() { michael@0: return this.objectOutputFunctions.cell.apply(this, arguments); michael@0: }, michael@0: michael@0: statictext: function statictext(aAccessible, aRoleStr, aState, aFlags) { michael@0: // Since we customize the list bullet's output, we add the static michael@0: // text from the first node in each listitem, so skip it here. michael@0: if (aAccessible.parent.role == Roles.LISTITEM) { michael@0: return []; michael@0: } michael@0: michael@0: return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); michael@0: }, michael@0: michael@0: _useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aState, aFlags) { michael@0: let braille = []; michael@0: michael@0: let desc = this._getLocalizedState(aState, aAccessible.role); michael@0: braille.push(desc.join(' ')); michael@0: michael@0: this._addName(braille, aAccessible, aFlags); michael@0: this._addLandmark(braille, aAccessible); michael@0: michael@0: return braille; michael@0: }, michael@0: michael@0: checkbutton: function checkbutton(aAccessible, aRoleStr, aState, aFlags) { michael@0: return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); michael@0: }, michael@0: michael@0: radiobutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) { michael@0: return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); michael@0: }, michael@0: michael@0: togglebutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) { michael@0: return this.objectOutputFunctions._useStateNotRole.apply(this, arguments); michael@0: } michael@0: }, michael@0: michael@0: _getContextStart: function _getContextStart(aContext) { michael@0: if (aContext.accessible.parent.role == Roles.LINK) { michael@0: return [aContext.accessible.parent]; michael@0: } michael@0: michael@0: return []; michael@0: }, michael@0: michael@0: _getOutputName: function _getOutputName(aName) { michael@0: return OutputGenerator._getOutputName(aName) + 'Abbr'; michael@0: }, michael@0: michael@0: _getLocalizedRole: function _getLocalizedRole(aRoleStr) { michael@0: try { michael@0: return Utils.stringBundle.GetStringFromName( michael@0: this._getOutputName(aRoleStr)); michael@0: } catch (x) { michael@0: try { michael@0: return Utils.stringBundle.GetStringFromName( michael@0: OutputGenerator._getOutputName(aRoleStr)); michael@0: } catch (y) { michael@0: return ''; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _getLocalizedState: function _getLocalizedState(aState, aRole) { michael@0: let stateBraille = []; michael@0: michael@0: let getResultMarker = function getResultMarker(aMarker) { michael@0: // aMarker is a simple boolean. michael@0: let resultMarker = []; michael@0: resultMarker.push('('); michael@0: resultMarker.push(aMarker ? 'x' : ' '); michael@0: resultMarker.push(')'); michael@0: michael@0: return resultMarker.join(''); michael@0: }; michael@0: michael@0: if (aState.contains(States.CHECKABLE)) { michael@0: stateBraille.push(getResultMarker(aState.contains(States.CHECKED))); michael@0: } michael@0: michael@0: if (aRole === Roles.TOGGLE_BUTTON) { michael@0: stateBraille.push(getResultMarker(aState.contains(States.PRESSED))); michael@0: } michael@0: michael@0: return stateBraille; michael@0: } michael@0: michael@0: };