/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/* global Components, XPCOMUtils, Utils, Logger, BraillePresenter, Presentation,
          UtteranceGenerator, BrailleGenerator, States, Roles, PivotContext */
/* exported Presentation */

'use strict';

const {utils: Cu, interfaces: Ci} = Components;

Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Logger', // jshint ignore:line
  'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PivotContext', // jshint ignore:line
  'resource://gre/modules/accessibility/Utils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'UtteranceGenerator', // jshint ignore:line
  'resource://gre/modules/accessibility/OutputGenerator.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'BrailleGenerator', // jshint ignore:line
  'resource://gre/modules/accessibility/OutputGenerator.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'States', // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');

this.EXPORTED_SYMBOLS = ['Presentation']; // jshint ignore:line

/**
 * The interface for all presenter classes. A presenter could be, for example,
 * a speech output module, or a visual cursor indicator.
 */
function Presenter() {}

Presenter.prototype = {
  /**
   * The type of presenter. Used for matching it with the appropriate output method.
   */
  type: 'Base',

  /**
   * The virtual cursor's position changed.
   * @param {PivotContext} aContext the context object for the new pivot
   *   position.
   * @param {int} aReason the reason for the pivot change.
   *   See nsIAccessiblePivot.
   * @param {bool} aIsFromUserInput the pivot change was invoked by the user
   */
  pivotChanged: function pivotChanged(aContext, aReason, aIsFromUserInput) {}, // jshint ignore:line

  /**
   * An object's action has been invoked.
   * @param {nsIAccessible} aObject the object that has been invoked.
   * @param {string} aActionName the name of the action.
   */
  actionInvoked: function actionInvoked(aObject, aActionName) {}, // jshint ignore:line

  /**
   * Text has changed, either by the user or by the system. TODO.
   */
  textChanged: function textChanged(aAccessible, aIsInserted, aStartOffset, // jshint ignore:line
                                    aLength, aText, aModifiedText) {}, // jshint ignore:line

  /**
   * Text selection has changed. TODO.
   */
  textSelectionChanged: function textSelectionChanged(
    aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput) {}, // jshint ignore:line

  /**
   * Selection has changed. TODO.
   * @param {nsIAccessible} aObject the object that has been selected.
   */
  selectionChanged: function selectionChanged(aObject) {}, // jshint ignore:line

  /**
   * Name has changed.
   * @param {nsIAccessible} aAccessible the object whose value has changed.
   */
  nameChanged: function nameChanged(aAccessible) {}, // jshint ignore: line

  /**
   * Value has changed.
   * @param {nsIAccessible} aAccessible the object whose value has changed.
   */
  valueChanged: function valueChanged(aAccessible) {}, // jshint ignore:line

  /**
   * The tab, or the tab's document state has changed.
   * @param {nsIAccessible} aDocObj the tab document accessible that has had its
   *    state changed, or null if the tab has no associated document yet.
   * @param {string} aPageState the state name for the tab, valid states are:
   *    'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
   */
  tabStateChanged: function tabStateChanged(aDocObj, aPageState) {}, // jshint ignore:line

  /**
   * The current tab has changed.
   * @param {PivotContext} aDocContext context object for tab's
   *   document.
   * @param {PivotContext} aVCContext context object for tab's current
   *   virtual cursor position.
   */
  tabSelected: function tabSelected(aDocContext, aVCContext) {}, // jshint ignore:line

  /**
   * The viewport has changed, either a scroll, pan, zoom, or
   *    landscape/portrait toggle.
   * @param {Window} aWindow window of viewport that changed.
   * @param {PivotContext} aCurrentContext context of last pivot change.
   */
  viewportChanged: function viewportChanged(aWindow, aCurrentContext) {}, // jshint ignore:line

  /**
   * We have entered or left text editing mode.
   */
  editingModeChanged: function editingModeChanged(aIsEditing) {}, // jshint ignore:line

  /**
   * Announce something. Typically an app state change.
   */
  announce: function announce(aAnnouncement) {}, // jshint ignore:line


  /**
   * User tried to move cursor forward or backward with no success.
   * @param {string} aMoveMethod move method that was used (eg. 'moveNext').
   */
  noMove: function noMove(aMoveMethod) {},

  /**
   * Announce a live region.
   * @param  {PivotContext} aContext context object for an accessible.
   * @param  {boolean} aIsPolite A politeness level for a live region.
   * @param  {boolean} aIsHide An indicator of hide/remove event.
   * @param  {string} aModifiedText Optional modified text.
   */
  liveRegion: function liveRegionShown(aContext, aIsPolite, aIsHide, // jshint ignore:line
    aModifiedText) {} // jshint ignore:line
};

