Bug 1128187 - Allow SelectionHandles in mixed LTR-RTL content, r=margaret
authorMark Capella <markcapella@twcny.rr.com>
Thu, 05 Feb 2015 21:47:15 -0500
changeset 227771 0a92f400101e19d42eec636a099cdc0c1a27ebb9
parent 227770 36284346398c55641aca8ef48863f7b86bc14219
child 227772 00a4e75da857b51978751099679ceb4f9984226f
push id28239
push userryanvm@gmail.com
push dateFri, 06 Feb 2015 13:52:40 +0000
treeherdermozilla-central@03b0004eba32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs1128187
milestone38.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 1128187 - Allow SelectionHandles in mixed LTR-RTL content, r=margaret
mobile/android/base/TextSelection.java
mobile/android/chrome/content/SelectionHandler.js
--- a/mobile/android/base/TextSelection.java
+++ b/mobile/android/base/TextSelection.java
@@ -163,23 +163,24 @@ class TextSelection extends Layer implem
                         }
 
                         mActionModeTimerTask = new ActionModeTimerTask();
                         mActionModeTimer.schedule(mActionModeTimerTask, 250);
 
                         anchorHandle.setVisibility(View.GONE);
                         caretHandle.setVisibility(View.GONE);
                         focusHandle.setVisibility(View.GONE);
