Bug 1471951 - Support expand selection with caret (2/2). r=yzen r=jchen
☠☠ backed out by 7236e824c48b ☠ ☠
authorEitan Isaacson <eitan@monotonous.org>
Thu, 12 Jul 2018 08:33:15 -0700
changeset 426343 cb7687c97e1f8ab7e233469823eb89afd8ba6322
parent 426342 590ba4c10edd5f488aeb57bbdcb7aeeac06b9628
child 426344 e6698ef51a7e2f049350b09d67fa7338d2bd405a
push id105211
push usereisaacson@mozilla.com
push dateThu, 12 Jul 2018 18:05:08 +0000
treeherdermozilla-inbound@cb7687c97e1f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen, jchen
bugs1471951
milestone63.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 1471951 - Support expand selection with caret (2/2). r=yzen r=jchen
accessible/jsat/ContentControl.jsm
accessible/jsat/EventManager.jsm
accessible/jsat/Presentation.jsm
accessible/tests/mochitest/jsat/test_text_editable_navigation.html
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
--- a/accessible/jsat/ContentControl.jsm
+++ b/accessible/jsat/ContentControl.jsm
@@ -17,17 +17,16 @@ ChromeUtils.defineModuleGetter(this, "Tr
   "resource://gre/modules/accessibility/Traversal.jsm");
 ChromeUtils.defineModuleGetter(this, "Presentation",
   "resource://gre/modules/accessibility/Presentation.jsm");
 
 var EXPORTED_SYMBOLS = ["ContentControl"];
 
 const MOVEMENT_GRANULARITY_CHARACTER = 1;
 const MOVEMENT_GRANULARITY_WORD = 2;
