accessible/src/jsat/Presentation.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/accessible/src/jsat/Presentation.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,665 @@
     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 +Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    1.16 +XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
    1.17 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.18 +XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
    1.19 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.20 +XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext',
    1.21 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.22 +XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator',
    1.23 +  'resource://gre/modules/accessibility/OutputGenerator.jsm');
    1.24 +XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator',
    1.25 +  'resource://gre/modules/accessibility/OutputGenerator.jsm');
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
    1.27 +  'resource://gre/modules/accessibility/Constants.jsm');
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, 'States',
    1.29 +  'resource://gre/modules/accessibility/Constants.jsm');
    1.30 +
    1.31 +this.EXPORTED_SYMBOLS = ['Presentation'];
    1.32 +
    1.33 +/**
    1.34 + * The interface for all presenter classes. A presenter could be, for example,
    1.35 + * a speech output module, or a visual cursor indicator.
    1.36 + */
    1.37 +function Presenter() {}
    1.38 +
    1.39 +Presenter.prototype = {
    1.40 +  /**
    1.41 +   * The type of presenter. Used for matching it with the appropriate output method.
    1.42 +   */
    1.43 +  type: 'Base',
    1.44 +
    1.45 +  /**
    1.46 +   * The virtual cursor's position changed.
    1.47 +   * @param {PivotContext} aContext the context object for the new pivot
    1.48 +   *   position.
    1.49 +   * @param {int} aReason the reason for the pivot change.
    1.50 +   *   See nsIAccessiblePivot.
    1.51 +   */
    1.52 +  pivotChanged: function pivotChanged(aContext, aReason) {},
    1.53 +
    1.54 +  /**
    1.55 +   * An object's action has been invoked.
    1.56 +   * @param {nsIAccessible} aObject the object that has been invoked.
    1.57 +   * @param {string} aActionName the name of the action.
    1.58 +   */
    1.59 +  actionInvoked: function actionInvoked(aObject, aActionName) {},
    1.60 +
    1.61 +  /**
    1.62 +   * Text has changed, either by the user or by the system. TODO.
    1.63 +   */
    1.64 +  textChanged: function textChanged(aIsInserted, aStartOffset,
    1.65 +                                    aLength, aText,
    1.66 +                                    aModifiedText) {},
    1.67 +
    1.68 +  /**
    1.69 +   * Text selection has changed. TODO.
    1.70 +   */
    1.71 +  textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUser) {},
    1.72 +
    1.73 +  /**
    1.74 +   * Selection has changed. TODO.
    1.75 +   * @param {nsIAccessible} aObject the object that has been selected.
    1.76 +   */
    1.77 +  selectionChanged: function selectionChanged(aObject) {},
    1.78 +
    1.79 +  /**
    1.80 +   * Value has changed.
    1.81 +   * @param {nsIAccessible} aAccessible the object whose value has changed.
    1.82 +   */
    1.83 +  valueChanged: function valueChanged(aAccessible) {},
    1.84 +
    1.85 +  /**
    1.86 +   * The tab, or the tab's document state has changed.
    1.87 +   * @param {nsIAccessible} aDocObj the tab document accessible that has had its
    1.88 +   *    state changed, or null if the tab has no associated document yet.
    1.89 +   * @param {string} aPageState the state name for the tab, valid states are:
    1.90 +   *    'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
    1.91 +   */
    1.92 +  tabStateChanged: function tabStateChanged(aDocObj, aPageState) {},
    1.93 +
    1.94 +  /**
    1.95 +   * The current tab has changed.
    1.96 +   * @param {PivotContext} aDocContext context object for tab's
    1.97 +   *   document.
    1.98 +   * @param {PivotContext} aVCContext context object for tab's current
    1.99 +   *   virtual cursor position.
   1.100 +   */
   1.101 +  tabSelected: function tabSelected(aDocContext, aVCContext) {},
   1.102 +
   1.103 +  /**
   1.104 +   * The viewport has changed, either a scroll, pan, zoom, or
   1.105 +   *    landscape/portrait toggle.
   1.106 +   * @param {Window} aWindow window of viewport that changed.
   1.107 +   */
   1.108 +  viewportChanged: function viewportChanged(aWindow) {},
   1.109 +
   1.110 +  /**
   1.111 +   * We have entered or left text editing mode.
   1.112 +   */
   1.113 +  editingModeChanged: function editingModeChanged(aIsEditing) {},
   1.114 +
   1.115 +  /**
   1.116 +   * Announce something. Typically an app state change.
   1.117 +   */
   1.118 +  announce: function announce(aAnnouncement) {},
   1.119 +
   1.120 +
   1.121 +
   1.122 +  /**
   1.123 +   * Announce a live region.
   1.124 +   * @param  {PivotContext} aContext context object for an accessible.
   1.125 +   * @param  {boolean} aIsPolite A politeness level for a live region.
   1.126 +   * @param  {boolean} aIsHide An indicator of hide/remove event.
   1.127 +   * @param  {string} aModifiedText Optional modified text.
   1.128 +   */
   1.129 +  liveRegion: function liveRegionShown(aContext, aIsPolite, aIsHide,
   1.130 +    aModifiedText) {}
   1.131 +};
   1.132 +
   1.133 +/**
   1.134 + * Visual presenter. Draws a box around the virtual cursor's position.
   1.135 + */
   1.136 +
   1.137 +this.VisualPresenter = function VisualPresenter() {
   1.138 +  this._displayedAccessibles = new WeakMap();
   1.139 +};
   1.140 +
   1.141 +VisualPresenter.prototype = {
   1.142 +  __proto__: Presenter.prototype,
   1.143 +
   1.144 +  type: 'Visual',
   1.145 +
   1.146 +  /**
   1.147 +   * The padding in pixels between the object and the highlight border.
   1.148 +   */
   1.149 +  BORDER_PADDING: 2,
   1.150 +
   1.151 +  viewportChanged: function VisualPresenter_viewportChanged(aWindow) {
   1.152 +    let currentDisplay = this._displayedAccessibles.get(aWindow);
   1.153 +    if (!currentDisplay) {
   1.154 +      return null;
   1.155 +    }
   1.156 +
   1.157 +    let currentAcc = currentDisplay.accessible;
   1.158 +    let start = currentDisplay.startOffset;
   1.159 +    let end = currentDisplay.endOffset;
   1.160 +    if (Utils.isAliveAndVisible(currentAcc)) {
   1.161 +      let bounds = (start === -1 && end === -1) ? Utils.getBounds(currentAcc) :
   1.162 +                   Utils.getTextBounds(currentAcc, start, end);
   1.163 +
   1.164 +      return {
   1.165 +        type: this.type,
   1.166 +        details: {
   1.167 +          method: 'showBounds',
   1.168 +          bounds: bounds,
   1.169 +          padding: this.BORDER_PADDING
   1.170 +        }
   1.171 +      };
   1.172 +    }
   1.173 +
   1.174 +    return null;
   1.175 +  },
   1.176 +
   1.177 +  pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) {
   1.178 +    if (!aContext.accessible) {
   1.179 +      // XXX: Don't hide because another vc may be using the highlight.
   1.180 +      return null;
   1.181 +    }
   1.182 +
   1.183 +    this._displayedAccessibles.set(aContext.accessible.document.window,
   1.184 +                                   { accessible: aContext.accessibleForBounds,
   1.185 +                                     startOffset: aContext.startOffset,
   1.186 +                                     endOffset: aContext.endOffset });
   1.187 +
   1.188 +    try {
   1.189 +      aContext.accessibleForBounds.scrollTo(
   1.190 +        Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
   1.191 +
   1.192 +      let bounds = (aContext.startOffset === -1 && aContext.endOffset === -1) ?
   1.193 +            aContext.bounds : Utils.getTextBounds(aContext.accessibleForBounds,
   1.194 +                                                  aContext.startOffset,
   1.195 +                                                  aContext.endOffset);
   1.196 +
   1.197 +      return {
   1.198 +        type: this.type,
   1.199 +        details: {
   1.200 +          method: 'showBounds',
   1.201 +          bounds: bounds,
   1.202 +          padding: this.BORDER_PADDING
   1.203 +        }
   1.204 +      };
   1.205 +    } catch (e) {
   1.206 +      Logger.logException(e, 'Failed to get bounds');
   1.207 +      return null;
   1.208 +    }
   1.209 +  },
   1.210 +
   1.211 +  tabSelected: function VisualPresenter_tabSelected(aDocContext, aVCContext) {
   1.212 +    return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
   1.213 +  },
   1.214 +
   1.215 +  tabStateChanged: function VisualPresenter_tabStateChanged(aDocObj,
   1.216 +                                                            aPageState) {
   1.217 +    if (aPageState == 'newdoc')
   1.218 +      return {type: this.type, details: {method: 'hideBounds'}};
   1.219 +
   1.220 +    return null;
   1.221 +  },
   1.222 +
   1.223 +  announce: function VisualPresenter_announce(aAnnouncement) {
   1.224 +    return {
   1.225 +      type: this.type,
   1.226 +      details: {
   1.227 +        method: 'showAnnouncement',
   1.228 +        text: aAnnouncement,
   1.229 +        duration: 1000
   1.230 +      }
   1.231 +    };
   1.232 +  }
   1.233 +};
   1.234 +
   1.235 +/**
   1.236 + * Android presenter. Fires Android a11y events.
   1.237 + */
   1.238 +
   1.239 +this.AndroidPresenter = function AndroidPresenter() {};
   1.240 +
   1.241 +AndroidPresenter.prototype = {
   1.242 +  __proto__: Presenter.prototype,
   1.243 +
   1.244 +  type: 'Android',
   1.245 +
   1.246 +  // Android AccessibilityEvent type constants.
   1.247 +  ANDROID_VIEW_CLICKED: 0x01,
   1.248 +  ANDROID_VIEW_LONG_CLICKED: 0x02,
   1.249 +  ANDROID_VIEW_SELECTED: 0x04,
   1.250 +  ANDROID_VIEW_FOCUSED: 0x08,
   1.251 +  ANDROID_VIEW_TEXT_CHANGED: 0x10,
   1.252 +  ANDROID_WINDOW_STATE_CHANGED: 0x20,
   1.253 +  ANDROID_VIEW_HOVER_ENTER: 0x80,
   1.254 +  ANDROID_VIEW_HOVER_EXIT: 0x100,
   1.255 +  ANDROID_VIEW_SCROLLED: 0x1000,
   1.256 +  ANDROID_VIEW_TEXT_SELECTION_CHANGED: 0x2000,
   1.257 +  ANDROID_ANNOUNCEMENT: 0x4000,
   1.258 +  ANDROID_VIEW_ACCESSIBILITY_FOCUSED: 0x8000,
   1.259 +  ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 0x20000,
   1.260 +
   1.261 +  pivotChanged: function AndroidPresenter_pivotChanged(aContext, aReason) {
   1.262 +    if (!aContext.accessible)
   1.263 +      return null;
   1.264 +
   1.265 +    let androidEvents = [];
   1.266 +
   1.267 +    let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT &&
   1.268 +                            Utils.AndroidSdkVersion >= 14);
   1.269 +    let focusEventType = (Utils.AndroidSdkVersion >= 16) ?
   1.270 +      this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED :
   1.271 +      this.ANDROID_VIEW_FOCUSED;
   1.272 +
   1.273 +    if (isExploreByTouch) {
   1.274 +      // This isn't really used by TalkBack so this is a half-hearted attempt
   1.275 +      // for now.
   1.276 +      androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
   1.277 +    }
   1.278 +
   1.279 +    let brailleOutput = {};
   1.280 +    if (Utils.AndroidSdkVersion >= 16) {
   1.281 +      if (!this._braillePresenter) {
   1.282 +        this._braillePresenter = new BraillePresenter();
   1.283 +      }
   1.284 +      brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason).
   1.285 +                         details;
   1.286 +    }
   1.287 +
   1.288 +    if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
   1.289 +      if (Utils.AndroidSdkVersion >= 16) {
   1.290 +        let adjustedText = aContext.textAndAdjustedOffsets;
   1.291 +
   1.292 +        androidEvents.push({
   1.293 +          eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
   1.294 +          text: [adjustedText.text],
   1.295 +          fromIndex: adjustedText.startOffset,
   1.296 +          toIndex: adjustedText.endOffset
   1.297 +        });
   1.298 +      }
   1.299 +    } else {
   1.300 +      let state = Utils.getState(aContext.accessible);
   1.301 +      androidEvents.push({eventType: (isExploreByTouch) ?
   1.302 +                           this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
   1.303 +                         text: UtteranceGenerator.genForContext(aContext).output,
   1.304 +                         bounds: aContext.bounds,
   1.305 +                         clickable: aContext.accessible.actionCount > 0,
   1.306 +                         checkable: state.contains(States.CHECKABLE),
   1.307 +                         checked: state.contains(States.CHECKED),
   1.308 +                         brailleOutput: brailleOutput});
   1.309 +    }
   1.310 +
   1.311 +
   1.312 +    return {
   1.313 +      type: this.type,
   1.314 +      details: androidEvents
   1.315 +    };
   1.316 +  },
   1.317 +
   1.318 +  actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) {
   1.319 +    let state = Utils.getState(aObject);
   1.320 +
   1.321 +    // Checkable objects will have a state changed event we will use instead.
   1.322 +    if (state.contains(States.CHECKABLE))
   1.323 +      return null;
   1.324 +
   1.325 +    return {
   1.326 +      type: this.type,
   1.327 +      details: [{
   1.328 +        eventType: this.ANDROID_VIEW_CLICKED,
   1.329 +        text: UtteranceGenerator.genForAction(aObject, aActionName),
   1.330 +        checked: state.contains(States.CHECKED)
   1.331 +      }]
   1.332 +    };
   1.333 +  },
   1.334 +
   1.335 +  tabSelected: function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
   1.336 +    // Send a pivot change message with the full context utterance for this doc.
   1.337 +    return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
   1.338 +  },
   1.339 +
   1.340 +  tabStateChanged: function AndroidPresenter_tabStateChanged(aDocObj,
   1.341 +                                                             aPageState) {
   1.342 +    return this.announce(
   1.343 +      UtteranceGenerator.genForTabStateChange(aDocObj, aPageState).join(' '));
   1.344 +  },
   1.345 +
   1.346 +  textChanged: function AndroidPresenter_textChanged(aIsInserted, aStart,
   1.347 +                                                     aLength, aText,
   1.348 +                                                     aModifiedText) {
   1.349 +    let eventDetails = {
   1.350 +      eventType: this.ANDROID_VIEW_TEXT_CHANGED,
   1.351 +      text: [aText],
   1.352 +      fromIndex: aStart,
   1.353 +      removedCount: 0,
   1.354 +      addedCount: 0
   1.355 +    };
   1.356 +
   1.357 +    if (aIsInserted) {
   1.358 +      eventDetails.addedCount = aLength;
   1.359 +      eventDetails.beforeText =
   1.360 +        aText.substring(0, aStart) + aText.substring(aStart + aLength);
   1.361 +    } else {
   1.362 +      eventDetails.removedCount = aLength;
   1.363 +      eventDetails.beforeText =
   1.364 +        aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
   1.365 +    }
   1.366 +
   1.367 +    return {type: this.type, details: [eventDetails]};
   1.368 +  },
   1.369 +
   1.370 +  textSelectionChanged: function AndroidPresenter_textSelectionChanged(aText, aStart,
   1.371 +                                                                       aEnd, aOldStart,
   1.372 +                                                                       aOldEnd, aIsFromUser) {
   1.373 +    let androidEvents = [];
   1.374 +
   1.375 +    if (Utils.AndroidSdkVersion >= 14 && !aIsFromUser) {
   1.376 +      if (!this._braillePresenter) {
   1.377 +        this._braillePresenter = new BraillePresenter();
   1.378 +      }
   1.379 +      let brailleOutput = this._braillePresenter.textSelectionChanged(aText, aStart, aEnd,
   1.380 +                                                                      aOldStart, aOldEnd,
   1.381 +                                                                      aIsFromUser).details;
   1.382 +
   1.383 +      androidEvents.push({
   1.384 +        eventType: this.ANDROID_VIEW_TEXT_SELECTION_CHANGED,
   1.385 +        text: [aText],
   1.386 +        fromIndex: aStart,
   1.387 +        toIndex: aEnd,
   1.388 +        itemCount: aText.length,
   1.389 +        brailleOutput: brailleOutput
   1.390 +      });
   1.391 +    }
   1.392 +
   1.393 +    if (Utils.AndroidSdkVersion >= 16 && aIsFromUser) {
   1.394 +      let [from, to] = aOldStart < aStart ? [aOldStart, aStart] : [aStart, aOldStart];
   1.395 +      androidEvents.push({
   1.396 +        eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
   1.397 +        text: [aText],
   1.398 +        fromIndex: from,
   1.399 +        toIndex: to
   1.400 +      });
   1.401 +    }
   1.402 +
   1.403 +    return {
   1.404 +      type: this.type,
   1.405 +      details: androidEvents
   1.406 +    };
   1.407 +  },
   1.408 +
   1.409 +  viewportChanged: function AndroidPresenter_viewportChanged(aWindow) {
   1.410 +    if (Utils.AndroidSdkVersion < 14)
   1.411 +      return null;
   1.412 +
   1.413 +    return {
   1.414 +      type: this.type,
   1.415 +      details: [{
   1.416 +        eventType: this.ANDROID_VIEW_SCROLLED,
   1.417 +        text: [],
   1.418 +        scrollX: aWindow.scrollX,
   1.419 +        scrollY: aWindow.scrollY,
   1.420 +        maxScrollX: aWindow.scrollMaxX,
   1.421 +        maxScrollY: aWindow.scrollMaxY
   1.422 +      }]
   1.423 +    };
   1.424 +  },
   1.425 +
   1.426 +  editingModeChanged: function AndroidPresenter_editingModeChanged(aIsEditing) {
   1.427 +    return this.announce(
   1.428 +      UtteranceGenerator.genForEditingMode(aIsEditing).join(' '));
   1.429 +  },
   1.430 +
   1.431 +  announce: function AndroidPresenter_announce(aAnnouncement) {
   1.432 +    return {
   1.433 +      type: this.type,
   1.434 +      details: [{
   1.435 +        eventType: (Utils.AndroidSdkVersion >= 16) ?
   1.436 +          this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
   1.437 +        text: [aAnnouncement],
   1.438 +        addedCount: aAnnouncement.length,
   1.439 +        removedCount: 0,
   1.440 +        fromIndex: 0
   1.441 +      }]
   1.442 +    };
   1.443 +  },
   1.444 +
   1.445 +  liveRegion: function AndroidPresenter_liveRegion(aContext, aIsPolite,
   1.446 +    aIsHide, aModifiedText) {
   1.447 +    return this.announce(
   1.448 +      UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
   1.449 +        aModifiedText).join(' '));
   1.450 +  }
   1.451 +};
   1.452 +
   1.453 +/**
   1.454 + * A speech presenter for direct TTS output
   1.455 + */
   1.456 +
   1.457 +this.SpeechPresenter = function SpeechPresenter() {};
   1.458 +
   1.459 +SpeechPresenter.prototype = {
   1.460 +  __proto__: Presenter.prototype,
   1.461 +
   1.462 +  type: 'Speech',
   1.463 +
   1.464 +  pivotChanged: function SpeechPresenter_pivotChanged(aContext, aReason) {
   1.465 +    if (!aContext.accessible)
   1.466 +      return null;
   1.467 +
   1.468 +    return {
   1.469 +      type: this.type,
   1.470 +      details: {
   1.471 +        actions: [
   1.472 +          {method: 'playEarcon',
   1.473 +           data: aContext.accessible.role === Roles.KEY ?
   1.474 +             'virtual_cursor_key' : 'virtual_cursor_move',
   1.475 +           options: {}},
   1.476 +          {method: 'speak',
   1.477 +            data: UtteranceGenerator.genForContext(aContext).output.join(' '),
   1.478 +            options: {enqueue: true}}
   1.479 +        ]
   1.480 +      }
   1.481 +    };
   1.482 +  },
   1.483 +
   1.484 +  valueChanged: function SpeechPresenter_valueChanged(aAccessible) {
   1.485 +    return {
   1.486 +      type: this.type,
   1.487 +      details: {
   1.488 +        actions: [
   1.489 +          { method: 'speak',
   1.490 +            data: aAccessible.value,
   1.491 +            options: { enqueue: false } }
   1.492 +        ]
   1.493 +      }
   1.494 +    }
   1.495 +  },
   1.496 +
   1.497 +  actionInvoked: function SpeechPresenter_actionInvoked(aObject, aActionName) {
   1.498 +    let actions = [];
   1.499 +    if (aActionName === 'click') {
   1.500 +      actions.push({method: 'playEarcon',
   1.501 +                    data: 'clicked',
   1.502 +                    options: {}});
   1.503 +    } else {
   1.504 +      actions.push({method: 'speak',
   1.505 +                    data: UtteranceGenerator.genForAction(aObject, aActionName).join(' '),
   1.506 +                    options: {enqueue: false}});
   1.507 +    }
   1.508 +    return { type: this.type, details: { actions: actions } };
   1.509 +  },
   1.510 +
   1.511 +  liveRegion: function SpeechPresenter_liveRegion(aContext, aIsPolite, aIsHide,
   1.512 +    aModifiedText) {
   1.513 +    return {
   1.514 +      type: this.type,
   1.515 +      details: {
   1.516 +        actions: [{
   1.517 +          method: 'speak',
   1.518 +          data: UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
   1.519 +            aModifiedText).join(' '),
   1.520 +          options: {enqueue: aIsPolite}
   1.521 +        }]
   1.522 +      }
   1.523 +    };
   1.524 +  },
   1.525 +
   1.526 +  announce: function SpeechPresenter_announce(aAnnouncement) {
   1.527 +    return {
   1.528 +      type: this.type,
   1.529 +      details: {
   1.530 +        actions: [{
   1.531 +          method: 'speak', data: aAnnouncement, options: { enqueue: false }
   1.532 +        }]
   1.533 +      }
   1.534 +    };
   1.535 +  }
   1.536 +};
   1.537 +
   1.538 +/**
   1.539 + * A haptic presenter
   1.540 + */
   1.541 +
   1.542 +this.HapticPresenter = function HapticPresenter() {};
   1.543 +
   1.544 +HapticPresenter.prototype = {
   1.545 +  __proto__: Presenter.prototype,
   1.546 +
   1.547 +  type: 'Haptic',
   1.548 +
   1.549 +  PIVOT_CHANGE_PATTERN: [40],
   1.550 +
   1.551 +  pivotChanged: function HapticPresenter_pivotChanged(aContext, aReason) {
   1.552 +    return { type: this.type, details: { pattern: this.PIVOT_CHANGE_PATTERN } };
   1.553 +  }
   1.554 +};
   1.555 +
   1.556 +/**
   1.557 + * A braille presenter
   1.558 + */
   1.559 +
   1.560 +this.BraillePresenter = function BraillePresenter() {};
   1.561 +
   1.562 +BraillePresenter.prototype = {
   1.563 +  __proto__: Presenter.prototype,
   1.564 +
   1.565 +  type: 'Braille',
   1.566 +
   1.567 +  pivotChanged: function BraillePresenter_pivotChanged(aContext, aReason) {
   1.568 +    if (!aContext.accessible) {
   1.569 +      return null;
   1.570 +    }
   1.571 +
   1.572 +    let brailleOutput = BrailleGenerator.genForContext(aContext);
   1.573 +    brailleOutput.output = brailleOutput.output.join(' ');
   1.574 +    brailleOutput.selectionStart = 0;
   1.575 +    brailleOutput.selectionEnd = 0;
   1.576 +
   1.577 +    return { type: this.type, details: brailleOutput };
   1.578 +  },
   1.579 +
   1.580 +  textSelectionChanged: function BraillePresenter_textSelectionChanged(aText, aStart,
   1.581 +                                                                       aEnd, aOldStart,
   1.582 +                                                                       aOldEnd, aIsFromUser) {
   1.583 +    return { type: this.type,
   1.584 +             details: { selectionStart: aStart,
   1.585 +                        selectionEnd: aEnd } };
   1.586 +  },
   1.587 +
   1.588 +
   1.589 +};
   1.590 +
   1.591 +this.Presentation = {
   1.592 +  get presenters() {
   1.593 +    delete this.presenters;
   1.594 +    let presenterMap = {
   1.595 +      'mobile/android': [VisualPresenter, AndroidPresenter],
   1.596 +      'b2g': [VisualPresenter, SpeechPresenter, HapticPresenter],
   1.597 +      'browser': [VisualPresenter, SpeechPresenter, HapticPresenter,
   1.598 +                  AndroidPresenter]
   1.599 +    };
   1.600 +    this.presenters = [new P() for (P of presenterMap[Utils.MozBuildApp])];
   1.601 +    return this.presenters;
   1.602 +  },
   1.603 +
   1.604 +  pivotChanged: function Presentation_pivotChanged(aPosition, aOldPosition, aReason,
   1.605 +                                                   aStartOffset, aEndOffset) {
   1.606 +    let context = new PivotContext(aPosition, aOldPosition, aStartOffset, aEndOffset);
   1.607 +    return [p.pivotChanged(context, aReason)
   1.608 +              for each (p in this.presenters)];
   1.609 +  },
   1.610 +
   1.611 +  actionInvoked: function Presentation_actionInvoked(aObject, aActionName) {
   1.612 +    return [p.actionInvoked(aObject, aActionName)
   1.613 +              for each (p in this.presenters)];
   1.614 +  },
   1.615 +
   1.616 +  textChanged: function Presentation_textChanged(aIsInserted, aStartOffset,
   1.617 +                                    aLength, aText,
   1.618 +                                    aModifiedText) {
   1.619 +    return [p.textChanged(aIsInserted, aStartOffset, aLength,
   1.620 +                          aText, aModifiedText)
   1.621 +              for each (p in this.presenters)];
   1.622 +  },
   1.623 +
   1.624 +  textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd,
   1.625 +                                                      aOldStart, aOldEnd,
   1.626 +                                                      aIsFromUser) {
   1.627 +    return [p.textSelectionChanged(aText, aStart, aEnd,
   1.628 +                                   aOldStart, aOldEnd, aIsFromUser)
   1.629 +              for each (p in this.presenters)];
   1.630 +  },
   1.631 +
   1.632 +  valueChanged: function valueChanged(aAccessible) {
   1.633 +    return [ p.valueChanged(aAccessible) for (p of this.presenters) ];
   1.634 +  },
   1.635 +
   1.636 +  tabStateChanged: function Presentation_tabStateChanged(aDocObj, aPageState) {
   1.637 +    return [p.tabStateChanged(aDocObj, aPageState)
   1.638 +              for each (p in this.presenters)];
   1.639 +  },
   1.640 +
   1.641 +  viewportChanged: function Presentation_viewportChanged(aWindow) {
   1.642 +    return [p.viewportChanged(aWindow)
   1.643 +              for each (p in this.presenters)];
   1.644 +  },
   1.645 +
   1.646 +  editingModeChanged: function Presentation_editingModeChanged(aIsEditing) {
   1.647 +    return [p.editingModeChanged(aIsEditing)
   1.648 +              for each (p in this.presenters)];
   1.649 +  },
   1.650 +
   1.651 +  announce: function Presentation_announce(aAnnouncement) {
   1.652 +    // XXX: Typically each presenter uses the UtteranceGenerator,
   1.653 +    // but there really isn't a point here.
   1.654 +    return [p.announce(UtteranceGenerator.genForAnnouncement(aAnnouncement)[0])
   1.655 +              for each (p in this.presenters)];
   1.656 +  },
   1.657 +
   1.658 +  liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
   1.659 +    aModifiedText) {
   1.660 +    let context;
   1.661 +    if (!aModifiedText) {
   1.662 +      context = new PivotContext(aAccessible, null, -1, -1, true,
   1.663 +        aIsHide ? true : false);
   1.664 +    }
   1.665 +    return [p.liveRegion(context, aIsPolite, aIsHide, aModifiedText) for (
   1.666 +      p of this.presenters)];
   1.667 +  }
   1.668 +};

mercurial