Bug 773749 - Add Jelly Bean support to AccessFu. r=davidb
authorEitan Isaacson <eitan@monotonous.org>
Mon, 20 Aug 2012 18:29:22 -0400
changeset 102936 a295ff4319fbafd50072a69fd233b95cdb515d2d
parent 102935 3e0e57eb237aac0a5f9c59927c46d7d439e59e7b
child 102937 0d4cb05fb97fa3a59d24a12728c06fc498ffebad
push id23317
push userryanvm@gmail.com
push dateWed, 22 Aug 2012 02:05:02 +0000
treeherdermozilla-central@abc17059522b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdavidb
bugs773749
milestone17.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 773749 - Add Jelly Bean support to AccessFu. r=davidb
accessible/src/jsat/AccessFu.jsm
accessible/src/jsat/Presenters.jsm
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -45,16 +45,19 @@ var AccessFu = {
     this.prefsBranch.addObserver('activate', this, false);
     this.prefsBranch.addObserver('explorebytouch', this, false);
 
     this.touchAdapter = TouchAdapter;
 
     switch(Utils.MozBuildApp) {
       case 'mobile/android':
         Services.obs.addObserver(this, 'Accessibility:Settings', false);
+        Services.obs.addObserver(this, 'Accessibility:NextObject', false);
+        Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
+        Services.obs.addObserver(this, 'Accessibility:CurrentObject', false);
         this.touchAdapter = AndroidTouchAdapter;
         break;
       case 'b2g':
         aWindow.addEventListener(
           'ContentStart',
           (function(event) {
              let content = aWindow.shell.contentBrowser.contentWindow;
              content.addEventListener('mozContentEvent', this, false, true);
@@ -82,20 +85,22 @@ var AccessFu = {
     let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
     this.stylesheet = this.chromeWin.document.createProcessingInstruction(
       'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
     this.chromeWin.document.insertBefore(this.stylesheet, this.chromeWin.document.firstChild);
 
     this.addPresenter(new VisualPresenter());
 
     // Implicitly add the Android presenter on Android.
-    if (Utils.MozBuildApp == 'mobile/android')
-      this.addPresenter(new AndroidPresenter());
-    else if (Utils.MozBuildApp == 'b2g')
+    if (Utils.MozBuildApp == 'mobile/android') {
+      this._androidPresenter = new AndroidPresenter();
+      this.addPresenter(this._androidPresenter);
+    } else if (Utils.MozBuildApp == 'b2g') {
       this.addPresenter(new SpeechPresenter());
+    }
 
     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);
@@ -232,16 +237,27 @@ var AccessFu = {
   },
 
   observe: function observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case 'Accessibility:Settings':
         this._processPreferences(JSON.parse(aData).enabled + 0,
                                  JSON.parse(aData).exploreByTouch + 0);
         break;
+      case 'Accessibility:NextObject':
+        VirtualCursorController.
+          moveForward(Utils.getCurrentContentDoc(this.chromeWin));
+        break;
+      case 'Accessibility:PreviousObject':
+        VirtualCursorController.
+          moveBackward(Utils.getCurrentContentDoc(this.chromeWin));
+        break;
+      case 'Accessibility:CurrentObject':
+        this._androidPresenter.accessibilityFocus();
+        break;
       case 'nsPref:changed':
         this._processPreferences(this.prefsBranch.getIntPref('activate'),
                                  this.prefsBranch.getIntPref('explorebytouch'));
         break;
       case 'accessible-event':
         let event;
         try {
           event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
--- a/accessible/src/jsat/Presenters.jsm
+++ b/accessible/src/jsat/Presenters.jsm
@@ -6,16 +6,17 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
 Cu.import('resource://gre/modules/accessibility/UtteranceGenerator.jsm');
+Cu.import('resource://gre/modules/Geometry.jsm');
 
 var EXPORTED_SYMBOLS = ['VisualPresenter',
                         'AndroidPresenter',
                         'DummyAndroidPresenter',
                         'SpeechPresenter',
                         'PresenterContext'];
 
 /**
@@ -132,32 +133,32 @@ VisualPresenter.prototype = {
   },
 
   detach: function VisualPresenter_detach() {
     this.highlightBox.parentNode.removeChild(this.highlightBox);
     this.highlightBox = this.stylesheet = null;
   },
 
   viewportChanged: function VisualPresenter_viewportChanged() {
-    if (this._currentObject)
-      this._highlight(this._currentObject);
+    if (this._currentContext)
+      this._highlight(this._currentContext);
   },
 
   pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) {
-    this._currentObject = aContext.accessible;
+    this._currentContext = aContext;
 
     if (!aContext.accessible) {
       this._hide();
       return;
     }
 
     try {
       aContext.accessible.scrollTo(
         Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
-      this._highlight(aContext.accessible);
+      this._highlight(aContext);
     } catch (e) {
       Logger.error('Failed to get bounds: ' + e);
       return;
     }
   },
 
   tabSelected: function VisualPresenter_tabSelected(aDocContext, aVCContext) {
     this.pivotChanged(aVCContext, Ci.nsIAccessiblePivot.REASON_NONE);
@@ -170,52 +171,27 @@ VisualPresenter.prototype = {
   },
 
   // Internals
 
   _hide: function _hide() {
     this.highlightBox.style.display = 'none';
   },
 
-  _highlight: function _highlight(aObject) {
+  _highlight: function _highlight(aContext) {
     let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 };
-    let bounds = this._getBounds(aObject, vp.zoom);
+    let r = aContext.bounds.scale(vp.zoom, vp.zoom).expandToIntegers();
 
     // First hide it to avoid flickering when changing the style.
     this.highlightBox.style.display = 'none';
-    this.highlightBox.style.top = bounds.top + 'px';
-    this.highlightBox.style.left = bounds.left + 'px';
-    this.highlightBox.style.width = bounds.width + 'px';
-    this.highlightBox.style.height = bounds.height + 'px';
+    this.highlightBox.style.top = (r.top - this.BORDER_PADDING) + 'px';
+    this.highlightBox.style.left = (r.left - this.BORDER_PADDING) + 'px';
+    this.highlightBox.style.width = (r.width + this.BORDER_PADDING*2) + 'px';
+    this.highlightBox.style.height = (r.height + this.BORDER_PADDING*2) + 'px';
     this.highlightBox.style.display = 'block';
-  },
-
-  _getBounds: function _getBounds(aObject, aZoom, aStart, aEnd) {
-    let objX = {}, objY = {}, objW = {}, objH = {};
-
-    if (aEnd >= 0 && aStart >= 0 && aEnd != aStart) {
-      // TODO: Get bounds for text ranges. Leaving this blank until we have
-      // proper text navigation in the virtual cursor.
-    }
-
-    aObject.getBounds(objX, objY, objW, objH);
-
-    // Can't specify relative coords in nsIAccessible.getBounds, so we do it.
-    let docX = {}, docY = {};
-    let docRoot = aObject.rootDocument.QueryInterface(Ci.nsIAccessible);
-    docRoot.getBounds(docX, docY, {}, {});
-
-    let rv = {
-      left: Math.round((objX.value - docX.value - this.BORDER_PADDING) * aZoom),
-      top: Math.round((objY.value - docY.value - this.BORDER_PADDING) * aZoom),
-      width: Math.round((objW.value + (this.BORDER_PADDING * 2)) * aZoom),
-      height: Math.round((objH.value + (this.BORDER_PADDING * 2)) * aZoom)
-    };
-
-    return rv;
   }
 };
 
 /**
  * Android presenter. Fires Android a11y events.
  */
 
 function AndroidPresenter() {}
@@ -228,40 +204,49 @@ AndroidPresenter.prototype = {
   ANDROID_VIEW_LONG_CLICKED: 0x02,
   ANDROID_VIEW_SELECTED: 0x04,
   ANDROID_VIEW_FOCUSED: 0x08,
   ANDROID_VIEW_TEXT_CHANGED: 0x10,
   ANDROID_WINDOW_STATE_CHANGED: 0x20,
   ANDROID_VIEW_HOVER_ENTER: 0x80,
   ANDROID_VIEW_HOVER_EXIT: 0x100,
   ANDROID_VIEW_SCROLLED: 0x1000,
+  ANDROID_ANNOUNCEMENT: 0x4000,
+  ANDROID_VIEW_ACCESSIBILITY_FOCUSED: 0x8000,
 
   attach: function AndroidPresenter_attach(aWindow) {
     this.chromeWin = aWindow;
   },
 
   pivotChanged: function AndroidPresenter_pivotChanged(aContext, aReason) {
     if (!aContext.accessible)
       return;
 
+    this._currentContext = aContext;
+
     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.
       this.sendMessageToJava({
          gecko: {
            type: 'Accessibility:Event',
            eventType: this.ANDROID_VIEW_HOVER_EXIT,
            text: []
          }
       });
     }
 
+    let vp = Utils.getViewport(this.chromeWin) || { zoom: 1.0, offsetY: 0 };
+    let bounds = aContext.bounds.scale(vp.zoom, vp.zoom).expandToIntegers();
     let output = [];
 
     aContext.newAncestry.forEach(
       function(acc) {
         output.push.apply(output, UtteranceGenerator.genForObject(acc));
       }
     );
 
@@ -272,20 +257,19 @@ AndroidPresenter.prototype = {
       function(acc) {
         output.push.apply(output, UtteranceGenerator.genForObject(acc));
       }
     );
 
     this.sendMessageToJava({
       gecko: {
         type: 'Accessibility:Event',
-        eventType: isExploreByTouch ?
-          this.ANDROID_VIEW_HOVER_ENTER :
-          this.ANDROID_VIEW_FOCUSED,
-        text: output
+        eventType: (isExploreByTouch) ? this.ANDROID_VIEW_HOVER_ENTER : focusEventType,
+        text: output,
+        bounds: bounds
       }
     });
   },
 
   actionInvoked: function AndroidPresenter_actionInvoked(aObject, aActionName) {
     this.sendMessageToJava({
       gecko: {
         type: 'Accessibility:Event',
@@ -355,25 +339,31 @@ AndroidPresenter.prototype = {
 
   _appAnnounce: function _appAnnounce(aUtterance) {
     if (!aUtterance.length)
       return;
 
     this.sendMessageToJava({
       gecko: {
         type: 'Accessibility:Event',
-        eventType: this.ANDROID_VIEW_TEXT_CHANGED,
+        eventType: (Utils.AndroidSdkVersion >= 16) ?
+          this.ANDROID_ANNOUNCEMENT : this.ANDROID_VIEW_TEXT_CHANGED,
         text: aUtterance,
         addedCount: aUtterance.join(' ').length,
         removedCount: 0,
         fromIndex: 0
       }
     });
   },
 
+  accessibilityFocus: function AndroidPresenter_accessibilityFocus() {
+    if (this._currentContext)
+      this.pivotChanged(this._currentContext);
+  },
+
   sendMessageToJava: function AndroidPresenter_sendMessageTojava(aMessage) {
     return Cc['@mozilla.org/android/bridge;1'].
       getService(Ci.nsIAndroidBridge).
       handleGeckoMessage(JSON.stringify(aMessage));
   }
 };
 
 /**
@@ -503,16 +493,35 @@ PresenterContext.prototype = {
     }
 
     if (!this._subtreePreOrder)
       this._subtreePreOrder = traversePreorder(this._accessible);
 
     return this._subtreePreOrder;
   },
 
+  get bounds() {
+    if (!this._bounds) {
+      let objX = {}, objY = {}, objW = {}, objH = {};
+
+      this._accessible.getBounds(objX, objY, objW, objH);
+
+      // Can't specify relative coords in nsIAccessible.getBounds, so we do it.
+      let docX = {}, docY = {};
+      let docRoot = this._accessible.rootDocument.
+        QueryInterface(Ci.nsIAccessible);
+      docRoot.getBounds(docX, docY, {}, {});
+
+      this._bounds = new Rect(objX.value, objY.value, objW.value, objH.value).
+        translate(-docX.value, -docY.value);
+    }
+
+    return this._bounds.clone();
+  },
+
   _isDefunct: function _isDefunct(aAccessible) {
     try {
       let extstate = {};
       aAccessible.getState({}, extstate);
       return !!(aAccessible.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT);
     } catch (x) {
       return true;
     }