/**
 * Visual presenter. Draws a box around the virtual cursor's position.
 */
function VisualPresenter() {}

VisualPresenter.prototype = Object.create(Presenter.prototype);

VisualPresenter.prototype.type = 'Visual';

/**
 * The padding in pixels between the object and the highlight border.
 */
VisualPresenter.prototype.BORDER_PADDING = 2;

VisualPresenter.prototype.viewportChanged =
  function VisualPresenter_viewportChanged(aWindow, aCurrentContext) {
    if (!aCurrentContext) {
      return null;
    }

    let currentAcc = aCurrentContext.accessibleForBounds;
    let start = aCurrentContext.startOffset;
    let end = aCurrentContext.endOffset;
    if (Utils.isAliveAndVisible(currentAcc)) {
      let bounds = (start === -1 && end === -1) ? Utils.getBounds(currentAcc) :
                   Utils.getTextBounds(currentAcc, start, end);

      return {
        type: this.type,
        details: {
          eventType: 'viewport-change',
          bounds: bounds,
          padding: this.BORDER_PADDING
        }
      };
    }

    return null;
  };

VisualPresenter.prototype.pivotChanged =
  function VisualPresenter_pivotChanged(aContext) {
    if (!aContext.accessible) {
      // XXX: Don't hide because another vc may be using the highlight.
      return null;
    }

    try {
      aContext.accessibleForBounds.scrollTo(
        Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);

      let bounds = (aContext.startOffset === -1 && aContext.endOffset === -1) ?
            aContext.bounds : Utils.getTextBounds(aContext.accessibleForBounds,
                                                  aContext.startOffset,
                                                  aContext.endOffset);

      return {
        type: this.type,
        details: {
          eventType: 'vc-change',
          bounds: bounds,
          padding: this.BORDER_PADDING
        }
      };
    } catch (e) {
      Logger.logException(e, 'Failed to get bounds');
      return null;
    }
  };

VisualPresenter.prototype.tabSelected =
  function VisualPresenter_tabSelected(aDocContext, aVCContext) {
    return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
  };

VisualPresenter.prototype.tabStateChanged =
  function VisualPresenter_tabStateChanged(aDocObj, aPageState) {
    if (aPageState == 'newdoc') {
      return {type: this.type, details: {eventType: 'tabstate-change'}};
    }

    return null;
  };

/**
 * Android presenter. Fires Android a11y events.
 */
function AndroidPresenter() {}

AndroidPresenter.prototype = Object.create(Presenter.prototype);

AndroidPresenter.prototype.type = 'Android';

// Android AccessibilityEvent type constants.
AndroidPresenter.prototype.ANDROID_VIEW_CLICKED = 0x01;
AndroidPresenter.prototype.ANDROID_VIEW_LONG_CLICKED = 0x02;
AndroidPresenter.prototype.ANDROID_VIEW_SELECTED = 0x04;
AndroidPresenter.prototype.ANDROID_VIEW_FOCUSED = 0x08;
AndroidPresenter.prototype.ANDROID_VIEW_TEXT_CHANGED = 0x10;
AndroidPresenter.prototype.ANDROID_WINDOW_STATE_CHANGED = 0x20;
AndroidPresenter.prototype.ANDROID_VIEW_HOVER_ENTER = 0x80;
AndroidPresenter.prototype.ANDROID_VIEW_HOVER_EXIT = 0x100;
AndroidPresenter.prototype.ANDROID_VIEW_SCROLLED = 0x1000;
AndroidPresenter.prototype.ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
AndroidPresenter.prototype.ANDROID_ANNOUNCEMENT = 0x4000;
AndroidPresenter.prototype.ANDROID_VIEW_ACCESSIBILITY_FOCUSED = 0x8000;
AndroidPresenter.prototype.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY =
  0x20000;

AndroidPresenter.prototype.pivotChanged =
  function AndroidPresenter_pivotChanged(aContext, aReason) {
    if (!aContext.accessible) {
      return null;
    }

    let androidEvents = [];

    let isExploreByTouch = (aReason == Ci.nsIAccessiblePivot.REASON_POINT &&
                            Utils.AndroidSdkVersion >= 14);
    let focusEventType = (Utils.AndroidSdkVersion >= 16) ?
      this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED :
      this.ANDROID_VIEW_FOCUSED;

    if (isExploreByTouch) {
      // This isn't really used by TalkBack so this is a half-hearted attempt
      // for now.
      androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []});
    }

    let brailleOutput = {};
    if (Utils.AndroidSdkVersion >= 16) {
      if (!this._braillePresenter) {
        this._braillePresenter = new BraillePresenter();
      }
      brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason).
                         details;
    }

    if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) {
      if (Utils.AndroidSdkVersion >= 16) {
        let adjustedText = aContext.textAndAdjustedOffsets;

        androidEvents.push({
          eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
          text: [adjustedText.text],
          fromIndex: adjustedText.startOffset,
          toIndex: adjustedText.endOffset
        });
      }
    } else {
      let state = Utils.getState(aContext.accessible);
      androidEvents.push({eventType: (isExploreByTouch) ?
                           this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
                         text: Utils.localize(UtteranceGenerator.genForContext(
                           aContext)),
                         bounds: aContext.bounds,
                         clickable: aContext.accessible.actionCount > 0,
                         checkable: state.contains(States.CHECKABLE),
                         checked: state.contains(States.CHECKED),
                         brailleOutput: brailleOutput});
    }


    return {
      type: this.type,
      details: androidEvents
    };
  };

AndroidPresenter.prototype.actionInvoked =
  function AndroidPresenter_actionInvoked(aObject, aActionName) {
    let state = Utils.getState(aObject);

    // Checkable objects use TalkBack's text derived from the event state,
    // so we don't populate the text here.
    let text = '';
    if (!state.contains(States.CHECKABLE)) {
      text = Utils.localize(UtteranceGenerator.genForAction(aObject,
        aActionName));
    }

    return {
      type: this.type,
      details: [{
        eventType: this.ANDROID_VIEW_CLICKED,
        text: text,
        checked: state.contains(States.CHECKED)
      }]
    };
  };

AndroidPresenter.prototype.tabSelected =
  function AndroidPresenter_tabSelected(aDocContext, aVCContext) {
    // Send a pivot change message with the full context utterance for this doc.
    return this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
  };

