Bug 1471951 - Support expand selection with caret (2/2). r=yzen r=jchen
authorEitan Isaacson <eitan@monotonous.org>
Thu, 12 Jul 2018 08:33:15 -0700
changeset 426754 d75a549c4e930a50b240b0d28f574dd211d6be28
parent 426753 1839e4b613bd71540882780c7968d8c60ca68aa5
child 426755 ddd3a8daeb331d69da2dd7547d5bc87fdc97acf1
push id34284
push userbtara@mozilla.com
push dateMon, 16 Jul 2018 21:55:18 +0000
treeherdermozilla-central@da5b3e1dca89 [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);