accessible/src/jsat/EventManager.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 'use strict';
     7 const Ci = Components.interfaces;
     8 const Cu = Components.utils;
    10 const TEXT_NODE = 3;
    12 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    13 XPCOMUtils.defineLazyModuleGetter(this, 'Services',
    14   'resource://gre/modules/Services.jsm');
    15 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
    16   'resource://gre/modules/accessibility/Utils.jsm');
    17 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
    18   'resource://gre/modules/accessibility/Utils.jsm');
    19 XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
    20   'resource://gre/modules/accessibility/Presentation.jsm');
    21 XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
    22   'resource://gre/modules/accessibility/TraversalRules.jsm');
    23 XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
    24   'resource://gre/modules/accessibility/Constants.jsm');
    25 XPCOMUtils.defineLazyModuleGetter(this, 'Events',
    26   'resource://gre/modules/accessibility/Constants.jsm');
    27 XPCOMUtils.defineLazyModuleGetter(this, 'States',
    28   'resource://gre/modules/accessibility/Constants.jsm');
    30 this.EXPORTED_SYMBOLS = ['EventManager'];
    32 this.EventManager = function EventManager(aContentScope) {
    33   this.contentScope = aContentScope;
    34   this.addEventListener = this.contentScope.addEventListener.bind(
    35     this.contentScope);
    36   this.removeEventListener = this.contentScope.removeEventListener.bind(
    37     this.contentScope);
    38   this.sendMsgFunc = this.contentScope.sendAsyncMessage.bind(
    39     this.contentScope);
    40   this.webProgress = this.contentScope.docShell.
    41     QueryInterface(Ci.nsIInterfaceRequestor).
    42     getInterface(Ci.nsIWebProgress);
    43 };
    45 this.EventManager.prototype = {
    46   editState: {},
    48   start: function start() {
    49     try {
    50       if (!this._started) {
    51         Logger.debug('EventManager.start');
    53         this._started = true;
    55         AccessibilityEventObserver.addListener(this);
    57         this.webProgress.addProgressListener(this,
    58           (Ci.nsIWebProgress.NOTIFY_STATE_ALL |
    59            Ci.nsIWebProgress.NOTIFY_LOCATION));
    60         this.addEventListener('wheel', this, true);
    61         this.addEventListener('scroll', this, true);
    62         this.addEventListener('resize', this, true);
    63       }
    64       this.present(Presentation.tabStateChanged(null, 'newtab'));
    66     } catch (x) {
    67       Logger.logException(x, 'Failed to start EventManager');
    68     }
    69   },
    71   // XXX: Stop is not called when the tab is closed (|TabClose| event is too
    72   // late). It is only called when the AccessFu is disabled explicitly.
    73   stop: function stop() {
    74     if (!this._started) {
    75       return;
    76     }
    77     Logger.debug('EventManager.stop');
    78     AccessibilityEventObserver.removeListener(this);
    79     try {
    80       this.webProgress.removeProgressListener(this);
    81       this.removeEventListener('wheel', this, true);
    82       this.removeEventListener('scroll', this, true);
    83       this.removeEventListener('resize', this, true);
    84     } catch (x) {
    85       // contentScope is dead.
    86     } finally {
    87       this._started = false;
    88     }
    89   },
    91   handleEvent: function handleEvent(aEvent) {
    92     Logger.debug(() => {
    93       return ['DOMEvent', aEvent.type];
    94     });
    96     try {
    97       switch (aEvent.type) {
    98       case 'wheel':
    99       {
   100         let attempts = 0;
   101         let delta = aEvent.deltaX || aEvent.deltaY;
   102         this.contentScope.contentControl.autoMove(
   103          null,
   104          { moveMethod: delta > 0 ? 'moveNext' : 'movePrevious',
   105            onScreenOnly: true, noOpIfOnScreen: true, delay: 500 });
   106         break;
   107       }
   108       case 'scroll':
   109       case 'resize':
   110       {
   111         // the target could be an element, document or window
   112         let window = null;
   113         if (aEvent.target instanceof Ci.nsIDOMWindow)
   114           window = aEvent.target;
   115         else if (aEvent.target instanceof Ci.nsIDOMDocument)
   116           window = aEvent.target.defaultView;
   117         else if (aEvent.target instanceof Ci.nsIDOMElement)
   118           window = aEvent.target.ownerDocument.defaultView;
   119         this.present(Presentation.viewportChanged(window));
   120         break;
   121       }
   122       }
   123     } catch (x) {
   124       Logger.logException(x, 'Error handling DOM event');
   125     }
   126   },
   128   handleAccEvent: function handleAccEvent(aEvent) {
   129     Logger.debug(() => {
   130       return ['A11yEvent', Logger.eventToString(aEvent),
   131               Logger.accessibleToString(aEvent.accessible)];
   132     });
   134     // Don't bother with non-content events in firefox.
   135     if (Utils.MozBuildApp == 'browser' &&
   136         aEvent.eventType != Events.VIRTUALCURSOR_CHANGED &&
   137         // XXX Bug 442005 results in DocAccessible::getDocType returning
   138         // NS_ERROR_FAILURE. Checking for aEvent.accessibleDocument.docType ==
   139         // 'window' does not currently work.
   140         (aEvent.accessibleDocument.DOMDocument.doctype &&
   141          aEvent.accessibleDocument.DOMDocument.doctype.name === 'window')) {
   142       return;
   143     }
   145     switch (aEvent.eventType) {
   146       case Events.VIRTUALCURSOR_CHANGED:
   147       {
   148         let pivot = aEvent.accessible.
   149           QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
   150         let position = pivot.position;
   151         if (position && position.role == Roles.INTERNAL_FRAME)
   152           break;
   153         let event = aEvent.
   154           QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
   155         let reason = event.reason;
   156         let oldAccessible = event.oldAccessible;
   158         if (this.editState.editing) {
   159           aEvent.accessibleDocument.takeFocus();
   160         }
   161         this.present(
   162           Presentation.pivotChanged(position, oldAccessible, reason,
   163                                     pivot.startOffset, pivot.endOffset));
   165         break;
   166       }
   167       case Events.STATE_CHANGE:
   168       {
   169         let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
   170         let state = Utils.getState(event);
   171         if (state.contains(States.CHECKED)) {
   172           this.present(
   173             Presentation.
   174               actionInvoked(aEvent.accessible,
   175                             event.isEnabled ? 'check' : 'uncheck'));
   176         } else if (state.contains(States.SELECTED)) {
   177           this.present(
   178             Presentation.
   179               actionInvoked(aEvent.accessible,
   180                             event.isEnabled ? 'select' : 'unselect'));
   181         }
   182         break;
   183       }
   184       case Events.SCROLLING_START:
   185       {
   186         this.contentScope.contentControl.autoMove(aEvent.accessible);
   187         break;
   188       }
   189       case Events.TEXT_CARET_MOVED:
   190       {
   191         let acc = aEvent.accessible;
   192         let characterCount = acc.
   193           QueryInterface(Ci.nsIAccessibleText).characterCount;
   194         let caretOffset = aEvent.
   195           QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
   197         // Update editing state, both for presenter and other things
   198         let state = Utils.getState(acc);
   199         let editState = {
   200           editing: state.contains(States.EDITABLE),
   201           multiline: state.contains(States.MULTI_LINE),
   202           atStart: caretOffset == 0,
   203           atEnd: caretOffset == characterCount
   204         };
   206         // Not interesting
   207         if (!editState.editing && editState.editing == this.editState.editing)
   208           break;
   210         if (editState.editing != this.editState.editing)
   211           this.present(Presentation.editingModeChanged(editState.editing));
   213         if (editState.editing != this.editState.editing ||
   214             editState.multiline != this.editState.multiline ||
   215             editState.atEnd != this.editState.atEnd ||
   216             editState.atStart != this.editState.atStart)
   217           this.sendMsgFunc("AccessFu:Input", editState);
   219         this.present(Presentation.textSelectionChanged(acc.getText(0,-1),
   220                      caretOffset, caretOffset, 0, 0, aEvent.isFromUserInput));
   222         this.editState = editState;
   223         break;
   224       }
   225       case Events.SHOW:
   226       {
   227         let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
   228           ['additions', 'all']);
   229         // Only handle show if it is a relevant live region.
   230         if (!liveRegion) {
   231           break;
   232         }
   233         // Show for text is handled by the EVENT_TEXT_INSERTED handler.
   234         if (aEvent.accessible.role === Roles.TEXT_LEAF) {
   235           break;
   236         }
   237         this._dequeueLiveEvent(Events.HIDE, liveRegion);
   238         this.present(Presentation.liveRegion(liveRegion, isPolite, false));
   239         break;
   240       }
   241       case Events.HIDE:
   242       {
   243         let evt = aEvent.QueryInterface(Ci.nsIAccessibleHideEvent);
   244         let {liveRegion, isPolite} = this._handleLiveRegion(
   245           evt, ['removals', 'all']);
   246         if (liveRegion) {
   247           // Hide for text is handled by the EVENT_TEXT_REMOVED handler.
   248           if (aEvent.accessible.role === Roles.TEXT_LEAF) {
   249             break;
   250           }
   251           this._queueLiveEvent(Events.HIDE, liveRegion, isPolite);
   252         } else {
   253           let vc = Utils.getVirtualCursor(this.contentScope.content.document);
   254           if (vc.position &&
   255             (Utils.getState(vc.position).contains(States.DEFUNCT) ||
   256               Utils.isInSubtree(vc.position, aEvent.accessible))) {
   257             this.contentScope.contentControl.autoMove(
   258               evt.targetPrevSibling || evt.targetParent,
   259               { moveToFocused: true, delay: 500 });
   260           }
   261         }
   262         break;
   263       }
   264       case Events.TEXT_INSERTED:
   265       case Events.TEXT_REMOVED:
   266       {
   267         let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
   268           ['text', 'all']);
   269         if (aEvent.isFromUserInput || liveRegion) {
   270           // Handle all text mutations coming from the user or if they happen
   271           // on a live region.
   272           this._handleText(aEvent, liveRegion, isPolite);
   273         }
   274         break;
   275       }
   276       case Events.FOCUS:
   277       {
   278         // Put vc where the focus is at
   279         let acc = aEvent.accessible;
   280         let doc = aEvent.accessibleDocument;
   281         if (acc.role != Roles.DOCUMENT && doc.role != Roles.CHROME_WINDOW) {
   282          this.contentScope.contentControl.autoMove(acc);
   283        }
   284        break;
   285       }
   286       case Events.DOCUMENT_LOAD_COMPLETE:
   287       {
   288         if (aEvent.accessible === aEvent.accessibleDocument) {
   289           break;
   290         }
   291         this.contentScope.contentControl.autoMove(
   292           aEvent.accessible, { delay: 500 });
   293         break;
   294       }
   295       case Events.VALUE_CHANGE:
   296       {
   297         let position = this.contentScope.contentControl.vc.position;
   298         let target = aEvent.accessible;
   299         if (position === target ||
   300             Utils.getEmbeddedControl(position) === target) {
   301           this.present(Presentation.valueChanged(target));
   302         }
   303       }
   304     }
   305   },
   307   _handleText: function _handleText(aEvent, aLiveRegion, aIsPolite) {
   308     let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
   309     let isInserted = event.isInserted;
   310     let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
   312     let text = '';
   313     try {
   314       text = txtIface.getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
   315     } catch (x) {
   316       // XXX we might have gotten an exception with of a
   317       // zero-length text. If we did, ignore it (bug #749810).
   318       if (txtIface.characterCount) {
   319         throw x;
   320       }
   321     }
   322     // If there are embedded objects in the text, ignore them.
   323     // Assuming changes to the descendants would already be handled by the
   324     // show/hide event.
   325     let modifiedText = event.modifiedText.replace(/\uFFFC/g, '').trim();
   326     if (!modifiedText) {
   327       return;
   328     }
   329     if (aLiveRegion) {
   330       if (aEvent.eventType === Events.TEXT_REMOVED) {
   331         this._queueLiveEvent(Events.TEXT_REMOVED, aLiveRegion, aIsPolite,
   332           modifiedText);
   333       } else {
   334         this._dequeueLiveEvent(Events.TEXT_REMOVED, aLiveRegion);
   335         this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
   336           modifiedText));
   337       }
   338     } else {
   339       this.present(Presentation.textChanged(isInserted, event.start,
   340         event.length, text, modifiedText));
   341     }
   342   },
   344   _handleLiveRegion: function _handleLiveRegion(aEvent, aRelevant) {
   345     if (aEvent.isFromUserInput) {
   346       return {};
   347     }
   348     let parseLiveAttrs = function parseLiveAttrs(aAccessible) {
   349       let attrs = Utils.getAttributes(aAccessible);
   350       if (attrs['container-live']) {
   351         return {
   352           live: attrs['container-live'],
   353           relevant: attrs['container-relevant'] || 'additions text',
   354           busy: attrs['container-busy'],
   355           atomic: attrs['container-atomic'],
   356           memberOf: attrs['member-of']
   357         };
   358       }
   359       return null;
   360     };
   361     // XXX live attributes are not set for hidden accessibles yet. Need to
   362     // climb up the tree to check for them.
   363     let getLiveAttributes = function getLiveAttributes(aEvent) {
   364       let liveAttrs = parseLiveAttrs(aEvent.accessible);
   365       if (liveAttrs) {
   366         return liveAttrs;
   367       }
   368       let parent = aEvent.targetParent;
   369       while (parent) {
   370         liveAttrs = parseLiveAttrs(parent);
   371         if (liveAttrs) {
   372           return liveAttrs;
   373         }
   374         parent = parent.parent
   375       }
   376       return {};
   377     };
   378     let {live, relevant, busy, atomic, memberOf} = getLiveAttributes(aEvent);
   379     // If container-live is not present or is set to |off| ignore the event.
   380     if (!live || live === 'off') {
   381       return {};
   382     }
   383     // XXX: support busy and atomic.
   385     // Determine if the type of the mutation is relevant. Default is additions
   386     // and text.
   387     let isRelevant = Utils.matchAttributeValue(relevant, aRelevant);
   388     if (!isRelevant) {
   389       return {};
   390     }
   391     return {
   392       liveRegion: aEvent.accessible,
   393       isPolite: live === 'polite'
   394     };
   395   },
   397   _dequeueLiveEvent: function _dequeueLiveEvent(aEventType, aLiveRegion) {
   398     let domNode = aLiveRegion.DOMNode;
   399     if (this._liveEventQueue && this._liveEventQueue.has(domNode)) {
   400       let queue = this._liveEventQueue.get(domNode);
   401       let nextEvent = queue[0];
   402       if (nextEvent.eventType === aEventType) {
   403         Utils.win.clearTimeout(nextEvent.timeoutID);
   404         queue.shift();
   405         if (queue.length === 0) {
   406           this._liveEventQueue.delete(domNode)
   407         }
   408       }
   409     }
   410   },
   412   _queueLiveEvent: function _queueLiveEvent(aEventType, aLiveRegion, aIsPolite, aModifiedText) {
   413     if (!this._liveEventQueue) {
   414       this._liveEventQueue = new WeakMap();
   415     }
   416     let eventHandler = {
   417       eventType: aEventType,
   418       timeoutID: Utils.win.setTimeout(this.present.bind(this),
   419         20, // Wait for a possible EVENT_SHOW or EVENT_TEXT_INSERTED event.
   420         Presentation.liveRegion(aLiveRegion, aIsPolite, true, aModifiedText))
   421     };
   423     let domNode = aLiveRegion.DOMNode;
   424     if (this._liveEventQueue.has(domNode)) {
   425       this._liveEventQueue.get(domNode).push(eventHandler);
   426     } else {
   427       this._liveEventQueue.set(domNode, [eventHandler]);
   428     }
   429   },
   431   present: function present(aPresentationData) {
   432     this.sendMsgFunc("AccessFu:Present", aPresentationData);
   433   },
   435   onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
   436     let tabstate = '';
   438     let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING |
   439       Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
   440     let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
   441       Ci.nsIWebProgressListener.STATE_IS_NETWORK;
   443     if ((aStateFlags & loadingState) == loadingState) {
   444       tabstate = 'loading';
   445     } else if ((aStateFlags & loadedState) == loadedState &&
   446                !aWebProgress.isLoadingDocument) {
   447       tabstate = 'loaded';
   448     }
   450     if (tabstate) {
   451       let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
   452       this.present(Presentation.tabStateChanged(docAcc, tabstate));
   453     }
   454   },
   456   onProgressChange: function onProgressChange() {},
   458   onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
   459     let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
   460     this.present(Presentation.tabStateChanged(docAcc, 'newdoc'));
   461   },
   463   onStatusChange: function onStatusChange() {},
   465   onSecurityChange: function onSecurityChange() {},
   467   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
   468                                          Ci.nsISupportsWeakReference,
   469                                          Ci.nsISupports,
   470                                          Ci.nsIObserver])
   471 };
   473 const AccessibilityEventObserver = {
   475   /**
   476    * A WeakMap containing [content, EventManager] pairs.
   477    */
   478   eventManagers: new WeakMap(),
   480   /**
   481    * A total number of registered eventManagers.
   482    */
   483   listenerCount: 0,
   485   /**
   486    * An indicator of an active 'accessible-event' observer.
   487    */
   488   started: false,
   490   /**
   491    * Start an AccessibilityEventObserver.
   492    */
   493   start: function start() {
   494     if (this.started || this.listenerCount === 0) {
   495       return;
   496     }
   497     Services.obs.addObserver(this, 'accessible-event', false);
   498     this.started = true;
   499   },
   501   /**
   502    * Stop an AccessibilityEventObserver.
   503    */
   504   stop: function stop() {
   505     if (!this.started) {
   506       return;
   507     }
   508     Services.obs.removeObserver(this, 'accessible-event');
   509     // Clean up all registered event managers.
   510     this.eventManagers.clear();
   511     this.listenerCount = 0;
   512     this.started = false;
   513   },
   515   /**
   516    * Register an EventManager and start listening to the
   517    * 'accessible-event' messages.
   518    *
   519    * @param aEventManager EventManager
   520    *        An EventManager object that was loaded into the specific content.
   521    */
   522   addListener: function addListener(aEventManager) {
   523     let content = aEventManager.contentScope.content;
   524     if (!this.eventManagers.has(content)) {
   525       this.listenerCount++;
   526     }
   527     this.eventManagers.set(content, aEventManager);
   528     // Since at least one EventManager was registered, start listening.
   529     Logger.debug('AccessibilityEventObserver.addListener. Total:',
   530       this.listenerCount);
   531     this.start();
   532   },
   534   /**
   535    * Unregister an EventManager and, optionally, stop listening to the
   536    * 'accessible-event' messages.
   537    *
   538    * @param aEventManager EventManager
   539    *        An EventManager object that was stopped in the specific content.
   540    */
   541   removeListener: function removeListener(aEventManager) {
   542     let content = aEventManager.contentScope.content;
   543     if (!this.eventManagers.delete(content)) {
   544       return;
   545     }
   546     this.listenerCount--;
   547     Logger.debug('AccessibilityEventObserver.removeListener. Total:',
   548       this.listenerCount);
   549     if (this.listenerCount === 0) {
   550       // If there are no EventManagers registered at the moment, stop listening
   551       // to the 'accessible-event' messages.
   552       this.stop();
   553     }
   554   },
   556   /**
   557    * Lookup an EventManager for a specific content. If the EventManager is not
   558    * found, walk up the hierarchy of parent windows.
   559    * @param content Window
   560    *        A content Window used to lookup the corresponding EventManager.
   561    */
   562   getListener: function getListener(content) {
   563     let eventManager = this.eventManagers.get(content);
   564     if (eventManager) {
   565       return eventManager;
   566     }
   567     let parent = content.parent;
   568     if (parent === content) {
   569       // There is no parent or the parent is of a different type.
   570       return null;
   571     }
   572     return this.getListener(parent);
   573   },
   575   /**
   576    * Handle the 'accessible-event' message.
   577    */
   578   observe: function observe(aSubject, aTopic, aData) {
   579     if (aTopic !== 'accessible-event') {
   580       return;
   581     }
   582     let event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
   583     if (!event.accessibleDocument) {
   584       Logger.warning(
   585         'AccessibilityEventObserver.observe: no accessible document:',
   586         Logger.eventToString(event), "accessible:",
   587         Logger.accessibleToString(event.accessible));
   588       return;
   589     }
   590     let content = event.accessibleDocument.window;
   591     // Match the content window to its EventManager.
   592     let eventManager = this.getListener(content);
   593     if (!eventManager || !eventManager._started) {
   594       if (Utils.MozBuildApp === 'browser' &&
   595           !(content instanceof Ci.nsIDOMChromeWindow)) {
   596         Logger.warning(
   597           'AccessibilityEventObserver.observe: ignored event:',
   598           Logger.eventToString(event), "accessible:",
   599           Logger.accessibleToString(event.accessible), "document:",
   600           Logger.accessibleToString(event.accessibleDocument));
   601       }
   602       return;
   603     }
   604     try {
   605       eventManager.handleAccEvent(event);
   606     } catch (x) {
   607       Logger.logException(x, 'Error handing accessible event');
   608     } finally {
   609       return;
   610     }
   611   }
   612 };

mercurial