-const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
 
 const CLIPBOARD_COPY = 0x4000;
 const CLIPBOARD_PASTE = 0x8000;
 const CLIPBOARD_CUT = 0x10000;
 
 function ContentControl(aContentScope) {
   this._contentScope = Cu.getWeakReference(aContentScope);
   this._childMessageSenders = new WeakMap();
@@ -302,21 +301,26 @@ this.ContentControl.prototype = {
         "keypress", false, true, null, false, false, false, false, keycode, 0);
       elem.dispatchEvent(evt);
     }
 
     return true;
   },
 
   handleMoveByGranularity: function cc_handleMoveByGranularity(aMessage) {
-    let { direction, granularity } = aMessage.json;
-    let focusedAcc = Utils.AccService.getAccessibleFor(this.document.activeElement);
-    if (focusedAcc && Utils.getState(focusedAcc).contains(States.EDITABLE)) {
-      this.moveCaret(focusedAcc, direction, granularity);
-      return;
+    const { direction, granularity, select } = aMessage.json;
+    const focusedAcc =
+      Utils.AccService.getAccessibleFor(this.document.activeElement);
+    const editable =
+      focusedAcc && Utils.getState(focusedAcc).contains(States.EDITABLE) ?
+      focusedAcc.QueryInterface(Ci.nsIAccessibleText) : null;
+
+    if (editable) {
+      const caretOffset = editable.caretOffset;
+      this.vc.setTextRange(editable, caretOffset, caretOffset, false);
     }
 
     let pivotGranularity;
     switch (granularity) {
       case MOVEMENT_GRANULARITY_CHARACTER:
         pivotGranularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY;
         break;
       case MOVEMENT_GRANULARITY_WORD:
@@ -326,16 +330,31 @@ this.ContentControl.prototype = {
         return;
     }
 
     if (direction === "Previous") {
       this.vc.movePreviousByText(pivotGranularity);
     } else if (direction === "Next") {
       this.vc.moveNextByText(pivotGranularity);
     }
+
+    if (editable) {
+      const newOffset = direction === "Next" ?
+        this.vc.endOffset : this.vc.startOffset;
+      if (select) {
+        let anchor = editable.caretOffset;
+        if (editable.selectionCount) {
+          const [startSel, endSel] = Utils.getTextSelection(editable);
+          anchor = startSel == anchor ? endSel : startSel;
+        }
+        editable.setSelectionBounds(0, anchor, newOffset);
+      } else {
+        editable.caretOffset = newOffset;
+      }
+    }
   },
 
   handleSetSelection: function cc_handleSetSelection(aMessage) {
     const { start, end } = aMessage.json;
     const focusedAcc =
       Utils.AccService.getAccessibleFor(this.document.activeElement);
     if (focusedAcc) {
       const accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText);
@@ -379,57 +398,16 @@ this.ContentControl.prototype = {
     aText, aOldOffset, aNewOffset) {
     if (aOldOffset !== aNewOffset) {
       let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
         aOldOffset, aOldOffset, true);
       this._contentScope.get().sendAsyncMessage("AccessFu:Present", msg);
     }
   },
 
-  moveCaret: function cc_moveCaret(accessible, direction, granularity) {
-    let accText = accessible.QueryInterface(Ci.nsIAccessibleText);
-    let oldOffset = accText.caretOffset;
-    let text = accText.getText(0, accText.characterCount);
-
-    let start = {}, end = {};
-    if (direction === "Previous" && oldOffset > 0) {
-      switch (granularity) {
-        case MOVEMENT_GRANULARITY_CHARACTER:
-          accText.caretOffset--;
-          break;
-        case MOVEMENT_GRANULARITY_WORD:
-          accText.getTextBeforeOffset(accText.caretOffset,
-            Ci.nsIAccessibleText.BOUNDARY_WORD_START, start, end);
-          accText.caretOffset = end.value === accText.caretOffset ?
-            start.value : end.value;
-          break;
-        case MOVEMENT_GRANULARITY_PARAGRAPH:
-          let startOfParagraph = text.lastIndexOf("\n", accText.caretOffset - 1);
-          accText.caretOffset = startOfParagraph !== -1 ? startOfParagraph : 0;
-          break;
-      }
-    } else if (direction === "Next" && oldOffset < accText.characterCount) {
-      switch (granularity) {
-        case MOVEMENT_GRANULARITY_CHARACTER:
-          accText.caretOffset++;
-          break;
-        case MOVEMENT_GRANULARITY_WORD:
-          accText.getTextAtOffset(accText.caretOffset,
-                                  Ci.nsIAccessibleText.BOUNDARY_WORD_END, start, end);
-          accText.caretOffset = end.value;
-          break;
-        case MOVEMENT_GRANULARITY_PARAGRAPH:
-          accText.caretOffset = text.indexOf("\n", accText.caretOffset + 1);
-          break;
-      }
-    }
-
-    this.presentCaretChange(text, oldOffset, accText.caretOffset);
-  },
-
   getChildCursor: function cc_getChildCursor(aAccessible) {
     let acc = aAccessible || this.vc.position;
     if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) {
       let domNode = acc.DOMNode;
       let mm = this._childMessageSenders.get(domNode, null);
       if (!mm) {
         mm = Utils.getMessageManager(domNode);
         mm.addWeakMessageListener("AccessFu:MoveCursor", this);
@@ -492,17 +470,17 @@ this.ContentControl.prototype = {
       let acc = aAnchor;
       let rule = aOptions.onScreenOnly ?
         TraversalRules.SimpleOnScreen : TraversalRules.Simple;
       let forcePresentFunc = () => {
         if (aOptions.forcePresent) {
           this._contentScope.get().sendAsyncMessage(
             "AccessFu:Present", Presentation.pivotChanged(
               vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE,
-              vc.startOffset, vc.endOffset, false));
+              vc.startOffset, vc.endOffset));
         }
       };
 
       if (aOptions.noOpIfOnScreen &&
         Utils.isAliveAndVisible(vc.position, true)) {
         forcePresentFunc();
         return;
       }
@@ -513,21 +491,21 @@ this.ContentControl.prototype = {
       }
 
       let moved = false;
       let moveMethod = aOptions.moveMethod || "moveNext"; // default is moveNext
       let moveFirstOrLast = moveMethod in ["moveFirst", "moveLast"];
       if (!moveFirstOrLast || acc) {
         // We either need next/previous or there is an anchor we need to use.
         moved = vc[moveFirstOrLast ? "moveNext" : moveMethod](rule, acc, true,
-                                                              false);
+                                                              true);
       }
       if (moveFirstOrLast && !moved) {
         // We move to first/last after no anchor move happened or succeeded.
-        moved = vc[moveMethod](rule, false);
+        moved = vc[moveMethod](rule, true);
       }
 
       let sentToChild = this.sendToChild(vc, {
         name: "AccessFu:AutoMove",
         json: {
           moveMethod: aOptions.moveMethod,
           moveToFocused: aOptions.moveToFocused,
           noOpIfOnScreen: true,
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -131,33 +131,37 @@ this.EventManager.prototype = {
         (aEvent.accessibleDocument.DOMDocument.doctype &&
          aEvent.accessibleDocument.DOMDocument.doctype.name === "window")) {
       return;
     }
 
     switch (aEvent.eventType) {
       case Events.VIRTUALCURSOR_CHANGED:
       {
-        let pivot = aEvent.accessible.
-          QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
-        let position = pivot.position;
-        if (position && position.role == Roles.INTERNAL_FRAME)
+        if (!aEvent.isFromUserInput) {
           break;
-        let event = aEvent.
+        }
+
+        const event = aEvent.
           QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
-        let reason = event.reason;
-        let oldAccessible = event.oldAccessible;
+        const position = event.newAccessible;
 
+        // We pass control to the vc in the embedded frame.
+        if (position && position.role == Roles.INTERNAL_FRAME) {
+          break;
+        }
+
+        // Blur to document if new position is not explicitly focused.
         if (!Utils.getState(position).contains(States.FOCUSED)) {
           aEvent.accessibleDocument.takeFocus();
         }
+
         this.present(
-          Presentation.pivotChanged(position, oldAccessible, reason,
-                                    pivot.startOffset, pivot.endOffset,
-                                    aEvent.isFromUserInput));
+          Presentation.pivotChanged(position, event.oldAccessible, event.reason,
+                                    event.newStartOffset, event.newEndOffset));
 
         break;
       }
       case Events.STATE_CHANGE:
       {
         let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
         let state = Utils.getState(event);
         if (state.contains(States.CHECKED)) {
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -33,17 +33,17 @@ class AndroidPresentor {
   /**
    * The virtual cursor's position changed.
    * @param {PivotContext} aContext the context object for the new pivot
    *   position.
    * @param {int} aReason the reason for the pivot change.
    *   See nsIAccessiblePivot.
    * @param {bool} aIsFromUserInput the pivot change was invoked by the user
    */
-  pivotChanged(aPosition, aOldPosition, aReason, aStartOffset, aEndOffset, aIsUserInput) {
+  pivotChanged(aPosition, aOldPosition, aReason, aStartOffset, aEndOffset) {
     let context = new PivotContext(
       aPosition, aOldPosition, aStartOffset, aEndOffset);
     if (!context.accessible) {
       return null;
     }
 
     let androidEvents = [];
 
--- a/accessible/tests/mochitest/jsat/test_text_editable_navigation.html
+++ b/accessible/tests/mochitest/jsat/test_text_editable_navigation.html
@@ -60,38 +60,28 @@
       evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
         AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkMoveCaret(...evt, 20, 29);
 
       evt = await runner.moveNextByGranularity(MovementGranularity.WORD,
         AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
-      checkMoveCaret(...evt, 29, 36);
+      checkMoveCaret(...evt, 30, 36);
 
       evt = await runner.moveNextByGranularity(MovementGranularity.CHARACTER,
         AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkMoveCaret(...evt, 36, 37);
 
       evt = await runner.moveNextByGranularity(MovementGranularity.CHARACTER,
         AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
         AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
       checkMoveCaret(...evt, 37, 38);
 
-      evt = await runner.moveNextByGranularity(MovementGranularity.PARAGRAPH,
-        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
-      checkMoveCaret(...evt, 38, 59);
-
-      evt = await runner.movePreviousByGranularity(MovementGranularity.WORD,
-        AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-        AndroidEvents.VIEW_TEXT_SELECTION_CHANGED);
-      checkMoveCaret(...evt, 59, 53);
-
       evt = await runner.blur(AndroidEvents.VIEW_FOCUSED);
       is(evt.editable, false, "Focused out of editable");
     }
 
     function doTest() {
       var doc = currentTabDocument();
 
       addA11yLoadEvent(async function() {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -216,19 +216,21 @@ public class SessionAccessibility {
                             // FAKE_GRANULARITY_CHANGE_SHIFTER = -4
                             int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
                             if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
                                 int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
                                 data = new GeckoBundle(1);
                                 data.putInt("keyIndex", keyIndex);
                                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data);
                             } else if (granularity > 0) {
-                                data = new GeckoBundle(2);
+                                boolean extendSelection = arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
+                                data = new GeckoBundle(3);
                                 data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous");
                                 data.putInt("granularity", granularity);
+                                data.putBoolean("select", extendSelection);
                                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data);
                             }
                             return true;
                         case AccessibilityNodeInfo.ACTION_SET_SELECTION:
                             if (arguments == null) {
                                 return false;
                             }
                             int selectionStart = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT);