Bug 893153 - Virtual cursor control refactor, fixes navigating in hidden frames. r=davidb r=maxli
authorEitan Isaacson <eitan@monotonous.org>
Tue, 16 Jul 2013 11:45:17 -0700
changeset 138741 d9579a00bb9301ee682a0ef63791d6a34d569997
parent 138740 0d0bb07cb10eb4ca80ceb885016b5d867d52bafa
child 138742 f3169dd4bfb6f493c7672fcaa62091fd39824f70
push idunknown
push userunknown
push dateunknown
reviewersdavidb, maxli
bugs893153
milestone25.0a1
Bug 893153 - Virtual cursor control refactor, fixes navigating in hidden frames. r=davidb r=maxli
accessible/src/jsat/AccessFu.jsm
accessible/src/jsat/Utils.jsm
accessible/src/jsat/content-script.js
--- a/accessible/src/jsat/AccessFu.jsm
+++ b/accessible/src/jsat/AccessFu.jsm
@@ -269,19 +269,17 @@ this.AccessFu = {
         this.Input.moveCursor('movePrevious', 'Simple', 'gesture');
         break;
       case 'Accessibility:ActivateObject':
         this.Input.activateCurrent(JSON.parse(aData));
         break;
       case 'Accessibility:Focus':
         this._focused = JSON.parse(aData);
         if (this._focused) {
-          let mm = Utils.getMessageManager(Utils.CurrentBrowser);
-          mm.sendAsyncMessage('AccessFu:VirtualCursor',
-                              {action: 'whereIsIt', move: true});
+          this.showCurrent(true);
         }
         break;
       case 'Accessibility:MoveCaret':
         this.Input.moveCaret(JSON.parse(aData));
         break;
       case 'remote-browser-frame-shown':
       case 'in-process-browser-or-app-frame-shown':
       {
@@ -322,30 +320,34 @@ this.AccessFu = {
           this._removeMessageListeners(mm);
           this._processedMessageManagers.splice(mmIndex, 1);
         }
         break;
       }
       case 'TabSelect':
       {
         if (this._focused) {
-          let mm = Utils.getMessageManager(Utils.CurrentBrowser);
           // We delay this for half a second so the awesomebar could close,
           // and we could use the current coordinates for the content item.
           // XXX TODO figure out how to avoid magic wait here.
           Utils.win.setTimeout(
             function () {
-              mm.sendAsyncMessage('AccessFu:VirtualCursor', {action: 'whereIsIt'});
-            }, 500);
+              this.showCurrent(false);
+            }.bind(this), 500);
         }
         break;
       }
     }
   },
 
+  showCurrent: function showCurrent(aMove) {
+    let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+    mm.sendAsyncMessage('AccessFu:ShowCurrent', { move: aMove });
+  },
+
   announce: function announce(aAnnouncement) {
     this._output(Presentation.announce(aAnnouncement),
                  Utils.CurrentBrowser);
   },
 
   // So we don't enable/disable twice
   _enabled: false,
 