AndroidPresenter.prototype.tabStateChanged =
  function AndroidPresenter_tabStateChanged(aDocObj, aPageState) {
    return this.announce(
      UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
  };

AndroidPresenter.prototype.textChanged = function AndroidPresenter_textChanged(
  aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
    let eventDetails = {
      eventType: this.ANDROID_VIEW_TEXT_CHANGED,
      text: [aText],
      fromIndex: aStart,
      removedCount: 0,
      addedCount: 0
    };

    if (aIsInserted) {
      eventDetails.addedCount = aLength;
      eventDetails.beforeText =
        aText.substring(0, aStart) + aText.substring(aStart + aLength);
    } else {
      eventDetails.removedCount = aLength;
      eventDetails.beforeText =
        aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
    }

    return {type: this.type, details: [eventDetails]};
  };

AndroidPresenter.prototype.textSelectionChanged =
  function AndroidPresenter_textSelectionChanged(aText, aStart, aEnd, aOldStart,
                                                 aOldEnd, aIsFromUserInput) {
    let androidEvents = [];

    if (Utils.AndroidSdkVersion >= 14 && !aIsFromUserInput) {
      if (!this._braillePresenter) {
        this._braillePresenter = new BraillePresenter();
      }
      let brailleOutput = this._braillePresenter.textSelectionChanged(
        aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput).details;

      androidEvents.push({
        eventType: this.ANDROID_VIEW_TEXT_SELECTION_CHANGED,
        text: [aText],
        fromIndex: aStart,
        toIndex: aEnd,
        itemCount: aText.length,
        brailleOutput: brailleOutput
      });
    }

    if (Utils.AndroidSdkVersion >= 16 && aIsFromUserInput) {
      let [from, to] = aOldStart < aStart ?
        [aOldStart, aStart] : [aStart, aOldStart];
      androidEvents.push({
        eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
        text: [aText],
        fromIndex: from,
        toIndex: to
      });
    }

    return {
      type: this.type,
      details: androidEvents
    };
  };

AndroidPresenter.prototype.viewportChanged =
  function AndroidPresenter_viewportChanged(aWindow, aCurrentContext) {
    if (Utils.AndroidSdkVersion < 14) {
      return null;
    }

    let events = [{
      eventType: this.ANDROID_VIEW_SCROLLED,
      text: [],
      scrollX: aWindow.scrollX,
      scrollY: aWindow.scrollY,
      maxScrollX: aWindow.scrollMaxX,
      maxScrollY: aWindow.scrollMaxY
    }];

    if (Utils.AndroidSdkVersion >= 16 && aCurrentContext) {
      let currentAcc = aCurrentContext.accessibleForBounds;
      if (Utils.isAliveAndVisible(currentAcc)) {
        events.push({
          eventType: this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED,
          bounds: Utils.getBounds(currentAcc)
        });
      }
    }

    return {
      type: this.type,
      details: events
    };
  };

AndroidPresenter.prototype.editingModeChanged =
  function AndroidPresenter_editingModeChanged(aIsEditing) {
    return this.announce(UtteranceGenerator.genForEditingMode(aIsEditing));
  };

AndroidPresenter.prototype.announce =
  function AndroidPresenter_announce(aAnnouncement) {
    let localizedAnnouncement = Utils.localize(aAnnouncement).join(' ');
    return {
      type: this.type,
      details: [{
        eventType: (Utils.AndroidSdkVersion >= 16) ?
          this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
        text: [localizedAnnouncement],
        addedCount: localizedAnnouncement.length,
        removedCount: 0,
        fromIndex: 0
      }]
    };
  };

AndroidPresenter.prototype.liveRegion =
  function AndroidPresenter_liveRegion(aContext, aIsPolite,
    aIsHide, aModifiedText) {
    return this.announce(
      UtteranceGenerator.genForLiveRegion(aContext, aIsHide, aModifiedText));
  };

AndroidPresenter.prototype.noMove =
  function AndroidPresenter_noMove(aMoveMethod) {
    return {
      type: this.type,
      details: [
      { eventType: this.ANDROID_VIEW_ACCESSIBILITY_FOCUSED,
        exitView: aMoveMethod,
        text: ['']
      }]
    };
  };

/**
 * A B2G presenter for Gaia.
 */
function B2GPresenter() {}

B2GPresenter.prototype = Object.create(Presenter.prototype);

B2GPresenter.prototype.type = 'B2G';

B2GPresenter.prototype.keyboardEchoSetting =
  new PrefCache('accessibility.accessfu.keyboard_echo');
B2GPresenter.prototype.NO_ECHO = 0;
B2GPresenter.prototype.CHARACTER_ECHO = 1;
B2GPresenter.prototype.WORD_ECHO = 2;
B2GPresenter.prototype.CHARACTER_AND_WORD_ECHO = 3;

/**
 * A pattern used for haptic feedback.
 * @type {Array}
 */
B2GPresenter.prototype.PIVOT_CHANGE_HAPTIC_PATTERN = [40];

/**
 * Pivot move reasons.
 * @type {Array}
 */
B2GPresenter.prototype.pivotChangedReasons = ['none', 'next', 'prev', 'first',
                                              'last', 'text', 'point'];

B2GPresenter.prototype.pivotChanged =
  function B2GPresenter_pivotChanged(aContext, aReason, aIsUserInput) {
    if (!aContext.accessible) {
      return null;
    }

    return {
      type: this.type,
      details: {
        eventType: 'vc-change',
        data: UtteranceGenerator.genForContext(aContext),
        options: {
          pattern: this.PIVOT_CHANGE_HAPTIC_PATTERN,
          isKey: Utils.isActivatableOnFingerUp(aContext.accessible),
          reason: this.pivotChangedReasons[aReason],
          isUserInput: aIsUserInput,
          hints: aContext.interactionHints
        }
      }
    };
  };

B2GPresenter.prototype.nameChanged =
  function B2GPresenter_nameChanged(aAccessible, aIsPolite = true) {
    return {
      type: this.type,
      details: {
        eventType: 'name-change',
        data: aAccessible.name,
        options: {enqueue: aIsPolite}
      }
    };
  };

B2GPresenter.prototype.valueChanged =
  function B2GPresenter_valueChanged(aAccessible, aIsPolite = true) {

    // the editable value changes are handled in the text changed presenter
    if (Utils.getState(aAccessible).contains(States.EDITABLE)) {
      return null;
    }

    return {
      type: this.type,
      details: {
        eventType: 'value-change',
        data: aAccessible.value,
        options: {enqueue: aIsPolite}
      }
    };
  };

B2GPresenter.prototype.textChanged = function B2GPresenter_textChanged(
  aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
    let echoSetting = this.keyboardEchoSetting.value;
    let text = '';

    if (echoSetting == this.CHARACTER_ECHO ||
        echoSetting == this.CHARACTER_AND_WORD_ECHO) {
      text = aModifiedText;
    }

    // add word if word boundary is added
    if ((echoSetting == this.WORD_ECHO ||
        echoSetting == this.CHARACTER_AND_WORD_ECHO) &&
        aIsInserted && aLength === 1) {
      let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
      let startBefore = {}, endBefore = {};
      let startAfter = {}, endAfter = {};
      accText.getTextBeforeOffset(aStart,
        Ci.nsIAccessibleText.BOUNDARY_WORD_END, startBefore, endBefore);
      let maybeWord = accText.getTextBeforeOffset(aStart + 1,
        Ci.nsIAccessibleText.BOUNDARY_WORD_END, startAfter, endAfter);
      if (endBefore.value !== endAfter.value) {
        text += maybeWord;
      }
    }

    return {
      type: this.type,
      details: {
        eventType: 'text-change',
        data: text
      }
    };

  };

B2GPresenter.prototype.actionInvoked =
  function B2GPresenter_actionInvoked(aObject, aActionName) {
    return {
      type: this.type,
      details: {
        eventType: 'action',
        data: UtteranceGenerator.genForAction(aObject, aActionName)
      }
    };
  };

B2GPresenter.prototype.liveRegion = function B2GPresenter_liveRegion(aContext,
  aIsPolite, aIsHide, aModifiedText) {
    return {
      type: this.type,
      details: {
        eventType: 'liveregion-change',
        data: UtteranceGenerator.genForLiveRegion(aContext, aIsHide,
          aModifiedText),
        options: {enqueue: aIsPolite}
      }
    };
  };

B2GPresenter.prototype.announce =
  function B2GPresenter_announce(aAnnouncement) {
    return {
      type: this.type,
      details: {
        eventType: 'announcement',
        data: aAnnouncement
      }
    };
  };

B2GPresenter.prototype.noMove =
  function B2GPresenter_noMove(aMoveMethod) {
    return {
      type: this.type,
      details: {
        eventType: 'no-move',
        data: aMoveMethod
      }
    };
  };

/**
 * A braille presenter
 */
function BraillePresenter() {}

BraillePresenter.prototype = Object.create(Presenter.prototype);

BraillePresenter.prototype.type = 'Braille';

BraillePresenter.prototype.pivotChanged =
  function BraillePresenter_pivotChanged(aContext) {
    if (!aContext.accessible) {
      return null;
    }

    return {
      type: this.type,
      details: {
        output: Utils.localize(BrailleGenerator.genForContext(aContext)).join(
          ' '),
        selectionStart: 0,
        selectionEnd: 0
      }
    };
  };

BraillePresenter.prototype.textSelectionChanged =
  function BraillePresenter_textSelectionChanged(aText, aStart, aEnd) {
    return {
      type: this.type,
      details: {
        selectionStart: aStart,
        selectionEnd: aEnd
      }
    };
  };

this.Presentation = { // jshint ignore:line
  get presenters() {
    delete this.presenters;
    let presenterMap = {
      'mobile/android': [VisualPresenter, AndroidPresenter],
      'b2g': [VisualPresenter, B2GPresenter],
      'browser': [VisualPresenter, B2GPresenter, AndroidPresenter]
    };
    this.presenters = presenterMap[Utils.MozBuildApp].map(P => new P());
    return this.presenters;
  },

  get displayedAccessibles() {
    delete this.displayedAccessibles;
    this.displayedAccessibles = new WeakMap();
    return this.displayedAccessibles;
  },

  pivotChanged: function Presentation_pivotChanged(
    aPosition, aOldPosition, aReason, aStartOffset, aEndOffset, aIsUserInput) {
    let context = new PivotContext(
      aPosition, aOldPosition, aStartOffset, aEndOffset);
    if (context.accessible) {
      this.displayedAccessibles.set(context.accessible.document.window, context);
    }

    return this.presenters.map(p => p.pivotChanged(context, aReason, aIsUserInput));
  },

  actionInvoked: function Presentation_actionInvoked(aObject, aActionName) {
    return this.presenters.map(p => p.actionInvoked(aObject, aActionName));
  },

  textChanged: function Presentation_textChanged(aAccessible, aIsInserted,
                                    aStartOffset, aLength, aText,
                                    aModifiedText) {
    return this.presenters.map(p => p.textChanged(aAccessible, aIsInserted,
                                                  aStartOffset, aLength,
                                                  aText, aModifiedText));
  },

  textSelectionChanged: function textSelectionChanged(aText, aStart, aEnd,
                                                      aOldStart, aOldEnd,
                                                      aIsFromUserInput) {
    return this.presenters.map(p => p.textSelectionChanged(aText, aStart, aEnd,
                                                           aOldStart, aOldEnd,
                                                           aIsFromUserInput));
  },

  nameChanged: function nameChanged(aAccessible) {
    return this.presenters.map(p => p.nameChanged(aAccessible));
  },

  valueChanged: function valueChanged(aAccessible) {
    return this.presenters.map(p => p.valueChanged(aAccessible));
  },

  tabStateChanged: function Presentation_tabStateChanged(aDocObj, aPageState) {
    return this.presenters.map(p => p.tabStateChanged(aDocObj, aPageState));
  },

  viewportChanged: function Presentation_viewportChanged(aWindow) {
    let context = this.displayedAccessibles.get(aWindow);
    return this.presenters.map(p => p.viewportChanged(aWindow, context));
  },

  editingModeChanged: function Presentation_editingModeChanged(aIsEditing) {
    return this.presenters.map(p => p.editingModeChanged(aIsEditing));
  },

  announce: function Presentation_announce(aAnnouncement) {
    // XXX: Typically each presenter uses the UtteranceGenerator,
    // but there really isn't a point here.
    return this.presenters.map(p => p.announce(UtteranceGenerator.genForAnnouncement(aAnnouncement)));
  },

  noMove: function Presentation_noMove(aMoveMethod) {
    return this.presenters.map(p => p.noMove(aMoveMethod));
  },

  liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
    aModifiedText) {
    let context;
    if (!aModifiedText) {
      context = new PivotContext(aAccessible, null, -1, -1, true,
        aIsHide ? true : false);
    }
    return this.presenters.map(p => p.liveRegion(context, aIsPolite, aIsHide,
                                                 aModifiedText));
  }
};