Bug 745986 - Report page loading, loaded and new tabs. r=surkov
authorEitan Isaacson <eitan@monotonous.org>
Mon, 07 May 2012 09:44:44 -0700
changeset 93350 1bb9382bcbd116ef0993d835cd62af39674c149e
parent 93349 7fc170b6f470d5e09068688fb647727bfce7cbe5
child 93351 bc7f3a9deba8d737c54369270a00fcb2b091cecc
push id9092
push usereisaacson@mozilla.com
push dateMon, 07 May 2012 16:52:51 +0000
treeherdermozilla-inbound@b58c6e5156d7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssurkov
bugs745986
milestone15.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 745986 - Report page loading, loaded and new tabs. r=surkov
accessible/src/jsat/AccessFu.jsm
accessible/src/jsat/Presenters.jsm
accessible/src/jsat/UtteranceGenerator.jsm
dom/locales/en-US/chrome/accessibility/AccessFu.properties
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -67,38 +67,34 @@ var AccessFu = {
 
     VirtualCursorController.attach(this.chromeWin);
 
     Services.obs.addObserver(this, 'accessible-event', false);
     this.chromeWin.addEventListener('DOMActivate', this, true);
     this.chromeWin.addEventListener('resize', this, true);
     this.chromeWin.addEventListener('scroll', this, true);
     this.chromeWin.addEventListener('TabOpen', this, true);
-    this.chromeWin.addEventListener('TabSelect', this, true);
-    this.chromeWin.addEventListener('TabClosed', this, true);
   },
 
   /**
    * Disable AccessFu and return to default interaction mode.
    */
   disable: function disable() {
     dump('AccessFu disable');
 
-    this.presenters.forEach(function(p) {p.detach();});
+    this.presenters.forEach(function(p) { p.detach(); });
     this.presenters = [];
 
     VirtualCursorController.detach();
 
     Services.obs.addObserver(this, 'accessible-event', false);
     this.chromeWin.removeEventListener('DOMActivate', this);
     this.chromeWin.removeEventListener('resize', this);
     this.chromeWin.removeEventListener('scroll', this);
     this.chromeWin.removeEventListener('TabOpen', this);
-    this.chromeWin.removeEventListener('TabSelect', this);
-    this.chromeWin.removeEventListener('TabClose', this);
   },
 
   amINeeded: function(aPref) {
     switch (aPref) {
       case ACCESSFU_ENABLE:
         return true;
       case ACCESSFU_AUTO:
         if (Services.appinfo.OS == 'Android') {
@@ -120,24 +116,27 @@ var AccessFu = {
 
   addPresenter: function addPresenter(presenter) {
     this.presenters.push(presenter);
     presenter.attach(this.chromeWin);
   },
 
   handleEvent: function handleEvent(aEvent) {
     switch (aEvent.type) {
-      case 'TabSelect':
-        {
-          this.getDocAccessible(
-              function(docAcc) {
-                this.presenters.forEach(function(p) {p.tabSelected(docAcc);});
-              });
-          break;
-        }
+      case 'TabOpen':
+      {
+        let browser = aEvent.target.linkedBrowser || aEvent.target;
+        // Store the new browser node. We will need to check later when a new
+        // content document is attached if it has been attached to this new tab.
+        // If it has, than we will need to send a 'loading' message along with
+        // the usual 'newdoc' to presenters.
+        this._pendingDocuments[browser] = true;
+        this.presenters.forEach(function(p) { p.tabStateChanged(null, 'newtab'); });
+        break;
+      }
       case 'DOMActivate':
       {
         let activatedAcc = getAccessible(aEvent.originalTarget);
         let state = {};
         activatedAcc.getState(state, {});
 
         // Checkable objects will have a state changed event that we will use
         // instead of this hackish DOMActivate. We will also know the true
@@ -148,35 +147,22 @@ var AccessFu = {
         this.presenters.forEach(function(p) {
                                   p.actionInvoked(activatedAcc, 'click');
                                 });
         break;
       }
       case 'scroll':
       case 'resize':
       {
-        this.presenters.forEach(function(p) {p.viewportChanged();});
+        this.presenters.forEach(function(p) { p.viewportChanged(); });
         break;
       }
     }
   },
 
-  getDocAccessible: function getDocAccessible(aCallback) {
-    let browserApp = (Services.appinfo.OS == 'Android') ?
-      this.chromeWin.BrowserApp : this.chromeWin.gBrowser;
-
-    let docAcc = getAccessible(browserApp.selectedBrowser.contentDocument);
-    if (!docAcc) {
-      // Wait for a reorder event fired by the parent of the new doc.
-      this._pendingDocuments[browserApp.selectedBrowser] = aCallback;
-    } else {
-      aCallback.apply(this, [docAcc]);
-    }
-  },
-
   observe: function observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case 'nsPref:changed':
         if (aData == 'accessfu') {
           if (this.amINeeded(this.prefsBranch.getIntPref('accessfu')))
             this.enable();
           else
             this.disable();
@@ -218,26 +204,96 @@ var AccessFu = {
               !(event.isExtraState())) {
             this.presenters.forEach(
               function(p) {
                 p.actionInvoked(aEvent.accessible,
                                 event.isEnabled() ? 'check' : 'uncheck');
               }
             );
           }
+          else if (event.state == Ci.nsIAccessibleStates.STATE_BUSY &&
+                   !(event.isExtraState()) && event.isEnabled()) {
+            let role = event.accessible.role;
+            if ((role == Ci.nsIAccessibleRole.ROLE_DOCUMENT ||
+                 role == Ci.nsIAccessibleRole.ROLE_APPLICATION)) {
+              // An existing document has changed to state "busy", this means
+              // something is loading. Send a 'loading' message to presenters.
+              this.presenters.forEach(
+                function(p) {
+                  p.tabStateChanged(event.accessible, 'loading');
+                }
+              );
+            }
+          }
           break;
         }
       case Ci.nsIAccessibleEvent.EVENT_REORDER:
         {
-          let node = aEvent.accessible.DOMNode;
-          let callback = this._pendingDocuments[node];
-          if (callback && aEvent.accessible.childCount) {
-            // We have a callback associated with a document.
-            callback.apply(this, [aEvent.accessible.getChildAt(0)]);
-            delete this._pendingDocuments[node];
+          let acc = aEvent.accessible;
+          if (acc.childCount) {
+            let docAcc = acc.getChildAt(0);
+            if (this._pendingDocuments[aEvent.DOMNode]) {
+              // This is a document in a new tab. Check if it is
+              // in a BUSY state (i.e. loading), and inform presenters.
+              // We need to do this because a state change event will not be
+              // fired when an object is created with the BUSY state.
+              // If this is not a new tab, don't bother because we sent 'loading'
+              // when the previous doc changed its state to BUSY.
+              let state = {};
+              docAcc.getState(state, {});
+              if (state.value & Ci.nsIAccessibleStates.STATE_BUSY &&
+                  this.isNotChromeDoc(docAcc))
+                this.presenters.forEach(
+                  function(p) { p.tabStateChanged(docAcc, 'loading'); }
+                );
+              delete this._pendingDocuments[aEvent.DOMNode];
+            }
+            if (this.isBrowserDoc(docAcc))
+              // A new top-level content document has been attached
+              this.presenters.forEach(
+                function(p) { p.tabStateChanged(docAcc, 'newdoc'); }
+              );
+          }
+          break;
+        }
+      case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE:
+        {
+          if (this.isNotChromeDoc(aEvent.accessible)) {
+            this.presenters.forEach(
+              function(p) {
+                p.tabStateChanged(aEvent.accessible, 'loaded');
+              }
+            );
+          }
+          break;
+        }
+      case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED:
+        {
+          this.presenters.forEach(
+            function(p) {
+              p.tabStateChanged(aEvent.accessible, 'loadstopped');
+            }
+          );
+          break;
+        }
+      case Ci.nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD:
+        {
+          this.presenters.forEach(
+            function(p) {
+              p.tabStateChanged(aEvent.accessible, 'reload');
+            }
+          );
+          break;
+        }
+      case Ci.nsIAccessibleEvent.EVENT_FOCUS:
+        {
+          if (this.isBrowserDoc(aEvent.accessible)) {
+            // The document recieved focus, call tabSelected to present current tab.
+            this.presenters.forEach(
+              function(p) { p.tabSelected(aEvent.accessible); });
           }
           break;
         }
       case Ci.nsIAccessibleEvent.EVENT_TEXT_INSERTED:
       case Ci.nsIAccessibleEvent.EVENT_TEXT_REMOVED:
       {
         if (aEvent.isFromUserInput) {
           // XXX support live regions as well.
@@ -264,16 +320,48 @@ var AccessFu = {
         }
         break;
       }
       default:
         break;
     }
   },
 
+  /**
+   * Check if accessible is a top-level content document (i.e. a child of a XUL
+   * browser node).
+   * @param {nsIAccessible} aDocAcc the accessible to check.
+   * @return {boolean} true if this is a top-level content document.
+   */
+  isBrowserDoc: function isBrowserDoc(aDocAcc) {
+    let parent = aDocAcc.parent;
+    if (!parent)
+      return false;
+
+    let domNode = parent.DOMNode;
+    if (!domNode)
+      return false;
+
+    const ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+    return (domNode.localName == 'browser' && domNode.namespaceURI == ns);
+  },
+
+  /**
+   * Check if document is not a local "chrome" document, like about:home.
+   * @param {nsIDOMDocument} aDocument the document to check.
+   * @return {boolean} true if this is not a chrome document.
+   */
+  isNotChromeDoc: function isNotChromeDoc(aDocument) {
+    let location = aDocument.DOMNode.location;
+    if (!location)
+      return false;
+
+    return location.protocol != "about:";
+  },
+
   getNewContext: function getNewContext(aOldObject, aNewObject) {
     let newLineage = [];
     let oldLineage = [];
 
     let parent = aNewObject;
     while ((parent = parent.parent))
       newLineage.push(parent);
 
--- a/accessible/src/jsat/Presenters.jsm
+++ b/accessible/src/jsat/Presenters.jsm
@@ -61,25 +61,31 @@ Presenter.prototype = {
 
   /**
    * Selection has changed. TODO.
    * @param {nsIAccessible} aObject the object that has been selected.
    */
   selectionChanged: function selectionChanged(aObject) {},
 
   /**
-   * The page state has changed, loading, stopped loading, etc. TODO.
+   * 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'.
    */
-  pageStateChanged: function pageStateChanged() {},
+  tabStateChanged: function tabStateChanged(aDocObj, aPageState) {},
 
   /**
-   * The tab has changed.
-   * @param {nsIAccessible} aObject the document contained in the tab.
+   * The current tab has changed.
+   * @param {nsIAccessible} aObject the document contained by the tab
+   *    accessible, or null if it is a new tab with no attached
+   *    document yet.
    */
-  tabSelected: function tabSelected(aObject) {},
+  tabSelected: function tabSelected(aDocObj) {},
 
   /**
    * The viewport has changed, either a scroll, pan, zoom, or
    *    landscape/portrait toggle.
    */
   viewportChanged: function viewportChanged() {}
 };
 
@@ -142,19 +148,27 @@ VisualPresenter.prototype.pivotChanged =
     aObject.scrollTo(Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
     this.highlight(aObject);
   } catch (e) {
     dump('Error getting bounds: ' + e);
     return;
   }
 };
 
-VisualPresenter.prototype.tabSelected = function(aObject) {
-  let vcDoc = aObject.QueryInterface(Ci.nsIAccessibleCursorable);
-  this.pivotChanged(vcDoc.virtualCursor.position);
+VisualPresenter.prototype.tabSelected = function(aDocObj) {
+  let vcPos = aDocObj ?
+    aDocObj.QueryInterface(Ci.nsIAccessibleCursorable).virtualCursor.position :
+    null;
+
+  this.pivotChanged(vcPos);
+};
+
+VisualPresenter.prototype.tabStateChanged = function(aDocObj, aPageState) {
+  if (aPageState == "newdoc")
+    this.pivotChanged(null);
 };
 
 // Internals
 
 VisualPresenter.prototype.hide = function hide() {
   this.highlightBox.style.display = 'none';
 };
 
@@ -237,26 +251,50 @@ AndroidPresenter.prototype.actionInvoked
     gecko: {
       type: 'Accessibility:Event',
       eventType: ANDROID_TYPE_VIEW_CLICKED,
       text: UtteranceGenerator.genForAction(aObject, aActionName)
     }
   });
 };
 
-AndroidPresenter.prototype.tabSelected = function(aObject) {
-  let vcDoc = aObject.QueryInterface(Ci.nsIAccessibleCursorable);
+AndroidPresenter.prototype.tabSelected = function(aDocObj) {
+  // Send a pivot change message with the full context utterance for this doc.
+  let vcDoc = aDocObj.QueryInterface(Ci.nsIAccessibleCursorable);
   let context = [];
 
-  let parent = vcDoc.virtualCursor.position || aObject;
-  while ((parent = parent.parent))
+  let parent = vcDoc.virtualCursor.position || aDocObj;
+  while ((parent = parent.parent)) {
     context.push(parent);
+    if (parent == aDocObj)
+      break;
+  }
+
   context.reverse();
 
-  this.pivotChanged(vcDoc.virtualCursor.position || aObject, context);
+  this.pivotChanged(vcDoc.virtualCursor.position || aDocObj, context);
+};
+
+AndroidPresenter.prototype.tabStateChanged = function(aDocObj, aPageState) {
+  let stateUtterance = UtteranceGenerator.
+    genForTabStateChange(aDocObj, aPageState);
+
+  if (!stateUtterance.length)
+    return;
+
+  this.sendMessageToJava({
+    gecko: {
+      type: 'Accessibility:Event',
+      eventType: ANDROID_TYPE_VIEW_TEXT_CHANGED,
+      text: stateUtterance,
+      addedCount: stateUtterance.join(' ').length,
+      removedCount: 0,
+      fromIndex: 0
+    }
+  });
 };
 
 AndroidPresenter.prototype.textChanged = function(aIsInserted, aStart, aLength, aText, aModifiedText) {
   let androidEvent = {
     type: 'Accessibility:Event',
     eventType: ANDROID_TYPE_VIEW_TEXT_CHANGED,
     text: [aText],
     fromIndex: aStart
--- a/accessible/src/jsat/UtteranceGenerator.jsm
+++ b/accessible/src/jsat/UtteranceGenerator.jsm
@@ -52,16 +52,34 @@ var UtteranceGenerator = {
 
     return func.apply(this, [aAccessible, roleString, flags]);
   },
 
   genForAction: function(aObject, aActionName) {
     return [gStringBundle.GetStringFromName(this.gActionMap[aActionName])];
   },
 
+  genForTabStateChange: function (aObject, aTabState) {
+    switch (aTabState) {
+      case 'newtab':
+        return [gStringBundle.GetStringFromName('tabNew')];
+      case 'loading':
+        return [gStringBundle.GetStringFromName('tabLoading')];
+      case 'loaded':
+        return [aObject.name || '',
+                gStringBundle.GetStringFromName('tabLoaded')];
+      case 'loadstopped':
+        return [gStringBundle.GetStringFromName('tabLoadStopped')];
+      case 'reload':
+        return [gStringBundle.GetStringFromName('tabReload')];
+      default:
+        return [];
+    }
+  },
+
   verbosityRoleMap: {
     'menubar': INCLUDE_ROLE,
     'scrollbar': INCLUDE_ROLE,
     'grip': INCLUDE_ROLE,
     'alert': INCLUDE_ROLE,
     'menupopup': INCLUDE_ROLE,
     'menuitem': INCLUDE_ROLE,
     'tooltip': INCLUDE_ROLE,
--- a/dom/locales/en-US/chrome/accessibility/AccessFu.properties
+++ b/dom/locales/en-US/chrome/accessibility/AccessFu.properties
@@ -86,9 +86,16 @@ uncheckAction  =      unchecked
 selectAction   =      selected
 openAction     =      opened
 closeAction    =      closed
 switchAction   =      switched
 clickAction    =      clicked
 collapseAction =      collapsed
 expandAction   =      expanded
 activateAction =      activated
-cycleAction    =      cycled
\ No newline at end of file
+cycleAction    =      cycled
+
+# Tab states
+tabLoading     =      loading
+tabLoaded      =      loaded
+tabNew         =      new tab
+tabLoadStopped =      loading stopped
+tabReload      =      reloading