+
                     } else if (event.equals("TextSelection:PositionHandles")) {
-                        final boolean rtl = message.getBoolean("rtl");
                         final JSONArray positions = message.getJSONArray("positions");
                         for (int i=0; i < positions.length(); i++) {
                             JSONObject position = positions.getJSONObject(i);
-                            int left = position.getInt("left");
-                            int top = position.getInt("top");
+                            final int left = position.getInt("left");
+                            final int top = position.getInt("top");
+                            final boolean rtl = position.getBoolean("rtl");
 
                             TextSelectionHandle handle = getHandle(position.getString("handle"));
                             handle.setVisibility(position.getBoolean("hidden") ? View.GONE : View.VISIBLE);
                             handle.positionFromGecko(left, top, rtl);
                         }
                     }
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "JSON exception", e);
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -34,16 +34,20 @@ var SelectionHandler = {
   TYPE_SELECTION: 2,
 
   SELECT_ALL: 0,
   SELECT_AT_POINT: 1,
 
   // Keeps track of data about the dimensions of the selection. Coordinates
   // stored here are relative to the _contentWindow window.
   _cache: null,
+  _targetIsRTL: false,
+  _anchorIsRTL: false,
+  _focusIsRTL: false,
+
   _activeType: 0, // TYPE_NONE
 
   _draggingHandles: false, // True while user drags text selection handles
   _dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events
   _dragStartFocusOffset: null, // Editables need initial pos during HandleMove events
 
   _ignoreCompositionChanges: false, // Persist caret during IME composition updates
   _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
@@ -73,18 +77,16 @@ var SelectionHandler = {
     this._targetElementRef = Cu.getWeakReference(aTargetElement);
   },
 
   get _domWinUtils() {
     return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
                                                     getInterface(Ci.nsIDOMWindowUtils);
   },
 
-  _isRTL: false,
-
   _addObservers: function sh_addObservers() {
     Services.obs.addObserver(this, "Gesture:SingleTap", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
     Services.obs.addObserver(this, "after-viewport-change", false);
     Services.obs.addObserver(this, "TextSelection:Move", false);
     Services.obs.addObserver(this, "TextSelection:Position", false);
     Services.obs.addObserver(this, "TextSelection:End", false);
     Services.obs.addObserver(this, "TextSelection:Action", false);
@@ -791,17 +793,17 @@ var SelectionHandler = {
         aElement.blur();
       }
       // Ensure targetElement is now focused normally
       aElement.focus();
     }
 
     this._stopDraggingHandles();
     this._contentWindow = aElement.ownerDocument.defaultView;
-    this._isRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
+    this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
 
     this._addObservers();
   },
 
   _getSelection: function sh_getSelection() {
     if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
       return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selection;
     else
@@ -1038,17 +1040,16 @@ var SelectionHandler = {
       let positions = this._prevHandlePositions;
       for (let i in positions) {
         positions[i].hidden = true;
       }
 
       Messaging.sendRequest({
         type: "TextSelection:PositionHandles",
         positions: positions,
-        rtl: this._isRTL
       });
     }
   },
 
   /**
    * Cancel any current deferred _closeSelection() action.
    */
   _cancelDeferredCloseSelection: function() {
@@ -1104,17 +1105,17 @@ var SelectionHandler = {
     if (this._activeType == this.TYPE_CURSOR) {
       BrowserApp.deck.removeEventListener("keyup", this);
       BrowserApp.deck.removeEventListener("compositionupdate", this);
       BrowserApp.deck.removeEventListener("compositionend", this);
     }
 
     this._contentWindow = null;
     this._targetElement = null;
-    this._isRTL = false;
+    this._targetIsRTL = false;
     this._cache = null;
     this._ignoreCompositionChanges = false;
     this._prevHandlePositions = [];
     this._prevTargetElementHasText = null;
 
     this._activeType = this.TYPE_NONE;
   },
 
@@ -1160,35 +1161,63 @@ var SelectionHandler = {
 
     selection.collapse(selection.focusNode, selection.focusOffset);
     selection.extend(newFocusNode, newFocusOffset);
   },
 
   /*
    * Updates the TYPE_SELECTION cache, with the handle anchor/focus point values
    * of the current selection. Passed to Java for UI positioning only.
+   *
+   * Note that the anchor handle and focus handle can reference text in nodes
+   * with mixed direction. (ie a.direction = "rtl" while f.direction = "ltr").
    */
   _updateCacheForSelection: function() {
-    let rects = this._getSelection().getRangeAt(0).getClientRects();
+    let selection = this._getSelection();
+    let rects = selection.getRangeAt(0).getClientRects();
     if (rects.length == 0) {
       // nsISelection object exists, but there's nothing actually selected
       throw "Failed to update cache for invalid selection";
     }
 
+    // Right-to-Left (ie: Hebrew) anchorPt is on right,
+    // Left-to-Right (ie: English) anchorPt is on left.
+    this._anchorIsRTL = this._isNodeRTL(selection.anchorNode);
     let anchorIdx = 0;
+    this._cache.anchorPt = (this._anchorIsRTL) ?
+      new Point(rects[anchorIdx].right, rects[anchorIdx].bottom) :
+      new Point(rects[anchorIdx].left, rects[anchorIdx].bottom);
+
+    // Right-to-Left (ie: Hebrew) focusPt is on left,
+    // Left-to-Right (ie: English) focusPt is on right.
+    this._focusIsRTL = this._isNodeRTL(selection.focusNode);
     let focusIdx = rects.length - 1;
-    if (this._isRTL) {
-      // Right-to-Left (ie: Hebrew) anchorPt is on right, focusPt is on left.
-      this._cache.anchorPt = new Point(rects[anchorIdx].right, rects[anchorIdx].bottom);
-      this._cache.focusPt = new Point(rects[focusIdx].left, rects[focusIdx].bottom);
-    } else {
-      // Left-to-Right (ie: English) anchorPt is on left, focusPt is on right.
-      this._cache.anchorPt = new Point(rects[anchorIdx].left, rects[anchorIdx].bottom);
-      this._cache.focusPt = new Point(rects[focusIdx].right, rects[focusIdx].bottom);
+    this._cache.focusPt = (this._focusIsRTL) ?
+      new Point(rects[focusIdx].left, rects[focusIdx].bottom) :
+      new Point(rects[focusIdx].right, rects[focusIdx].bottom);
+  },
+
+  /*
+   * Return true if text associated with a node is RTL.
+   */
+  _isNodeRTL: function(node) {
+    // Find containing node that supports .direction attribute (needed
+    // when target node is #text for example).
+    while (node && !(node instanceof Element)) {
+      node = node.parentNode;
     }
+
+    // Worst case, use original direction from _targetElement.
+    if (!node) {
+      return this._targetIsRTL;
+    }
+
+    let nodeWin = node.ownerDocument.defaultView;
+    let nodeStyle = nodeWin.getComputedStyle(node, "");
+    return (nodeStyle.direction == "rtl");
   },
 
   _getHandlePositions: function sh_getHandlePositions(scroll) {
     // the checkHidden function tests to see if the given point is hidden inside an
     // iframe/subdocument. this is so that if we select some text inside an iframe and
     // scroll the iframe so the selection is out of view, we hide the handles rather
     // than having them float on top of the main page content.
     let checkHidden = function(x, y) {
@@ -1223,35 +1252,38 @@ var SelectionHandler = {
 
       // Translate coordinates to account for selections in sub-frames. We can't cache
       // this because the top-level page may have scrolled since selection started.
       let offset = this._getViewOffset();
 
       return  [{ handle: this.HANDLE_TYPE_ANCHOR,
                  left: anchorX + offset.x + scroll.X,
                  top: anchorY + offset.y + scroll.Y,
+                 rtl: this._anchorIsRTL,
                  hidden: checkHidden(anchorX, anchorY) },
                { handle: this.HANDLE_TYPE_FOCUS,
                  left: focusX + offset.x + scroll.X,
                  top: focusY + offset.y + scroll.Y,
+                 rtl: this._focusIsRTL,
                  hidden: checkHidden(focusX, focusY) }];
     }
   },
 
   // Position handles, but avoid superfluous re-positioning (helps during
   // "TextSelection:LayerReflow", "scroll" of top-level document, etc).
   _positionHandlesOnChange: function() {
     // Helper function to compare position messages
     let samePositions = function(aPrev, aCurr) {
       if (aPrev.length != aCurr.length) {
         return false;
       }
       for (let i = 0; i < aPrev.length; i++) {
         if (aPrev[i].left != aCurr[i].left ||
             aPrev[i].top != aCurr[i].top ||
+            aPrev[i].rtl != aCurr[i].rtl ||
             aPrev[i].hidden != aCurr[i].hidden) {
           return false;
         }
       }
       return true;
     }
 
     let positions = this._getHandlePositions(this._getScrollPos());
@@ -1266,17 +1298,16 @@ var SelectionHandler = {
   // which we get from _getHandlePositions.
   _positionHandles: function sh_positionHandles(positions) {
     if (!positions) {
       positions = this._getHandlePositions(this._getScrollPos());
     }
     Messaging.sendRequest({
       type: "TextSelection:PositionHandles",
       positions: positions,
-      rtl: this._isRTL
     });
     this._prevHandlePositions = positions;
 
     // Text state transitions (text <--> no text) will affect selection context and actionbar display
     let currTargetElementHasText = (this._targetElement.textLength > 0);
     if (currTargetElementHasText != this._prevTargetElementHasText) {
       this._prevTargetElementHasText = currTargetElementHasText;
       this._updateMenu();