accessible/src/jsat/OutputGenerator.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/accessible/src/jsat/OutputGenerator.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,863 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +'use strict';
     1.9 +
    1.10 +const Cc = Components.classes;
    1.11 +const Ci = Components.interfaces;
    1.12 +const Cu = Components.utils;
    1.13 +const Cr = Components.results;
    1.14 +
    1.15 +const INCLUDE_DESC = 0x01;
    1.16 +const INCLUDE_NAME = 0x02;
    1.17 +const INCLUDE_VALUE = 0x04;
    1.18 +const INCLUDE_CUSTOM = 0x08;
    1.19 +const NAME_FROM_SUBTREE_RULE = 0x10;
    1.20 +const IGNORE_EXPLICIT_NAME = 0x20;
    1.21 +
    1.22 +const OUTPUT_DESC_FIRST = 0;
    1.23 +const OUTPUT_DESC_LAST = 1;
    1.24 +
    1.25 +Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
    1.27 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, 'PrefCache',
    1.29 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.30 +XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
    1.31 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.32 +XPCOMUtils.defineLazyModuleGetter(this, 'PluralForm',
    1.33 +  'resource://gre/modules/PluralForm.jsm');
    1.34 +XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
    1.35 +  'resource://gre/modules/accessibility/Constants.jsm');
    1.36 +XPCOMUtils.defineLazyModuleGetter(this, 'States',
    1.37 +  'resource://gre/modules/accessibility/Constants.jsm');
    1.38 +
    1.39 +this.EXPORTED_SYMBOLS = ['UtteranceGenerator', 'BrailleGenerator'];
    1.40 +
    1.41 +this.OutputGenerator = {
    1.42 +
    1.43 +  defaultOutputOrder: OUTPUT_DESC_LAST,
    1.44 +
    1.45 +  /**
    1.46 +   * Generates output for a PivotContext.
    1.47 +   * @param {PivotContext} aContext object that generates and caches
    1.48 +   *    context information for a given accessible and its relationship with
    1.49 +   *    another accessible.
    1.50 +   * @return {Object} An object that neccessarily has an output property which
    1.51 +   *    is an array of strings. Depending on the utterance order,
    1.52 +   *    the strings describe the context for an accessible object either
    1.53 +   *    starting from the accessible's ancestry or accessible's subtree.
    1.54 +   *    The object may also have properties specific to the type of output
    1.55 +   *    generated.
    1.56 +   */
    1.57 +  genForContext: function genForContext(aContext) {
    1.58 +    let output = [];
    1.59 +    let self = this;
    1.60 +    let addOutput = function addOutput(aAccessible) {
    1.61 +      output.push.apply(output, self.genForObject(aAccessible, aContext));
    1.62 +    };
    1.63 +    let ignoreSubtree = function ignoreSubtree(aAccessible) {
    1.64 +      let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
    1.65 +      let nameRule = self.roleRuleMap[roleString] || 0;
    1.66 +      // Ignore subtree if the name is explicit and the role's name rule is the
    1.67 +      // NAME_FROM_SUBTREE_RULE.
    1.68 +      return (((nameRule & INCLUDE_VALUE) && aAccessible.value) ||
    1.69 +              ((nameRule & NAME_FROM_SUBTREE_RULE) &&
    1.70 +               (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' &&
    1.71 +               !(nameRule & IGNORE_EXPLICIT_NAME))));
    1.72 +    };
    1.73 +
    1.74 +    let contextStart = this._getContextStart(aContext);
    1.75 +
    1.76 +    if (this.outputOrder === OUTPUT_DESC_FIRST) {
    1.77 +      contextStart.forEach(addOutput);
    1.78 +      addOutput(aContext.accessible);
    1.79 +      [addOutput(node) for
    1.80 +        (node of aContext.subtreeGenerator(true, ignoreSubtree))];
    1.81 +    } else {
    1.82 +      [addOutput(node) for
    1.83 +        (node of aContext.subtreeGenerator(false, ignoreSubtree))];
    1.84 +      addOutput(aContext.accessible);
    1.85 +      contextStart.reverse().forEach(addOutput);
    1.86 +    }
    1.87 +
    1.88 +    // Clean up the white space.
    1.89 +    let trimmed;
    1.90 +    output = [trimmed for (word of output) if (trimmed = word.trim())];
    1.91 +    return {output: output};
    1.92 +  },
    1.93 +
    1.94 +
    1.95 +  /**
    1.96 +   * Generates output for an object.
    1.97 +   * @param {nsIAccessible} aAccessible accessible object to generate output
    1.98 +   *    for.
    1.99 +   * @param {PivotContext} aContext object that generates and caches
   1.100 +   *    context information for a given accessible and its relationship with
   1.101 +   *    another accessible.
   1.102 +   * @return {Array} Two string array. The first string describes the object
   1.103 +   *    and its state. The second string is the object's name. Whether the
   1.104 +   *    object's description or it's role is included is determined by
   1.105 +   *    {@link roleRuleMap}.
   1.106 +   */
   1.107 +  genForObject: function genForObject(aAccessible, aContext) {
   1.108 +    let roleString = Utils.AccRetrieval.getStringRole(aAccessible.role);
   1.109 +    let func = this.objectOutputFunctions[
   1.110 +      OutputGenerator._getOutputName(roleString)] ||
   1.111 +      this.objectOutputFunctions.defaultFunc;
   1.112 +
   1.113 +    let flags = this.roleRuleMap[roleString] || 0;
   1.114 +
   1.115 +    if (aAccessible.childCount == 0)
   1.116 +      flags |= INCLUDE_NAME;
   1.117 +
   1.118 +    return func.apply(this, [aAccessible, roleString,
   1.119 +                             Utils.getState(aAccessible), flags, aContext]);
   1.120 +  },
   1.121 +
   1.122 +  /**
   1.123 +   * Generates output for an action performed.
   1.124 +   * @param {nsIAccessible} aAccessible accessible object that the action was
   1.125 +   *    invoked in.
   1.126 +   * @param {string} aActionName the name of the action, one of the keys in
   1.127 +   *    {@link gActionMap}.
   1.128 +   * @return {Array} A one string array with the action.
   1.129 +   */
   1.130 +  genForAction: function genForAction(aObject, aActionName) {},
   1.131 +
   1.132 +  /**
   1.133 +   * Generates output for an announcement. Basically attempts to localize
   1.134 +   * the announcement string.
   1.135 +   * @param {string} aAnnouncement unlocalized announcement.
   1.136 +   * @return {Array} A one string array with the announcement.
   1.137 +   */
   1.138 +  genForAnnouncement: function genForAnnouncement(aAnnouncement) {},
   1.139 +
   1.140 +  /**
   1.141 +   * Generates output for a tab state change.
   1.142 +   * @param {nsIAccessible} aAccessible accessible object of the tab's attached
   1.143 +   *    document.
   1.144 +   * @param {string} aTabState the tab state name, see
   1.145 +   *    {@link Presenter.tabStateChanged}.
   1.146 +   * @return {Array} The tab state utterace.
   1.147 +   */
   1.148 +  genForTabStateChange: function genForTabStateChange(aObject, aTabState) {},
   1.149 +
   1.150 +  /**
   1.151 +   * Generates output for announcing entering and leaving editing mode.
   1.152 +   * @param {aIsEditing} boolean true if we are in editing mode
   1.153 +   * @return {Array} The mode utterance
   1.154 +   */
   1.155 +  genForEditingMode: function genForEditingMode(aIsEditing) {},
   1.156 +
   1.157 +  _getContextStart: function getContextStart(aContext) {},
   1.158 +
   1.159 +  _addName: function _addName(aOutput, aAccessible, aFlags) {
   1.160 +    let name;
   1.161 +    if ((Utils.getAttributes(aAccessible)['explicit-name'] === 'true' &&
   1.162 +         !(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) {
   1.163 +      name = aAccessible.name;
   1.164 +    }
   1.165 +
   1.166 +    let description = aAccessible.description;
   1.167 +    if (description) {
   1.168 +      // Compare against the calculated name unconditionally, regardless of name rule,
   1.169 +      // so we can make sure we don't speak duplicated descriptions
   1.170 +      let tmpName = name || aAccessible.name;
   1.171 +      if (tmpName && (description !== tmpName)) {
   1.172 +        name = name || '';
   1.173 +        name = this.outputOrder === OUTPUT_DESC_FIRST ?
   1.174 +          description + ' - ' + name :
   1.175 +          name + ' - ' + description;
   1.176 +      }
   1.177 +    }
   1.178 +
   1.179 +    if (name) {
   1.180 +      aOutput[this.outputOrder === OUTPUT_DESC_FIRST ?
   1.181 +        'push' : 'unshift'](name);
   1.182 +    }
   1.183 +  },
   1.184 +
   1.185 +  /**
   1.186 +   * Adds a landmark role to the output if available.
   1.187 +   * @param {Array} aOutput Output array.
   1.188 +   * @param {nsIAccessible} aAccessible current accessible object.
   1.189 +   */
   1.190 +  _addLandmark: function _addLandmark(aOutput, aAccessible) {
   1.191 +    let landmarkName = Utils.getLandmarkName(aAccessible);
   1.192 +    if (!landmarkName) {
   1.193 +      return;
   1.194 +    }
   1.195 +
   1.196 +    let landmark = Utils.stringBundle.GetStringFromName(landmarkName);
   1.197 +    if (!landmark) {
   1.198 +      return;
   1.199 +    }
   1.200 +
   1.201 +    aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push'](
   1.202 +      landmark);
   1.203 +  },
   1.204 +
   1.205 +  /**
   1.206 +   * Adds an entry type attribute to the description if available.
   1.207 +   * @param {Array} aDesc Description array.
   1.208 +   * @param {nsIAccessible} aAccessible current accessible object.
   1.209 +   * @param {String} aRoleStr aAccessible's role string.
   1.210 +   */
   1.211 +  _addType: function _addType(aDesc, aAccessible, aRoleStr) {
   1.212 +    if (aRoleStr !== 'entry') {
   1.213 +      return;
   1.214 +    }
   1.215 +
   1.216 +    let typeName = Utils.getAttributes(aAccessible)['text-input-type'];
   1.217 +    // Ignore the the input type="text" case.
   1.218 +    if (!typeName || typeName === 'text') {
   1.219 +      return;
   1.220 +    }
   1.221 +    typeName = 'textInputType_' + typeName;
   1.222 +    try {
   1.223 +      aDesc.push(Utils.stringBundle.GetStringFromName(typeName));
   1.224 +    } catch (x) {
   1.225 +      Logger.warning('Failed to get a string from a bundle for', typeName);
   1.226 +    }
   1.227 +  },
   1.228 +
   1.229 +  get outputOrder() {
   1.230 +    if (!this._utteranceOrder) {
   1.231 +      this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance');
   1.232 +    }
   1.233 +    return typeof this._utteranceOrder.value === 'number' ?
   1.234 +      this._utteranceOrder.value : this.defaultOutputOrder;
   1.235 +  },
   1.236 +
   1.237 +  _getOutputName: function _getOutputName(aName) {
   1.238 +    return aName.replace(' ', '');
   1.239 +  },
   1.240 +
   1.241 +  _getLocalizedRole: function _getLocalizedRole(aRoleStr) {},
   1.242 +
   1.243 +  _getLocalizedState: function _getLocalizedState(aState) {},
   1.244 +
   1.245 +  _getPluralFormString: function _getPluralFormString(aString, aCount) {
   1.246 +    let str = Utils.stringBundle.GetStringFromName(this._getOutputName(aString));
   1.247 +    str = PluralForm.get(aCount, str);
   1.248 +    return str.replace('#1', aCount);
   1.249 +  },
   1.250 +
   1.251 +  roleRuleMap: {
   1.252 +    'menubar': INCLUDE_DESC,
   1.253 +    'scrollbar': INCLUDE_DESC,
   1.254 +    'grip': INCLUDE_DESC,
   1.255 +    'alert': INCLUDE_DESC | INCLUDE_NAME,
   1.256 +    'menupopup': INCLUDE_DESC,
   1.257 +    'menuitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.258 +    'tooltip': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.259 +    'columnheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.260 +    'rowheader': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.261 +    'column': NAME_FROM_SUBTREE_RULE,
   1.262 +    'row': NAME_FROM_SUBTREE_RULE,
   1.263 +    'cell': INCLUDE_DESC | INCLUDE_NAME,
   1.264 +    'application': INCLUDE_NAME,
   1.265 +    'document': INCLUDE_NAME,
   1.266 +    'grouping': INCLUDE_DESC | INCLUDE_NAME,
   1.267 +    'toolbar': INCLUDE_DESC,
   1.268 +    'table': INCLUDE_DESC | INCLUDE_NAME,
   1.269 +    'link': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.270 +    'helpballoon': NAME_FROM_SUBTREE_RULE,
   1.271 +    'list': INCLUDE_DESC | INCLUDE_NAME,
   1.272 +    'listitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.273 +    'outline': INCLUDE_DESC,
   1.274 +    'outlineitem': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.275 +    'pagetab': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.276 +    'graphic': INCLUDE_DESC,
   1.277 +    'pushbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.278 +    'checkbutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.279 +    'radiobutton': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.280 +    'buttondropdown': NAME_FROM_SUBTREE_RULE,
   1.281 +    'combobox': INCLUDE_DESC,
   1.282 +    'droplist': INCLUDE_DESC,
   1.283 +    'progressbar': INCLUDE_DESC | INCLUDE_VALUE,
   1.284 +    'slider': INCLUDE_DESC | INCLUDE_VALUE,
   1.285 +    'spinbutton': INCLUDE_DESC | INCLUDE_VALUE,
   1.286 +    'diagram': INCLUDE_DESC,
   1.287 +    'animation': INCLUDE_DESC,
   1.288 +    'equation': INCLUDE_DESC,
   1.289 +    'buttonmenu': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.290 +    'buttondropdowngrid': NAME_FROM_SUBTREE_RULE,
   1.291 +    'pagetablist': INCLUDE_DESC,
   1.292 +    'canvas': INCLUDE_DESC,
   1.293 +    'check menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.294 +    'label': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.295 +    'password text': INCLUDE_DESC,
   1.296 +    'popup menu': INCLUDE_DESC,
   1.297 +    'radio menu item': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.298 +    'table column header': NAME_FROM_SUBTREE_RULE,
   1.299 +    'table row header': NAME_FROM_SUBTREE_RULE,
   1.300 +    'tear off menu item': NAME_FROM_SUBTREE_RULE,
   1.301 +    'toggle button': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.302 +    'parent menuitem': NAME_FROM_SUBTREE_RULE,
   1.303 +    'header': INCLUDE_DESC,
   1.304 +    'footer': INCLUDE_DESC,
   1.305 +    'entry': INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE,
   1.306 +    'caption': INCLUDE_DESC,
   1.307 +    'document frame': INCLUDE_DESC,
   1.308 +    'heading': INCLUDE_DESC,
   1.309 +    'calendar': INCLUDE_DESC | INCLUDE_NAME,
   1.310 +    'combobox list': INCLUDE_DESC,
   1.311 +    'combobox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.312 +    'listbox option': INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
   1.313 +    'listbox rich option': NAME_FROM_SUBTREE_RULE,
   1.314 +    'gridcell': NAME_FROM_SUBTREE_RULE,
   1.315 +    'check rich option': NAME_FROM_SUBTREE_RULE,
   1.316 +    'term': NAME_FROM_SUBTREE_RULE,
   1.317 +    'definition': NAME_FROM_SUBTREE_RULE,
   1.318 +    'key': NAME_FROM_SUBTREE_RULE,
   1.319 +    'image map': INCLUDE_DESC,
   1.320 +    'option': INCLUDE_DESC,
   1.321 +    'listbox': INCLUDE_DESC,
   1.322 +    'definitionlist': INCLUDE_DESC | INCLUDE_NAME,
   1.323 +    'dialog': INCLUDE_DESC | INCLUDE_NAME,
   1.324 +    'chrome window': IGNORE_EXPLICIT_NAME,
   1.325 +    'app root': IGNORE_EXPLICIT_NAME },
   1.326 +
   1.327 +  objectOutputFunctions: {
   1.328 +    _generateBaseOutput: function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) {
   1.329 +      let output = [];
   1.330 +
   1.331 +      if (aFlags & INCLUDE_DESC) {
   1.332 +        let desc = this._getLocalizedState(aState);
   1.333 +        let roleStr = this._getLocalizedRole(aRoleStr);
   1.334 +        if (roleStr) {
   1.335 +          this._addType(desc, aAccessible, aRoleStr);
   1.336 +          desc.push(roleStr);
   1.337 +        }
   1.338 +        output.push(desc.join(' '));
   1.339 +      }
   1.340 +
   1.341 +      if (aFlags & INCLUDE_VALUE) {
   1.342 +        let value = aAccessible.value;
   1.343 +        if (value) {
   1.344 +          output[this.outputOrder === OUTPUT_DESC_FIRST ?
   1.345 +                 'push' : 'unshift'](value);
   1.346 +        }
   1.347 +      }
   1.348 +
   1.349 +      this._addName(output, aAccessible, aFlags);
   1.350 +      this._addLandmark(output, aAccessible);
   1.351 +
   1.352 +      return output;
   1.353 +    },
   1.354 +
   1.355 +    label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) {
   1.356 +      if (aContext.isNestedControl ||
   1.357 +          aContext.accessible == Utils.getEmbeddedControl(aAccessible)) {
   1.358 +        // If we are on a nested control, or a nesting label,
   1.359 +        // we don't need the context.
   1.360 +        return [];
   1.361 +      }
   1.362 +
   1.363 +      return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
   1.364 +    },
   1.365 +
   1.366 +    entry: function entry(aAccessible, aRoleStr, aState, aFlags) {
   1.367 +      let rolestr = aState.contains(States.MULTI_LINE) ? 'textarea' : 'entry';
   1.368 +      return this.objectOutputFunctions.defaultFunc.apply(
   1.369 +        this, [aAccessible, rolestr, aState, aFlags]);
   1.370 +    },
   1.371 +
   1.372 +    pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) {
   1.373 +      let localizedRole = this._getLocalizedRole(aRoleStr);
   1.374 +      let itemno = {};
   1.375 +      let itemof = {};
   1.376 +      aAccessible.groupPosition({}, itemof, itemno);
   1.377 +      let output = [];
   1.378 +      let desc = this._getLocalizedState(aState);
   1.379 +      desc.push(
   1.380 +        Utils.stringBundle.formatStringFromName(
   1.381 +          'objItemOf', [localizedRole, itemno.value, itemof.value], 3));
   1.382 +      output.push(desc.join(' '));
   1.383 +
   1.384 +      this._addName(output, aAccessible, aFlags);
   1.385 +      this._addLandmark(output, aAccessible);
   1.386 +
   1.387 +      return output;
   1.388 +    },
   1.389 +
   1.390 +    table: function table(aAccessible, aRoleStr, aState, aFlags) {
   1.391 +      let output = [];
   1.392 +      let table;
   1.393 +      try {
   1.394 +        table = aAccessible.QueryInterface(Ci.nsIAccessibleTable);
   1.395 +      } catch (x) {
   1.396 +        Logger.logException(x);
   1.397 +        return output;
   1.398 +      } finally {
   1.399 +        // Check if it's a layout table, and bail out if true.
   1.400 +        // We don't want to speak any table information for layout tables.
   1.401 +        if (table.isProbablyForLayout()) {
   1.402 +          return output;
   1.403 +        }
   1.404 +        let tableColumnInfo = this._getPluralFormString('tableColumnInfo',
   1.405 +          table.columnCount);
   1.406 +        let tableRowInfo = this._getPluralFormString('tableRowInfo',
   1.407 +          table.rowCount);
   1.408 +        output.push(Utils.stringBundle.formatStringFromName(
   1.409 +          this._getOutputName('tableInfo'), [this._getLocalizedRole(aRoleStr),
   1.410 +            tableColumnInfo, tableRowInfo], 3));
   1.411 +        this._addName(output, aAccessible, aFlags);
   1.412 +        this._addLandmark(output, aAccessible);
   1.413 +        return output;
   1.414 +      }
   1.415 +    }
   1.416 +  }
   1.417 +};
   1.418 +
   1.419 +/**
   1.420 + * Generates speech utterances from objects, actions and state changes.
   1.421 + * An utterance is an array of strings.
   1.422 + *
   1.423 + * It should not be assumed that flattening an utterance array would create a
   1.424 + * gramatically correct sentence. For example, {@link genForObject} might
   1.425 + * return: ['graphic', 'Welcome to my home page'].
   1.426 + * Each string element in an utterance should be gramatically correct in itself.
   1.427 + * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
   1.428 + *
   1.429 + * An utterance is ordered from the least to the most important. Speaking the
   1.430 + * last string usually makes sense, but speaking the first often won't.
   1.431 + * For example {@link genForAction} might return ['button', 'clicked'] for a
   1.432 + * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
   1.433 + * not.
   1.434 + */
   1.435 +this.UtteranceGenerator = {
   1.436 +  __proto__: OutputGenerator,
   1.437 +
   1.438 +  gActionMap: {
   1.439 +    jump: 'jumpAction',
   1.440 +    press: 'pressAction',
   1.441 +    check: 'checkAction',
   1.442 +    uncheck: 'uncheckAction',
   1.443 +    select: 'selectAction',
   1.444 +    unselect: 'unselectAction',
   1.445 +    open: 'openAction',
   1.446 +    close: 'closeAction',
   1.447 +    switch: 'switchAction',
   1.448 +    click: 'clickAction',
   1.449 +    collapse: 'collapseAction',
   1.450 +    expand: 'expandAction',
   1.451 +    activate: 'activateAction',
   1.452 +    cycle: 'cycleAction'
   1.453 +  },
   1.454 +
   1.455 +  //TODO: May become more verbose in the future.
   1.456 +  genForAction: function genForAction(aObject, aActionName) {
   1.457 +    return [Utils.stringBundle.GetStringFromName(this.gActionMap[aActionName])];
   1.458 +  },
   1.459 +
   1.460 +  genForLiveRegion: function genForLiveRegion(aContext, aIsHide, aModifiedText) {
   1.461 +    let utterance = [];
   1.462 +    if (aIsHide) {
   1.463 +      utterance.push(Utils.stringBundle.GetStringFromName('hidden'));
   1.464 +    }
   1.465 +    return utterance.concat(
   1.466 +      aModifiedText || this.genForContext(aContext).output);
   1.467 +  },
   1.468 +
   1.469 +  genForAnnouncement: function genForAnnouncement(aAnnouncement) {
   1.470 +    try {
   1.471 +      return [Utils.stringBundle.GetStringFromName(aAnnouncement)];
   1.472 +    } catch (x) {
   1.473 +      return [aAnnouncement];
   1.474 +    }
   1.475 +  },
   1.476 +
   1.477 +  genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
   1.478 +    switch (aTabState) {
   1.479 +      case 'newtab':
   1.480 +        return [Utils.stringBundle.GetStringFromName('tabNew')];
   1.481 +      case 'loading':
   1.482 +        return [Utils.stringBundle.GetStringFromName('tabLoading')];
   1.483 +      case 'loaded':
   1.484 +        return [aObject.name || '',
   1.485 +                Utils.stringBundle.GetStringFromName('tabLoaded')];
   1.486 +      case 'loadstopped':
   1.487 +        return [Utils.stringBundle.GetStringFromName('tabLoadStopped')];
   1.488 +      case 'reload':
   1.489 +        return [Utils.stringBundle.GetStringFromName('tabReload')];
   1.490 +      default:
   1.491 +        return [];
   1.492 +    }
   1.493 +  },
   1.494 +
   1.495 +  genForEditingMode: function genForEditingMode(aIsEditing) {
   1.496 +    return [Utils.stringBundle.GetStringFromName(
   1.497 +      aIsEditing ? 'editingMode' : 'navigationMode')];
   1.498 +  },
   1.499 +
   1.500 +  objectOutputFunctions: {
   1.501 +
   1.502 +    __proto__: OutputGenerator.objectOutputFunctions,
   1.503 +
   1.504 +    defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) {
   1.505 +      return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
   1.506 +    },
   1.507 +
   1.508 +    heading: function heading(aAccessible, aRoleStr, aState, aFlags) {
   1.509 +      let level = {};
   1.510 +      aAccessible.groupPosition(level, {}, {});
   1.511 +      let utterance =
   1.512 +        [Utils.stringBundle.formatStringFromName(
   1.513 +          'headingLevel', [level.value], 1)];
   1.514 +
   1.515 +      this._addName(utterance, aAccessible, aFlags);
   1.516 +      this._addLandmark(utterance, aAccessible);
   1.517 +
   1.518 +      return utterance;
   1.519 +    },
   1.520 +
   1.521 +    listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
   1.522 +      let itemno = {};
   1.523 +      let itemof = {};
   1.524 +      aAccessible.groupPosition({}, itemof, itemno);
   1.525 +      let utterance = [];
   1.526 +      if (itemno.value == 1) // Start of list
   1.527 +        utterance.push(Utils.stringBundle.GetStringFromName('listStart'));
   1.528 +      else if (itemno.value == itemof.value) // last item
   1.529 +        utterance.push(Utils.stringBundle.GetStringFromName('listEnd'));
   1.530 +
   1.531 +      this._addName(utterance, aAccessible, aFlags);
   1.532 +      this._addLandmark(utterance, aAccessible);
   1.533 +
   1.534 +      return utterance;
   1.535 +    },
   1.536 +
   1.537 +    list: function list(aAccessible, aRoleStr, aState, aFlags) {
   1.538 +      return this._getListUtterance
   1.539 +        (aAccessible, aRoleStr, aFlags, aAccessible.childCount);
   1.540 +    },
   1.541 +
   1.542 +    definitionlist: function definitionlist(aAccessible, aRoleStr, aState, aFlags) {
   1.543 +      return this._getListUtterance
   1.544 +        (aAccessible, aRoleStr, aFlags, aAccessible.childCount / 2);
   1.545 +    },
   1.546 +
   1.547 +    application: function application(aAccessible, aRoleStr, aState, aFlags) {
   1.548 +      // Don't utter location of applications, it gets tiring.
   1.549 +      if (aAccessible.name != aAccessible.DOMNode.location)
   1.550 +        return this.objectOutputFunctions.defaultFunc.apply(this,
   1.551 +          [aAccessible, aRoleStr, aState, aFlags]);
   1.552 +
   1.553 +      return [];
   1.554 +    },
   1.555 +
   1.556 +    cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
   1.557 +      let utterance = [];
   1.558 +      let cell = aContext.getCellInfo(aAccessible);
   1.559 +      if (cell) {
   1.560 +        let desc = [];
   1.561 +        let addCellChanged = function addCellChanged(aDesc, aChanged, aString, aIndex) {
   1.562 +          if (aChanged) {
   1.563 +            aDesc.push(Utils.stringBundle.formatStringFromName(aString,
   1.564 +              [aIndex + 1], 1));
   1.565 +          }
   1.566 +        };
   1.567 +        let addExtent = function addExtent(aDesc, aExtent, aString) {
   1.568 +          if (aExtent > 1) {
   1.569 +            aDesc.push(Utils.stringBundle.formatStringFromName(aString,
   1.570 +              [aExtent], 1));
   1.571 +          }
   1.572 +        };
   1.573 +        let addHeaders = function addHeaders(aDesc, aHeaders) {
   1.574 +          if (aHeaders.length > 0) {
   1.575 +            aDesc.push.apply(aDesc, aHeaders);
   1.576 +          }
   1.577 +        };
   1.578 +
   1.579 +        addCellChanged(desc, cell.columnChanged, 'columnInfo', cell.columnIndex);
   1.580 +        addCellChanged(desc, cell.rowChanged, 'rowInfo', cell.rowIndex);
   1.581 +
   1.582 +        addExtent(desc, cell.columnExtent, 'spansColumns');
   1.583 +        addExtent(desc, cell.rowExtent, 'spansRows');
   1.584 +
   1.585 +        addHeaders(desc, cell.columnHeaders);
   1.586 +        addHeaders(desc, cell.rowHeaders);
   1.587 +
   1.588 +        utterance.push(desc.join(' '));
   1.589 +      }
   1.590 +
   1.591 +      this._addName(utterance, aAccessible, aFlags);
   1.592 +      this._addLandmark(utterance, aAccessible);
   1.593 +
   1.594 +      return utterance;
   1.595 +    },
   1.596 +
   1.597 +    columnheader: function columnheader() {
   1.598 +      return this.objectOutputFunctions.cell.apply(this, arguments);
   1.599 +    },
   1.600 +
   1.601 +    rowheader: function rowheader() {
   1.602 +      return this.objectOutputFunctions.cell.apply(this, arguments);
   1.603 +    }
   1.604 +  },
   1.605 +
   1.606 +  _getContextStart: function _getContextStart(aContext) {
   1.607 +    return aContext.newAncestry;
   1.608 +  },
   1.609 +
   1.610 +  _getLocalizedRole: function _getLocalizedRole(aRoleStr) {
   1.611 +    try {
   1.612 +      return Utils.stringBundle.GetStringFromName(
   1.613 +        this._getOutputName(aRoleStr));
   1.614 +    } catch (x) {
   1.615 +      return '';
   1.616 +    }
   1.617 +  },
   1.618 +
   1.619 +  _getLocalizedState: function _getLocalizedState(aState) {
   1.620 +    let stateUtterances = [];
   1.621 +
   1.622 +    if (aState.contains(States.UNAVAILABLE)) {
   1.623 +      stateUtterances.push(
   1.624 +        Utils.stringBundle.GetStringFromName('stateUnavailable'));
   1.625 +    }
   1.626 +
   1.627 +    // Don't utter this in Jelly Bean, we let TalkBack do it for us there.
   1.628 +    // This is because we expose the checked information on the node itself.
   1.629 +    // XXX: this means the checked state is always appended to the end, regardless
   1.630 +    // of the utterance ordering preference.
   1.631 +    if ((Utils.AndroidSdkVersion < 16 || Utils.MozBuildApp === 'browser') &&
   1.632 +      aState.contains(States.CHECKABLE)) {
   1.633 +      let statetr = aState.contains(States.CHECKED) ?
   1.634 +        'stateChecked' : 'stateNotChecked';
   1.635 +      stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr));
   1.636 +    }
   1.637 +
   1.638 +    if (aState.contains(States.PRESSED)) {
   1.639 +      stateUtterances.push(
   1.640 +        Utils.stringBundle.GetStringFromName('statePressed'));
   1.641 +    }
   1.642 +
   1.643 +    if (aState.contains(States.EXPANDABLE)) {
   1.644 +      let statetr = aState.contains(States.EXPANDED) ?
   1.645 +        'stateExpanded' : 'stateCollapsed';
   1.646 +      stateUtterances.push(Utils.stringBundle.GetStringFromName(statetr));
   1.647 +    }
   1.648 +
   1.649 +    if (aState.contains(States.REQUIRED)) {
   1.650 +      stateUtterances.push(
   1.651 +        Utils.stringBundle.GetStringFromName('stateRequired'));
   1.652 +    }
   1.653 +
   1.654 +    if (aState.contains(States.TRAVERSED)) {
   1.655 +      stateUtterances.push(
   1.656 +        Utils.stringBundle.GetStringFromName('stateTraversed'));
   1.657 +    }
   1.658 +
   1.659 +    if (aState.contains(States.HASPOPUP)) {
   1.660 +      stateUtterances.push(
   1.661 +        Utils.stringBundle.GetStringFromName('stateHasPopup'));
   1.662 +    }
   1.663 +
   1.664 +    if (aState.contains(States.SELECTED)) {
   1.665 +      stateUtterances.push(
   1.666 +        Utils.stringBundle.GetStringFromName('stateSelected'));
   1.667 +    }
   1.668 +
   1.669 +    return stateUtterances;
   1.670 +  },
   1.671 +
   1.672 +  _getListUtterance: function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) {
   1.673 +    let desc = [];
   1.674 +    let roleStr = this._getLocalizedRole(aRoleStr);
   1.675 +    if (roleStr) {
   1.676 +      desc.push(roleStr);
   1.677 +    }
   1.678 +    desc.push(this._getPluralFormString('listItemsCount', aItemCount));
   1.679 +    let utterance = [desc.join(' ')];
   1.680 +
   1.681 +    this._addName(utterance, aAccessible, aFlags);
   1.682 +    this._addLandmark(utterance, aAccessible);
   1.683 +
   1.684 +    return utterance;
   1.685 +  }
   1.686 +};
   1.687 +
   1.688 +
   1.689 +this.BrailleGenerator = {
   1.690 +  __proto__: OutputGenerator,
   1.691 +
   1.692 +  genForContext: function genForContext(aContext) {
   1.693 +    let output = OutputGenerator.genForContext.apply(this, arguments);
   1.694 +
   1.695 +    let acc = aContext.accessible;
   1.696 +
   1.697 +    // add the static text indicating a list item; do this for both listitems or
   1.698 +    // direct first children of listitems, because these are both common browsing
   1.699 +    // scenarios
   1.700 +    let addListitemIndicator = function addListitemIndicator(indicator = '*') {
   1.701 +      output.output.unshift(indicator);
   1.702 +    };
   1.703 +
   1.704 +    if (acc.indexInParent === 1 &&
   1.705 +        acc.parent.role == Roles.LISTITEM &&
   1.706 +        acc.previousSibling.role == Roles.STATICTEXT) {
   1.707 +      if (acc.parent.parent && acc.parent.parent.DOMNode &&
   1.708 +          acc.parent.parent.DOMNode.nodeName == 'UL') {
   1.709 +        addListitemIndicator();
   1.710 +      } else {
   1.711 +        addListitemIndicator(acc.previousSibling.name.trim());
   1.712 +      }
   1.713 +    } else if (acc.role == Roles.LISTITEM && acc.firstChild &&
   1.714 +               acc.firstChild.role == Roles.STATICTEXT) {
   1.715 +      if (acc.parent.DOMNode.nodeName == 'UL') {
   1.716 +        addListitemIndicator();
   1.717 +      } else {
   1.718 +        addListitemIndicator(acc.firstChild.name.trim());
   1.719 +      }
   1.720 +    }
   1.721 +
   1.722 +    if (acc instanceof Ci.nsIAccessibleText) {
   1.723 +      output.endOffset = this.outputOrder === OUTPUT_DESC_FIRST ?
   1.724 +                         output.output.join(' ').length : acc.characterCount;
   1.725 +      output.startOffset = output.endOffset - acc.characterCount;
   1.726 +    }
   1.727 +
   1.728 +    return output;
   1.729 +  },
   1.730 +
   1.731 +  objectOutputFunctions: {
   1.732 +
   1.733 +    __proto__: OutputGenerator.objectOutputFunctions,
   1.734 +
   1.735 +    defaultFunc: function defaultFunc(aAccessible, aRoleStr, aState, aFlags) {
   1.736 +      return this.objectOutputFunctions._generateBaseOutput.apply(this, arguments);
   1.737 +    },
   1.738 +
   1.739 +    listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
   1.740 +      let braille = [];
   1.741 +
   1.742 +      this._addName(braille, aAccessible, aFlags);
   1.743 +      this._addLandmark(braille, aAccessible);
   1.744 +
   1.745 +      return braille;
   1.746 +    },
   1.747 +
   1.748 +    cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
   1.749 +      let braille = [];
   1.750 +      let cell = aContext.getCellInfo(aAccessible);
   1.751 +      if (cell) {
   1.752 +        let desc = [];
   1.753 +        let addHeaders = function addHeaders(aDesc, aHeaders) {
   1.754 +          if (aHeaders.length > 0) {
   1.755 +            aDesc.push.apply(aDesc, aHeaders);
   1.756 +          }
   1.757 +        };
   1.758 +
   1.759 +        desc.push(Utils.stringBundle.formatStringFromName(
   1.760 +          this._getOutputName('cellInfo'), [cell.columnIndex + 1,
   1.761 +            cell.rowIndex + 1], 2));
   1.762 +
   1.763 +        addHeaders(desc, cell.columnHeaders);
   1.764 +        addHeaders(desc, cell.rowHeaders);
   1.765 +        braille.push(desc.join(' '));
   1.766 +      }
   1.767 +
   1.768 +      this._addName(braille, aAccessible, aFlags);
   1.769 +      this._addLandmark(braille, aAccessible);
   1.770 +      return braille;
   1.771 +    },
   1.772 +
   1.773 +    columnheader: function columnheader() {
   1.774 +      return this.objectOutputFunctions.cell.apply(this, arguments);
   1.775 +    },
   1.776 +
   1.777 +    rowheader: function rowheader() {
   1.778 +      return this.objectOutputFunctions.cell.apply(this, arguments);
   1.779 +    },
   1.780 +
   1.781 +    statictext: function statictext(aAccessible, aRoleStr, aState, aFlags) {
   1.782 +      // Since we customize the list bullet's output, we add the static
   1.783 +      // text from the first node in each listitem, so skip it here.
   1.784 +      if (aAccessible.parent.role == Roles.LISTITEM) {
   1.785 +        return [];
   1.786 +      }
   1.787 +
   1.788 +      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   1.789 +    },
   1.790 +
   1.791 +    _useStateNotRole: function _useStateNotRole(aAccessible, aRoleStr, aState, aFlags) {
   1.792 +      let braille = [];
   1.793 +
   1.794 +      let desc = this._getLocalizedState(aState, aAccessible.role);
   1.795 +      braille.push(desc.join(' '));
   1.796 +
   1.797 +      this._addName(braille, aAccessible, aFlags);
   1.798 +      this._addLandmark(braille, aAccessible);
   1.799 +
   1.800 +      return braille;
   1.801 +    },
   1.802 +
   1.803 +    checkbutton: function checkbutton(aAccessible, aRoleStr, aState, aFlags) {
   1.804 +      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   1.805 +    },
   1.806 +
   1.807 +    radiobutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) {
   1.808 +      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   1.809 +    },
   1.810 +
   1.811 +    togglebutton: function radiobutton(aAccessible, aRoleStr, aState, aFlags) {
   1.812 +      return this.objectOutputFunctions._useStateNotRole.apply(this, arguments);
   1.813 +    }
   1.814 +  },
   1.815 +
   1.816 +  _getContextStart: function _getContextStart(aContext) {
   1.817 +    if (aContext.accessible.parent.role == Roles.LINK) {
   1.818 +      return [aContext.accessible.parent];
   1.819 +    }
   1.820 +
   1.821 +    return [];
   1.822 +  },
   1.823 +
   1.824 +  _getOutputName: function _getOutputName(aName) {
   1.825 +    return OutputGenerator._getOutputName(aName) + 'Abbr';
   1.826 +  },
   1.827 +
   1.828 +  _getLocalizedRole: function _getLocalizedRole(aRoleStr) {
   1.829 +    try {
   1.830 +      return Utils.stringBundle.GetStringFromName(
   1.831 +        this._getOutputName(aRoleStr));
   1.832 +    } catch (x) {
   1.833 +      try {
   1.834 +        return Utils.stringBundle.GetStringFromName(
   1.835 +          OutputGenerator._getOutputName(aRoleStr));
   1.836 +      } catch (y) {
   1.837 +        return '';
   1.838 +      }
   1.839 +    }
   1.840 +  },
   1.841 +
   1.842 +  _getLocalizedState: function _getLocalizedState(aState, aRole) {
   1.843 +    let stateBraille = [];
   1.844 +
   1.845 +    let getResultMarker = function getResultMarker(aMarker) {
   1.846 +      // aMarker is a simple boolean.
   1.847 +      let resultMarker = [];
   1.848 +      resultMarker.push('(');
   1.849 +      resultMarker.push(aMarker ? 'x' : ' ');
   1.850 +      resultMarker.push(')');
   1.851 +
   1.852 +      return resultMarker.join('');
   1.853 +    };
   1.854 +
   1.855 +    if (aState.contains(States.CHECKABLE)) {
   1.856 +      stateBraille.push(getResultMarker(aState.contains(States.CHECKED)));
   1.857 +    }
   1.858 +
   1.859 +    if (aRole === Roles.TOGGLE_BUTTON) {
   1.860 +      stateBraille.push(getResultMarker(aState.contains(States.PRESSED)));
   1.861 +    }
   1.862 +
   1.863 +    return stateBraille;
   1.864 +  }
   1.865 +
   1.866 +};

mercurial