accessible/src/jsat/EventManager.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/accessible/src/jsat/EventManager.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,612 @@
     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 Ci = Components.interfaces;
    1.11 +const Cu = Components.utils;
    1.12 +
    1.13 +const TEXT_NODE = 3;
    1.14 +
    1.15 +Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    1.16 +XPCOMUtils.defineLazyModuleGetter(this, 'Services',
    1.17 +  'resource://gre/modules/Services.jsm');
    1.18 +XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
    1.19 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.20 +XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
    1.21 +  'resource://gre/modules/accessibility/Utils.jsm');
    1.22 +XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
    1.23 +  'resource://gre/modules/accessibility/Presentation.jsm');
    1.24 +XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
    1.25 +  'resource://gre/modules/accessibility/TraversalRules.jsm');
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
    1.27 +  'resource://gre/modules/accessibility/Constants.jsm');
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, 'Events',
    1.29 +  'resource://gre/modules/accessibility/Constants.jsm');
    1.30 +XPCOMUtils.defineLazyModuleGetter(this, 'States',
    1.31 +  'resource://gre/modules/accessibility/Constants.jsm');
    1.32 +
    1.33 +this.EXPORTED_SYMBOLS = ['EventManager'];
    1.34 +
    1.35 +this.EventManager = function EventManager(aContentScope) {
    1.36 +  this.contentScope = aContentScope;
    1.37 +  this.addEventListener = this.contentScope.addEventListener.bind(
    1.38 +    this.contentScope);
    1.39 +  this.removeEventListener = this.contentScope.removeEventListener.bind(
    1.40 +    this.contentScope);
    1.41 +  this.sendMsgFunc = this.contentScope.sendAsyncMessage.bind(
    1.42 +    this.contentScope);
    1.43 +  this.webProgress = this.contentScope.docShell.
    1.44 +    QueryInterface(Ci.nsIInterfaceRequestor).
    1.45 +    getInterface(Ci.nsIWebProgress);
    1.46 +};
    1.47 +
    1.48 +this.EventManager.prototype = {
    1.49 +  editState: {},
    1.50 +
    1.51 +  start: function start() {
    1.52 +    try {
    1.53 +      if (!this._started) {
    1.54 +        Logger.debug('EventManager.start');
    1.55 +
    1.56 +        this._started = true;
    1.57 +
    1.58 +        AccessibilityEventObserver.addListener(this);
    1.59 +
    1.60 +        this.webProgress.addProgressListener(this,
    1.61 +          (Ci.nsIWebProgress.NOTIFY_STATE_ALL |
    1.62 +           Ci.nsIWebProgress.NOTIFY_LOCATION));
    1.63 +        this.addEventListener('wheel', this, true);
    1.64 +        this.addEventListener('scroll', this, true);
    1.65 +        this.addEventListener('resize', this, true);
    1.66 +      }
    1.67 +      this.present(Presentation.tabStateChanged(null, 'newtab'));
    1.68 +
    1.69 +    } catch (x) {
    1.70 +      Logger.logException(x, 'Failed to start EventManager');
    1.71 +    }
    1.72 +  },
    1.73 +
    1.74 +  // XXX: Stop is not called when the tab is closed (|TabClose| event is too
    1.75 +  // late). It is only called when the AccessFu is disabled explicitly.
    1.76 +  stop: function stop() {
    1.77 +    if (!this._started) {
    1.78 +      return;
    1.79 +    }
    1.80 +    Logger.debug('EventManager.stop');
    1.81 +    AccessibilityEventObserver.removeListener(this);
    1.82 +    try {
    1.83 +      this.webProgress.removeProgressListener(this);
    1.84 +      this.removeEventListener('wheel', this, true);
    1.85 +      this.removeEventListener('scroll', this, true);
    1.86 +      this.removeEventListener('resize', this, true);
    1.87 +    } catch (x) {
    1.88 +      // contentScope is dead.
    1.89 +    } finally {
    1.90 +      this._started = false;
    1.91 +    }
    1.92 +  },
    1.93 +
    1.94 +  handleEvent: function handleEvent(aEvent) {
    1.95 +    Logger.debug(() => {
    1.96 +      return ['DOMEvent', aEvent.type];
    1.97 +    });
    1.98 +
    1.99 +    try {
   1.100 +      switch (aEvent.type) {
   1.101 +      case 'wheel':
   1.102 +      {
   1.103 +        let attempts = 0;
   1.104 +        let delta = aEvent.deltaX || aEvent.deltaY;
   1.105 +        this.contentScope.contentControl.autoMove(
   1.106 +         null,
   1.107 +         { moveMethod: delta > 0 ? 'moveNext' : 'movePrevious',
   1.108 +           onScreenOnly: true, noOpIfOnScreen: true, delay: 500 });
   1.109 +        break;
   1.110 +      }
   1.111 +      case 'scroll':
   1.112 +      case 'resize':
   1.113 +      {
   1.114 +        // the target could be an element, document or window
   1.115 +        let window = null;
   1.116 +        if (aEvent.target instanceof Ci.nsIDOMWindow)
   1.117 +          window = aEvent.target;
   1.118 +        else if (aEvent.target instanceof Ci.nsIDOMDocument)
   1.119 +          window = aEvent.target.defaultView;
   1.120 +        else if (aEvent.target instanceof Ci.nsIDOMElement)
   1.121 +          window = aEvent.target.ownerDocument.defaultView;
   1.122 +        this.present(Presentation.viewportChanged(window));
   1.123 +        break;
   1.124 +      }
   1.125 +      }
   1.126 +    } catch (x) {
   1.127 +      Logger.logException(x, 'Error handling DOM event');
   1.128 +    }
   1.129 +  },
   1.130 +
   1.131 +  handleAccEvent: function handleAccEvent(aEvent) {
   1.132 +    Logger.debug(() => {
   1.133 +      return ['A11yEvent', Logger.eventToString(aEvent),
   1.134 +              Logger.accessibleToString(aEvent.accessible)];
   1.135 +    });
   1.136 +
   1.137 +    // Don't bother with non-content events in firefox.
   1.138 +    if (Utils.MozBuildApp == 'browser' &&
   1.139 +        aEvent.eventType != Events.VIRTUALCURSOR_CHANGED &&
   1.140 +        // XXX Bug 442005 results in DocAccessible::getDocType returning
   1.141 +        // NS_ERROR_FAILURE. Checking for aEvent.accessibleDocument.docType ==
   1.142 +        // 'window' does not currently work.
   1.143 +        (aEvent.accessibleDocument.DOMDocument.doctype &&
   1.144 +         aEvent.accessibleDocument.DOMDocument.doctype.name === 'window')) {
   1.145 +      return;
   1.146 +    }
   1.147 +
   1.148 +    switch (aEvent.eventType) {
   1.149 +      case Events.VIRTUALCURSOR_CHANGED:
   1.150 +      {
   1.151 +        let pivot = aEvent.accessible.
   1.152 +          QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
   1.153 +        let position = pivot.position;
   1.154 +        if (position && position.role == Roles.INTERNAL_FRAME)
   1.155 +          break;
   1.156 +        let event = aEvent.
   1.157 +          QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
   1.158 +        let reason = event.reason;
   1.159 +        let oldAccessible = event.oldAccessible;
   1.160 +
   1.161 +        if (this.editState.editing) {
   1.162 +          aEvent.accessibleDocument.takeFocus();
   1.163 +        }
   1.164 +        this.present(
   1.165 +          Presentation.pivotChanged(position, oldAccessible, reason,
   1.166 +                                    pivot.startOffset, pivot.endOffset));
   1.167 +
   1.168 +        break;
   1.169 +      }
   1.170 +      case Events.STATE_CHANGE:
   1.171 +      {
   1.172 +        let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
   1.173 +        let state = Utils.getState(event);
   1.174 +        if (state.contains(States.CHECKED)) {
   1.175 +          this.present(
   1.176 +            Presentation.
   1.177 +              actionInvoked(aEvent.accessible,
   1.178 +                            event.isEnabled ? 'check' : 'uncheck'));
   1.179 +        } else if (state.contains(States.SELECTED)) {
   1.180 +          this.present(
   1.181 +            Presentation.
   1.182 +              actionInvoked(aEvent.accessible,
   1.183 +                            event.isEnabled ? 'select' : 'unselect'));
   1.184 +        }
   1.185 +        break;
   1.186 +      }
   1.187 +      case Events.SCROLLING_START:
   1.188 +      {
   1.189 +        this.contentScope.contentControl.autoMove(aEvent.accessible);
   1.190 +        break;
   1.191 +      }
   1.192 +      case Events.TEXT_CARET_MOVED:
   1.193 +      {
   1.194 +        let acc = aEvent.accessible;
   1.195 +        let characterCount = acc.
   1.196 +          QueryInterface(Ci.nsIAccessibleText).characterCount;
   1.197 +        let caretOffset = aEvent.
   1.198 +          QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
   1.199 +
   1.200 +        // Update editing state, both for presenter and other things
   1.201 +        let state = Utils.getState(acc);
   1.202 +        let editState = {
   1.203 +          editing: state.contains(States.EDITABLE),
   1.204 +          multiline: state.contains(States.MULTI_LINE),
   1.205 +          atStart: caretOffset == 0,
   1.206 +          atEnd: caretOffset == characterCount
   1.207 +        };
   1.208 +
   1.209 +        // Not interesting
   1.210 +        if (!editState.editing && editState.editing == this.editState.editing)
   1.211 +          break;
   1.212 +
   1.213 +        if (editState.editing != this.editState.editing)
   1.214 +          this.present(Presentation.editingModeChanged(editState.editing));
   1.215 +
   1.216 +        if (editState.editing != this.editState.editing ||
   1.217 +            editState.multiline != this.editState.multiline ||
   1.218 +            editState.atEnd != this.editState.atEnd ||
   1.219 +            editState.atStart != this.editState.atStart)
   1.220 +          this.sendMsgFunc("AccessFu:Input", editState);
   1.221 +
   1.222 +        this.present(Presentation.textSelectionChanged(acc.getText(0,-1),
   1.223 +                     caretOffset, caretOffset, 0, 0, aEvent.isFromUserInput));
   1.224 +
   1.225 +        this.editState = editState;
   1.226 +        break;
   1.227 +      }
   1.228 +      case Events.SHOW:
   1.229 +      {
   1.230 +        let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
   1.231 +          ['additions', 'all']);
   1.232 +        // Only handle show if it is a relevant live region.
   1.233 +        if (!liveRegion) {
   1.234 +          break;
   1.235 +        }
   1.236 +        // Show for text is handled by the EVENT_TEXT_INSERTED handler.
   1.237 +        if (aEvent.accessible.role === Roles.TEXT_LEAF) {
   1.238 +          break;
   1.239 +        }
   1.240 +        this._dequeueLiveEvent(Events.HIDE, liveRegion);
   1.241 +        this.present(Presentation.liveRegion(liveRegion, isPolite, false));
   1.242 +        break;
   1.243 +      }
   1.244 +      case Events.HIDE:
   1.245 +      {
   1.246 +        let evt = aEvent.QueryInterface(Ci.nsIAccessibleHideEvent);
   1.247 +        let {liveRegion, isPolite} = this._handleLiveRegion(
   1.248 +          evt, ['removals', 'all']);
   1.249 +        if (liveRegion) {
   1.250 +          // Hide for text is handled by the EVENT_TEXT_REMOVED handler.
   1.251 +          if (aEvent.accessible.role === Roles.TEXT_LEAF) {
   1.252 +            break;
   1.253 +          }
   1.254 +          this._queueLiveEvent(Events.HIDE, liveRegion, isPolite);
   1.255 +        } else {
   1.256 +          let vc = Utils.getVirtualCursor(this.contentScope.content.document);
   1.257 +          if (vc.position &&
   1.258 +            (Utils.getState(vc.position).contains(States.DEFUNCT) ||
   1.259 +              Utils.isInSubtree(vc.position, aEvent.accessible))) {
   1.260 +            this.contentScope.contentControl.autoMove(
   1.261 +              evt.targetPrevSibling || evt.targetParent,
   1.262 +              { moveToFocused: true, delay: 500 });
   1.263 +          }
   1.264 +        }
   1.265 +        break;
   1.266 +      }
   1.267 +      case Events.TEXT_INSERTED:
   1.268 +      case Events.TEXT_REMOVED:
   1.269 +      {
   1.270 +        let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
   1.271 +          ['text', 'all']);
   1.272 +        if (aEvent.isFromUserInput || liveRegion) {
   1.273 +          // Handle all text mutations coming from the user or if they happen
   1.274 +          // on a live region.
   1.275 +          this._handleText(aEvent, liveRegion, isPolite);
   1.276 +        }
   1.277 +        break;
   1.278 +      }
   1.279 +      case Events.FOCUS:
   1.280 +      {
   1.281 +        // Put vc where the focus is at
   1.282 +        let acc = aEvent.accessible;
   1.283 +        let doc = aEvent.accessibleDocument;
   1.284 +        if (acc.role != Roles.DOCUMENT && doc.role != Roles.CHROME_WINDOW) {
   1.285 +         this.contentScope.contentControl.autoMove(acc);
   1.286 +       }
   1.287 +       break;
   1.288 +      }
   1.289 +      case Events.DOCUMENT_LOAD_COMPLETE:
   1.290 +      {
   1.291 +        if (aEvent.accessible === aEvent.accessibleDocument) {
   1.292 +          break;
   1.293 +        }
   1.294 +        this.contentScope.contentControl.autoMove(
   1.295 +          aEvent.accessible, { delay: 500 });
   1.296 +        break;
   1.297 +      }
   1.298 +      case Events.VALUE_CHANGE:
   1.299 +      {
   1.300 +        let position = this.contentScope.contentControl.vc.position;
   1.301 +        let target = aEvent.accessible;
   1.302 +        if (position === target ||
   1.303 +            Utils.getEmbeddedControl(position) === target) {
   1.304 +          this.present(Presentation.valueChanged(target));
   1.305 +        }
   1.306 +      }
   1.307 +    }
   1.308 +  },
   1.309 +
   1.310 +  _handleText: function _handleText(aEvent, aLiveRegion, aIsPolite) {
   1.311 +    let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
   1.312 +    let isInserted = event.isInserted;
   1.313 +    let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
   1.314 +
   1.315 +    let text = '';
   1.316 +    try {
   1.317 +      text = txtIface.getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
   1.318 +    } catch (x) {
   1.319 +      // XXX we might have gotten an exception with of a
   1.320 +      // zero-length text. If we did, ignore it (bug #749810).
   1.321 +      if (txtIface.characterCount) {
   1.322 +        throw x;
   1.323 +      }
   1.324 +    }
   1.325 +    // If there are embedded objects in the text, ignore them.
   1.326 +    // Assuming changes to the descendants would already be handled by the
   1.327 +    // show/hide event.
   1.328 +    let modifiedText = event.modifiedText.replace(/\uFFFC/g, '').trim();
   1.329 +    if (!modifiedText) {
   1.330 +      return;
   1.331 +    }
   1.332 +    if (aLiveRegion) {
   1.333 +      if (aEvent.eventType === Events.TEXT_REMOVED) {
   1.334 +        this._queueLiveEvent(Events.TEXT_REMOVED, aLiveRegion, aIsPolite,
   1.335 +          modifiedText);
   1.336 +      } else {
   1.337 +        this._dequeueLiveEvent(Events.TEXT_REMOVED, aLiveRegion);
   1.338 +        this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
   1.339 +          modifiedText));
   1.340 +      }
   1.341 +    } else {
   1.342 +      this.present(Presentation.textChanged(isInserted, event.start,
   1.343 +        event.length, text, modifiedText));
   1.344 +    }
   1.345 +  },
   1.346 +
   1.347 +  _handleLiveRegion: function _handleLiveRegion(aEvent, aRelevant) {
   1.348 +    if (aEvent.isFromUserInput) {
   1.349 +      return {};
   1.350 +    }
   1.351 +    let parseLiveAttrs = function parseLiveAttrs(aAccessible) {
   1.352 +      let attrs = Utils.getAttributes(aAccessible);
   1.353 +      if (attrs['container-live']) {
   1.354 +        return {
   1.355 +          live: attrs['container-live'],
   1.356 +          relevant: attrs['container-relevant'] || 'additions text',
   1.357 +          busy: attrs['container-busy'],
   1.358 +          atomic: attrs['container-atomic'],
   1.359 +          memberOf: attrs['member-of']
   1.360 +        };
   1.361 +      }
   1.362 +      return null;
   1.363 +    };
   1.364 +    // XXX live attributes are not set for hidden accessibles yet. Need to
   1.365 +    // climb up the tree to check for them.
   1.366 +    let getLiveAttributes = function getLiveAttributes(aEvent) {
   1.367 +      let liveAttrs = parseLiveAttrs(aEvent.accessible);
   1.368 +      if (liveAttrs) {
   1.369 +        return liveAttrs;
   1.370 +      }
   1.371 +      let parent = aEvent.targetParent;
   1.372 +      while (parent) {
   1.373 +        liveAttrs = parseLiveAttrs(parent);
   1.374 +        if (liveAttrs) {
   1.375 +          return liveAttrs;
   1.376 +        }
   1.377 +        parent = parent.parent
   1.378 +      }
   1.379 +      return {};
   1.380 +    };
   1.381 +    let {live, relevant, busy, atomic, memberOf} = getLiveAttributes(aEvent);
   1.382 +    // If container-live is not present or is set to |off| ignore the event.
   1.383 +    if (!live || live === 'off') {
   1.384 +      return {};
   1.385 +    }
   1.386 +    // XXX: support busy and atomic.
   1.387 +
   1.388 +    // Determine if the type of the mutation is relevant. Default is additions
   1.389 +    // and text.
   1.390 +    let isRelevant = Utils.matchAttributeValue(relevant, aRelevant);
   1.391 +    if (!isRelevant) {
   1.392 +      return {};
   1.393 +    }
   1.394 +    return {
   1.395 +      liveRegion: aEvent.accessible,
   1.396 +      isPolite: live === 'polite'
   1.397 +    };
   1.398 +  },
   1.399 +
   1.400 +  _dequeueLiveEvent: function _dequeueLiveEvent(aEventType, aLiveRegion) {
   1.401 +    let domNode = aLiveRegion.DOMNode;
   1.402 +    if (this._liveEventQueue && this._liveEventQueue.has(domNode)) {
   1.403 +      let queue = this._liveEventQueue.get(domNode);
   1.404 +      let nextEvent = queue[0];
   1.405 +      if (nextEvent.eventType === aEventType) {
   1.406 +        Utils.win.clearTimeout(nextEvent.timeoutID);
   1.407 +        queue.shift();
   1.408 +        if (queue.length === 0) {
   1.409 +          this._liveEventQueue.delete(domNode)
   1.410 +        }
   1.411 +      }
   1.412 +    }
   1.413 +  },
   1.414 +
   1.415 +  _queueLiveEvent: function _queueLiveEvent(aEventType, aLiveRegion, aIsPolite, aModifiedText) {
   1.416 +    if (!this._liveEventQueue) {
   1.417 +      this._liveEventQueue = new WeakMap();
   1.418 +    }
   1.419 +    let eventHandler = {
   1.420 +      eventType: aEventType,
   1.421 +      timeoutID: Utils.win.setTimeout(this.present.bind(this),
   1.422 +        20, // Wait for a possible EVENT_SHOW or EVENT_TEXT_INSERTED event.
   1.423 +        Presentation.liveRegion(aLiveRegion, aIsPolite, true, aModifiedText))
   1.424 +    };
   1.425 +
   1.426 +    let domNode = aLiveRegion.DOMNode;
   1.427 +    if (this._liveEventQueue.has(domNode)) {
   1.428 +      this._liveEventQueue.get(domNode).push(eventHandler);
   1.429 +    } else {
   1.430 +      this._liveEventQueue.set(domNode, [eventHandler]);
   1.431 +    }
   1.432 +  },
   1.433 +
   1.434 +  present: function present(aPresentationData) {
   1.435 +    this.sendMsgFunc("AccessFu:Present", aPresentationData);
   1.436 +  },
   1.437 +
   1.438 +  onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
   1.439 +    let tabstate = '';
   1.440 +
   1.441 +    let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING |
   1.442 +      Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
   1.443 +    let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
   1.444 +      Ci.nsIWebProgressListener.STATE_IS_NETWORK;
   1.445 +
   1.446 +    if ((aStateFlags & loadingState) == loadingState) {
   1.447 +      tabstate = 'loading';
   1.448 +    } else if ((aStateFlags & loadedState) == loadedState &&
   1.449 +               !aWebProgress.isLoadingDocument) {
   1.450 +      tabstate = 'loaded';
   1.451 +    }
   1.452 +
   1.453 +    if (tabstate) {
   1.454 +      let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
   1.455 +      this.present(Presentation.tabStateChanged(docAcc, tabstate));
   1.456 +    }
   1.457 +  },
   1.458 +
   1.459 +  onProgressChange: function onProgressChange() {},
   1.460 +
   1.461 +  onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
   1.462 +    let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
   1.463 +    this.present(Presentation.tabStateChanged(docAcc, 'newdoc'));
   1.464 +  },
   1.465 +
   1.466 +  onStatusChange: function onStatusChange() {},
   1.467 +
   1.468 +  onSecurityChange: function onSecurityChange() {},
   1.469 +
   1.470 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
   1.471 +                                         Ci.nsISupportsWeakReference,
   1.472 +                                         Ci.nsISupports,
   1.473 +                                         Ci.nsIObserver])
   1.474 +};
   1.475 +
   1.476 +const AccessibilityEventObserver = {
   1.477 +
   1.478 +  /**
   1.479 +   * A WeakMap containing [content, EventManager] pairs.
   1.480 +   */
   1.481 +  eventManagers: new WeakMap(),
   1.482 +
   1.483 +  /**
   1.484 +   * A total number of registered eventManagers.
   1.485 +   */
   1.486 +  listenerCount: 0,
   1.487 +
   1.488 +  /**
   1.489 +   * An indicator of an active 'accessible-event' observer.
   1.490 +   */
   1.491 +  started: false,
   1.492 +
   1.493 +  /**
   1.494 +   * Start an AccessibilityEventObserver.
   1.495 +   */
   1.496 +  start: function start() {
   1.497 +    if (this.started || this.listenerCount === 0) {
   1.498 +      return;
   1.499 +    }
   1.500 +    Services.obs.addObserver(this, 'accessible-event', false);
   1.501 +    this.started = true;
   1.502 +  },
   1.503 +
   1.504 +  /**
   1.505 +   * Stop an AccessibilityEventObserver.
   1.506 +   */
   1.507 +  stop: function stop() {
   1.508 +    if (!this.started) {
   1.509 +      return;
   1.510 +    }
   1.511 +    Services.obs.removeObserver(this, 'accessible-event');
   1.512 +    // Clean up all registered event managers.
   1.513 +    this.eventManagers.clear();
   1.514 +    this.listenerCount = 0;
   1.515 +    this.started = false;
   1.516 +  },
   1.517 +
   1.518 +  /**
   1.519 +   * Register an EventManager and start listening to the
   1.520 +   * 'accessible-event' messages.
   1.521 +   *
   1.522 +   * @param aEventManager EventManager
   1.523 +   *        An EventManager object that was loaded into the specific content.
   1.524 +   */
   1.525 +  addListener: function addListener(aEventManager) {
   1.526 +    let content = aEventManager.contentScope.content;
   1.527 +    if (!this.eventManagers.has(content)) {
   1.528 +      this.listenerCount++;
   1.529 +    }
   1.530 +    this.eventManagers.set(content, aEventManager);
   1.531 +    // Since at least one EventManager was registered, start listening.
   1.532 +    Logger.debug('AccessibilityEventObserver.addListener. Total:',
   1.533 +      this.listenerCount);
   1.534 +    this.start();
   1.535 +  },
   1.536 +
   1.537 +  /**
   1.538 +   * Unregister an EventManager and, optionally, stop listening to the
   1.539 +   * 'accessible-event' messages.
   1.540 +   *
   1.541 +   * @param aEventManager EventManager
   1.542 +   *        An EventManager object that was stopped in the specific content.
   1.543 +   */
   1.544 +  removeListener: function removeListener(aEventManager) {
   1.545 +    let content = aEventManager.contentScope.content;
   1.546 +    if (!this.eventManagers.delete(content)) {
   1.547 +      return;
   1.548 +    }
   1.549 +    this.listenerCount--;
   1.550 +    Logger.debug('AccessibilityEventObserver.removeListener. Total:',
   1.551 +      this.listenerCount);
   1.552 +    if (this.listenerCount === 0) {
   1.553 +      // If there are no EventManagers registered at the moment, stop listening
   1.554 +      // to the 'accessible-event' messages.
   1.555 +      this.stop();
   1.556 +    }
   1.557 +  },
   1.558 +
   1.559 +  /**
   1.560 +   * Lookup an EventManager for a specific content. If the EventManager is not
   1.561 +   * found, walk up the hierarchy of parent windows.
   1.562 +   * @param content Window
   1.563 +   *        A content Window used to lookup the corresponding EventManager.
   1.564 +   */
   1.565 +  getListener: function getListener(content) {
   1.566 +    let eventManager = this.eventManagers.get(content);
   1.567 +    if (eventManager) {
   1.568 +      return eventManager;
   1.569 +    }
   1.570 +    let parent = content.parent;
   1.571 +    if (parent === content) {
   1.572 +      // There is no parent or the parent is of a different type.
   1.573 +      return null;
   1.574 +    }
   1.575 +    return this.getListener(parent);
   1.576 +  },
   1.577 +
   1.578 +  /**
   1.579 +   * Handle the 'accessible-event' message.
   1.580 +   */
   1.581 +  observe: function observe(aSubject, aTopic, aData) {
   1.582 +    if (aTopic !== 'accessible-event') {
   1.583 +      return;
   1.584 +    }
   1.585 +    let event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
   1.586 +    if (!event.accessibleDocument) {
   1.587 +      Logger.warning(
   1.588 +        'AccessibilityEventObserver.observe: no accessible document:',
   1.589 +        Logger.eventToString(event), "accessible:",
   1.590 +        Logger.accessibleToString(event.accessible));
   1.591 +      return;
   1.592 +    }
   1.593 +    let content = event.accessibleDocument.window;
   1.594 +    // Match the content window to its EventManager.
   1.595 +    let eventManager = this.getListener(content);
   1.596 +    if (!eventManager || !eventManager._started) {
   1.597 +      if (Utils.MozBuildApp === 'browser' &&
   1.598 +          !(content instanceof Ci.nsIDOMChromeWindow)) {
   1.599 +        Logger.warning(
   1.600 +          'AccessibilityEventObserver.observe: ignored event:',
   1.601 +          Logger.eventToString(event), "accessible:",
   1.602 +          Logger.accessibleToString(event.accessible), "document:",
   1.603 +          Logger.accessibleToString(event.accessibleDocument));
   1.604 +      }
   1.605 +      return;
   1.606 +    }
   1.607 +    try {
   1.608 +      eventManager.handleAccEvent(event);
   1.609 +    } catch (x) {
   1.610 +      Logger.logException(x, 'Error handing accessible event');
   1.611 +    } finally {
   1.612 +      return;
   1.613 +    }
   1.614 +  }
   1.615 +};

mercurial