@@ -627,18 +629,17 @@ var Input = {
   _handleGesture: function _handleGesture(aGesture) {
     let gestureName = aGesture.type + aGesture.touches.length;
     Logger.info('Gesture', aGesture.type,
                 '(fingers: ' + aGesture.touches.length + ')');
 
     switch (gestureName) {
       case 'dwell1':
       case 'explore1':
-        this.moveCursor('moveToPoint', 'SimpleTouch', 'gesture',
-                        aGesture.x, aGesture.y);
+        this.moveToPoint('SimpleTouch', aGesture.x, aGesture.y);
         break;
       case 'doubletap1':
         this.activateCurrent();
         break;
       case 'doubletaphold1':
         this.sendContextMenuMessage();
         break;
       case 'swiperight1':
@@ -749,22 +750,28 @@ var Input = {
     default:
       return;
     }
 
     aEvent.preventDefault();
     aEvent.stopPropagation();
   },
 
-  moveCursor: function moveCursor(aAction, aRule, aInputType, aX, aY) {
+  moveToPoint: function moveToPoint(aRule, aX, aY) {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
-    mm.sendAsyncMessage('AccessFu:VirtualCursor',
+    mm.sendAsyncMessage('AccessFu:MoveToPoint', {rule: aRule,
+                                                 x: aX, y: aY,
+                                                 origin: 'top'});
+  },
+
+  moveCursor: function moveCursor(aAction, aRule, aInputType) {
+    let mm = Utils.getMessageManager(Utils.CurrentBrowser);
+    mm.sendAsyncMessage('AccessFu:MoveCursor',
                         {action: aAction, rule: aRule,
-                         x: aX, y: aY, origin: 'top',
-                         inputType: aInputType});
+                         origin: 'top', inputType: aInputType});
   },
 
   moveCaret: function moveCaret(aDetails) {
     if (!this.editState.editing) {
       return;
     }
 
     aDetails.atStart = this.editState.atStart;
--- a/accessible/src/jsat/Utils.jsm
+++ b/accessible/src/jsat/Utils.jsm
@@ -229,17 +229,18 @@ this.Utils = {
   getBounds: function getBounds(aAccessible) {
       let objX = {}, objY = {}, objW = {}, objH = {};
       aAccessible.getBounds(objX, objY, objW, objH);
       return new Rect(objX.value, objY.value, objW.value, objH.value);
   },
 
   inHiddenSubtree: function inHiddenSubtree(aAccessible) {
     for (let acc=aAccessible; acc; acc=acc.parent) {
-      if (JSON.parse(Utils.getAttributes(acc).hidden)) {
+      let hidden = Utils.getAttributes(acc).hidden;
+      if (hidden && JSON.parse(hidden)) {
         return true;
       }
     }
     return false;
   },
 
   isAliveAndVisible: function isAliveAndVisible(aAccessible) {
     if (!aAccessible) {
--- a/accessible/src/jsat/content-script.js
+++ b/accessible/src/jsat/content-script.js
@@ -21,110 +21,146 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   'resource://gre/modules/accessibility/EventManager.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, 'ObjectWrapper',
   'resource://gre/modules/ObjectWrapper.jsm');
 
 Logger.debug('content-script.js');
 
 let eventManager = null;
 
-function virtualCursorControl(aMessage) {
-  if (Logger.logLevel >= Logger.DEBUG)
-    Logger.debug(aMessage.name, JSON.stringify(aMessage.json));
+function moveCursor(aMessage) {
+  if (Logger.logLevel >= Logger.DEBUG) {
+    Logger.debug(aMessage.name, JSON.stringify(aMessage.json, null, ' '));
+  }
 
-  try {
-    let vc = Utils.getVirtualCursor(content.document);
-    let origin = aMessage.json.origin;
-    if (origin != 'child') {
-      if (forwardMessage(vc, aMessage))
-        return;
-    }
+  let vc = Utils.getVirtualCursor(content.document);
+  let origin = aMessage.json.origin;
+  let action = aMessage.json.action;
+  let rule = TraversalRules[aMessage.json.rule];
 
-    let details = aMessage.json;
-    let rule = TraversalRules[details.rule];
-    let moved = 0;
-    switch (details.action) {
-    case 'moveFirst':
-    case 'moveLast':
-      moved = vc[details.action](rule);
-      break;
-    case 'moveNext':
-    case 'movePrevious':
-      try {
-        if (origin == 'parent' && vc.position == null) {
-          if (details.action == 'moveNext')
-            moved = vc.moveFirst(rule);
-          else
-            moved = vc.moveLast(rule);
-        } else {
-          moved = vc[details.action](rule);
-        }
-      } catch (x) {
-        let acc = Utils.AccRetrieval.
-          getAccessibleFor(content.document.activeElement);
-        moved = vc.moveNext(rule, acc, true);
-      }
-      break;
-    case 'moveToPoint':
-      if (!this._ppcp) {
-        this._ppcp = Utils.getPixelsPerCSSPixel(content);
-      }
-      moved = vc.moveToPoint(rule,
-                             details.x * this._ppcp, details.y * this._ppcp,
-                             true);
-      break;
-    case 'whereIsIt':
-      if (!forwardMessage(vc, aMessage)) {
-        if (!vc.position && aMessage.json.move)
-          vc.moveFirst(TraversalRules.Simple);
-        else {
-          sendAsyncMessage('AccessFu:Present', Presentation.pivotChanged(
-            vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE));
+  function moveCursorInner() {
+    try {
+      if (origin == 'parent' &&
+          !Utils.isAliveAndVisible(vc.position)) {
+        // We have a bad position in this frame, move vc to last or first item.
+        if (action == 'moveNext') {
+          return vc.moveFirst(rule);
+        } else if (action == 'movePrevious') {
+          return vc.moveLast(rule);
         }
       }
 
-      break;
-    default:
-      break;
+      return vc[action](rule);
+    } catch (x) {
+      if (action == 'moveNext' || action == 'movePrevious') {
+        // If we are trying to move next/prev put the vc on the focused item.
+        let acc = Utils.AccRetrieval.
+          getAccessibleFor(content.document.activeElement);
+        return vc.moveNext(rule, acc, true);
+      } else {
+        throw x;
+      }
     }
 
-    if (moved == true) {
-      forwardMessage(vc, aMessage);
-    } else if (moved == false && details.action != 'moveToPoint') {
+    return false;
+  }
+
+  try {
+    if (origin != 'child' &&
+        forwardToChild(aMessage, moveCursor, vc.position)) {
+      // We successfully forwarded the move to the child document.
+      return;
+    }
+
+    if (moveCursorInner()) {
+      // If we moved, try forwarding the message to the new position,
+      // it may be a frame with a vc of its own.
+      forwardToChild(aMessage, moveCursor, vc.position);
+    } else {
+      // If we did not move, we probably reached the end or start of the
+      // document, go back to parent content and move us out of the iframe.
       if (origin == 'parent') {
         vc.position = null;
       }
-      aMessage.json.origin = 'child';
-      sendAsyncMessage('AccessFu:VirtualCursor', aMessage.json);
+      forwardToParent(aMessage);
     }
   } catch (x) {
-    Logger.logException(x, 'Failed to move virtual cursor');
+    Logger.logException(x, 'Cursor move failed');
   }
 }
 
-function forwardMessage(aVirtualCursor, aMessage) {
+function moveToPoint(aMessage) {
+  if (Logger.logLevel >= Logger.DEBUG) {
+    Logger.debug(aMessage.name, JSON.stringify(aMessage.json, null, ' '));
+  }
+
+  let vc = Utils.getVirtualCursor(content.document);
+  let details = aMessage.json;
+  let rule = TraversalRules[details.rule];
+
   try {
-    let acc = aVirtualCursor.position;
-    if (acc && acc.role == ROLE_INTERNAL_FRAME) {
-      let mm = Utils.getMessageManager(acc.DOMNode);
-      mm.addMessageListener(aMessage.name, virtualCursorControl);
-      aMessage.json.origin = 'parent';
-      if (Utils.isContentProcess) {
-        // XXX: OOP content's screen offset is 0,
-        // so we remove the real screen offset here.
-        aMessage.json.x -= content.mozInnerScreenX;
-        aMessage.json.y -= content.mozInnerScreenY;
-      }
-      mm.sendAsyncMessage(aMessage.name, aMessage.json);
-      return true;
+    if (!this._ppcp) {
+      this._ppcp = Utils.getPixelsPerCSSPixel(content);
+    }
+    vc.moveToPoint(rule, details.x * this._ppcp, details.y * this._ppcp, true);
+    forwardToChild(aMessage, moveToPoint, vc.position);
+  } catch (x) {
+    Logger.logException(x, 'Failed move to point');
+  }
+}
+
+function showCurrent(aMessage) {
+  if (Logger.logLevel >= Logger.DEBUG) {
+    Logger.debug(aMessage.name, JSON.stringify(aMessage.json, null, ' '));
+  }
+
+  let vc = Utils.getVirtualCursor(content.document);
+
+  if (!forwardToChild(vc, showCurrent, aMessage)) {
+    if (!vc.position && aMessage.json.move) {
+      vc.moveFirst(TraversalRules.Simple);
+    } else {
+      sendAsyncMessage('AccessFu:Present', Presentation.pivotChanged(
+                         vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE));
     }
-  } catch (x) {
-    // Frame may be hidden, we regard this case as false.
+  }
+}
+
+function forwardToParent(aMessage) {
+  // XXX: This is a silly way to make a deep copy
+  let newJSON = JSON.parse(JSON.stringify(aMessage.json));
+  newJSON.origin = 'child';
+  sendAsyncMessage(aMessage.name, newJSON);
+}
+
+function forwardToChild(aMessage, aListener, aVCPosition) {
+  let acc = aVCPosition || Utils.getVirtualCursor(content.document).position;
+
+  if (!Utils.isAliveAndVisible(acc) || acc.role != ROLE_INTERNAL_FRAME) {
+    return false;
   }
-  return false;
+
+  if (Logger.logLevel >= Logger.DEBUG) {
+    Logger.debug('forwardToChild', Logger.accessibleToString(acc),
+                 aMessage.name, JSON.stringify(aMessage.json, null, '  '));
+  }
+
+  let mm = Utils.getMessageManager(acc.DOMNode);
+  mm.addMessageListener(aMessage.name, aListener);
+  // XXX: This is a silly way to make a deep copy
+  let newJSON = JSON.parse(JSON.stringify(aMessage.json));
+  newJSON.origin = 'parent';
+  if (Utils.isContentProcess) {
+    // XXX: OOP content's screen offset is 0,
+    // so we remove the real screen offset here.
+    newJSON.x -= content.mozInnerScreenX;
+    newJSON.y -= content.mozInnerScreenY;
+  }
+  mm.sendAsyncMessage(aMessage.name, newJSON);
+  return true;
 }
 
 function activateCurrent(aMessage) {
   Logger.debug('activateCurrent');
   function activateAccessible(aAccessible) {
     if (aAccessible.actionCount > 0) {
       aAccessible.doAction(0);
     } else {
@@ -169,33 +205,34 @@ function activateCurrent(aMessage) {
   }
 
   let focusedAcc = Utils.AccRetrieval.getAccessibleFor(content.document.activeElement);
   if (focusedAcc && focusedAcc.role === ROLE_ENTRY) {
     moveCaretTo(focusedAcc, aMessage.json.offset);
     return;
   }
 
-  let vc = Utils.getVirtualCursor(content.document);
-  if (!forwardMessage(vc, aMessage))
-    activateAccessible(vc.position);
+  let position = Utils.getVirtualCursor(content.document).position;
+  if (!forwardToChild(aMessage, activateCurrent, position)) {
+    activateAccessible(position);
+  }
 }
 
 function activateContextMenu(aMessage) {
   function sendContextMenuCoordinates(aAccessible) {
     let objX = {}, objY = {}, objW = {}, objH = {};
     aAccessible.getBounds(objX, objY, objW, objH);
     let x = objX.value + objW.value / 2;
     let y = objY.value + objH.value / 2;
     sendAsyncMessage('AccessFu:ActivateContextMenu', {x: x, y: y});
   }
 
-  let vc = Utils.getVirtualCursor(content.document);
-  if (!forwardMessage(vc, aMessage))
+  if (!forwardToChild(aMessage, activateContextMenu, vc.position)) {
     sendContextMenuCoordinates(vc.position);
+  }
 }
 
 function moveCaret(aMessage) {
   const MOVEMENT_GRANULARITY_CHARACTER = 1;
   const MOVEMENT_GRANULARITY_WORD = 2;
   const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
 
   let direction = aMessage.json.direction;
@@ -310,53 +347,56 @@ function scroll(aMessage) {
                 (page < 0 && content.scrollX > 0))) {
       content.scroll(content.innerWidth * page + content.scrollX);
       return true;
     }
 
     return false;
   }
 
-  if (aMessage.json.origin != 'child') {
-    if (forwardMessage(vc, aMessage))
-      return;
+  if (aMessage.json.origin != 'child' &&
+      forwardToChild(aMessage, scroll, vc.position)) {
+    return;
   }
 
   if (!tryToScroll()) {
     // Failed to scroll anything in this document. Try in parent document.
-    aMessage.json.origin = 'child';
-    sendAsyncMessage('AccessFu:Scroll', aMessage.json);
+    forwardToParent(aMessage);
   }
 }
 
 addMessageListener(
   'AccessFu:Start',
   function(m) {
     Logger.debug('AccessFu:Start');
     if (m.json.buildApp)
       Utils.MozBuildApp = m.json.buildApp;
 
-    addMessageListener('AccessFu:VirtualCursor', virtualCursorControl);
+    addMessageListener('AccessFu:MoveToPoint', moveToPoint);
+    addMessageListener('AccessFu:MoveCursor', moveCursor);
+    addMessageListener('AccessFu:ShowCurrent', showCurrent);
     addMessageListener('AccessFu:Activate', activateCurrent);
     addMessageListener('AccessFu:ContextMenu', activateContextMenu);
     addMessageListener('AccessFu:Scroll', scroll);
     addMessageListener('AccessFu:MoveCaret', moveCaret);
 
     if (!eventManager) {
       eventManager = new EventManager(this);
     }
     eventManager.start();
   });
 
 addMessageListener(
   'AccessFu:Stop',
   function(m) {
     Logger.debug('AccessFu:Stop');
 
-    removeMessageListener('AccessFu:VirtualCursor', virtualCursorControl);
+    removeMessageListener('AccessFu:MoveToPoint', moveToPoint);
+    removeMessageListener('AccessFu:MoveCursor', moveCursor);
+    removeMessageListener('AccessFu:ShowCurrent', showCurrent);
     removeMessageListener('AccessFu:Activate', activateCurrent);
     removeMessageListener('AccessFu:ContextMenu', activateContextMenu);
     removeMessageListener('AccessFu:Scroll', scroll);
     removeMessageListener('AccessFu:MoveCaret', moveCaret);
 
     eventManager.stop();
   });