Bug 895463 - (p4) Reposition SelectionHandler on layer reflow, r=kats, margaret
authorMark Capella <markcapella@twcny.rr.com>
Sat, 26 Apr 2014 12:46:41 -0400
changeset 180815 9afd168b50f70c03d20366b87debf5b0a934929c
parent 180814 ed6d2e76df4403ec5238a7eb19240bc0ab819bc2
child 180816 2ddc109368cc38da259789deb3d078d9aa7f2323
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewerskats, margaret
bugs895463
milestone31.0a1
Bug 895463 - (p4) Reposition SelectionHandler on layer reflow, r=kats, margaret
mobile/android/base/TextSelection.java
mobile/android/chrome/content/SelectionHandler.js
--- a/mobile/android/base/TextSelection.java
+++ b/mobile/android/base/TextSelection.java
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.gfx.LayerView.DrawListener;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.ActionModeCompat.Callback;
 
@@ -35,16 +36,19 @@ import android.view.View;
 class TextSelection extends Layer implements GeckoEventListener {
     private static final String LOGTAG = "GeckoTextSelection";
 
     private final TextSelectionHandle mStartHandle;
     private final TextSelectionHandle mMiddleHandle;
     private final TextSelectionHandle mEndHandle;
     private final EventDispatcher mEventDispatcher;
 
+    private final DrawListener mDrawListener;
+    private boolean mDraggingHandles;
+
     private float mViewLeft;
     private float mViewTop;
     private float mViewZoom;
 
     private String mCurrentItems;
 
     private TextSelectionActionModeCallback mCallback;
 
@@ -69,74 +73,95 @@ class TextSelection extends Layer implem
                   TextSelectionHandle endHandle,
                   EventDispatcher eventDispatcher,
                   GeckoApp activity) {
         mStartHandle = startHandle;
         mMiddleHandle = middleHandle;
         mEndHandle = endHandle;
         mEventDispatcher = eventDispatcher;
 
+        mDrawListener = new DrawListener() {
+            @Override
+            public void drawFinished() {
+                if (!mDraggingHandles) {
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:LayerReflow", ""));
+                }
+            }
+        };
+
         // Only register listeners if we have valid start/middle/end handles
         if (mStartHandle == null || mMiddleHandle == null || mEndHandle == null) {
             Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
         } else {
             registerEventListener("TextSelection:ShowHandles");
             registerEventListener("TextSelection:HideHandles");
             registerEventListener("TextSelection:PositionHandles");
             registerEventListener("TextSelection:Update");
+            registerEventListener("TextSelection:DraggingHandle");
         }
     }
 
     void destroy() {
         unregisterEventListener("TextSelection:ShowHandles");
         unregisterEventListener("TextSelection:HideHandles");
         unregisterEventListener("TextSelection:PositionHandles");
         unregisterEventListener("TextSelection:Update");
+        unregisterEventListener("TextSelection:DraggingHandle");
     }
 
     private TextSelectionHandle getHandle(String name) {
         if (name.equals("START")) {
             return mStartHandle;
         } else if (name.equals("MIDDLE")) {
             return mMiddleHandle;
         } else {
             return mEndHandle;
         }
     }
 
     @Override
     public void handleMessage(final String event, final JSONObject message) {
+        if ("TextSelection:DraggingHandle".equals(event)) {
+            mDraggingHandles = message.optBoolean("dragging", false);
+            return;
+        }
+
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 try {
                     if (event.equals("TextSelection:ShowHandles")) {
                         final JSONArray handles = message.getJSONArray("handles");
                         for (int i=0; i < handles.length(); i++) {
                             String handle = handles.getString(i);
                             getHandle(handle).setVisibility(View.VISIBLE);
                         }
 
                         mViewLeft = 0.0f;
                         mViewTop = 0.0f;
                         mViewZoom = 0.0f;
+
+                        // Create text selection layer and add draw-listener for positioning on reflows
                         LayerView layerView = GeckoAppShell.getLayerView();
                         if (layerView != null) {
+                            layerView.addDrawListener(mDrawListener);
                             layerView.addLayer(TextSelection.this);
                         }
 
                         if (handles.length() > 1)
                             GeckoAppShell.performHapticFeedback(true);
                     } else if (event.equals("TextSelection:Update")) {
                         if (mActionModeTimerTask != null)
                             mActionModeTimerTask.cancel();
                         showActionMode(message.getJSONArray("actions"));
                     } else if (event.equals("TextSelection:HideHandles")) {
+                        // Remove draw-listener and text selection layer
                         LayerView layerView = GeckoAppShell.getLayerView();
                         if (layerView != null) {
+                            layerView.removeDrawListener(mDrawListener);
                             layerView.removeLayer(TextSelection.this);
                         }
 
                         mActionModeTimerTask = new ActionModeTimerTask();
                         mActionModeTimer.schedule(mActionModeTimerTask, 250);
 
                         mStartHandle.setVisibility(View.GONE);
                         mMiddleHandle.setVisibility(View.GONE);
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -17,16 +17,17 @@ var SelectionHandler = {
   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,
   _activeType: 0, // TYPE_NONE
   _draggingHandles: false, // True while user drags text selection handles
   _ignoreCompositionChanges: false, // Persist caret during IME composition updates
+  _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
 
   // TargetElement changes (text <--> no text) trigger actionbar UI update
   _prevTargetElementHasText: null,
 
   // The window that holds the selection (can be a sub-frame)
   get _contentWindow() {
     if (this._contentWindowRef)
       return this._contentWindowRef.get();
@@ -57,38 +58,52 @@ var SelectionHandler = {
   _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);
+    Services.obs.addObserver(this, "TextSelection:LayerReflow", false);
 
     BrowserApp.deck.addEventListener("pagehide", this, false);
     BrowserApp.deck.addEventListener("blur", this, true);
     BrowserApp.deck.addEventListener("scroll", this, true);
   },
 
   _removeObservers: function sh_removeObservers() {
     Services.obs.removeObserver(this, "Gesture:SingleTap");
     Services.obs.removeObserver(this, "Tab:Selected");
     Services.obs.removeObserver(this, "after-viewport-change");
     Services.obs.removeObserver(this, "TextSelection:Move");
     Services.obs.removeObserver(this, "TextSelection:Position");
     Services.obs.removeObserver(this, "TextSelection:End");
     Services.obs.removeObserver(this, "TextSelection:Action");
+    Services.obs.removeObserver(this, "TextSelection:LayerReflow");
 
     BrowserApp.deck.removeEventListener("pagehide", this, false);
     BrowserApp.deck.removeEventListener("blur", this, true);
     BrowserApp.deck.removeEventListener("scroll", this, true);
   },
 
   observe: function sh_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
+      // Update handle/caret position on page reflow (keyboard open/close,
+      // dynamic DOM changes, orientation updates, etc).
+      case "TextSelection:LayerReflow": {
+        if (this._activeType == this.TYPE_SELECTION) {
+          this._updateCacheForSelection();
+        }
+        if (this._activeType != this.TYPE_NONE) {
+          this._positionHandlesOnChange();
+        }
+        break;
+      }
+
       // Update caret position on keyboard activity
       case "TextSelection:UpdateCaretPos":
         // Generated by IME close, autoCorrection / styling
         this._positionHandles();
         break;
 
       case "Gesture:SingleTap": {
         if (this._activeType == this.TYPE_SELECTION) {
@@ -123,16 +138,18 @@ var SelectionHandler = {
       }
       case "TextSelection:Move": {
         let data = JSON.parse(aData);
         if (this._activeType == this.TYPE_SELECTION) {
           this._startDraggingHandles();
           this._moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y);
 
         } else if (this._activeType == this.TYPE_CURSOR) {
+          this._startDraggingHandles();
+
           // Ignore IMM composition notifications when caret movement starts
           this._ignoreCompositionChanges = true;
 
           // Send a click event to the text box, which positions the caret
           this._sendMouseEvents(data.x, data.y);
 
           // Move the handle directly under the caret
           this._positionHandles();
@@ -164,16 +181,17 @@ var SelectionHandler = {
           this._stopDraggingHandles();
           this._positionHandles();
           // Changes to handle position can affect selection context and actionbar display
           this._updateMenu();
 
         } else if (this._activeType == this.TYPE_CURSOR) {
           // Act on IMM composition notifications after caret movement ends
           this._ignoreCompositionChanges = false;
+          this._stopDraggingHandles();
           this._positionHandles();
 
         } else {
           Cu.reportError("Ignored \"TextSelection:Position\" message during invalid selection status");
         }
 
         break;
       }
@@ -205,17 +223,17 @@ var SelectionHandler = {
       sendMessageToJava({ type: "TextSelection:DraggingHandle", dragging: false });
     }
   },
 
   handleEvent: function sh_handleEvent(aEvent) {
     switch (aEvent.type) {
       case "scroll":
         // Maintain position when top-level document is scrolled
-        this._positionHandles();
+        this._positionHandlesOnChange();
         break;
 
       case "pagehide":
       case "blur":
         this._closeSelection();
         break;
 
       // Update caret position on keyboard activity
@@ -937,16 +955,17 @@ var SelectionHandler = {
       BrowserApp.deck.removeEventListener("compositionend", this);
     }
 
     this._contentWindow = null;
     this._targetElement = null;
     this._isRTL = false;
     this._cache = null;
     this._ignoreCompositionChanges = false;
+    this._prevHandlePositions = [];
     this._prevTargetElementHasText = null;
 
     this._activeType = this.TYPE_NONE;
   },
 
   _getViewOffset: function sh_getViewOffset() {
     let offset = { x: 0, y: 0 };
     let win = this._contentWindow;
@@ -1042,27 +1061,54 @@ var SelectionHandler = {
                  hidden: checkHidden(sx, sy) },
                { handle: this.HANDLE_TYPE_END,
                  left: ex + offset.x + scroll.X,
                  top: ey + offset.y + scroll.Y,
                  hidden: checkHidden(ex, ey) }];
     }
   },
 
+  // 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].hidden != aCurr[i].hidden) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    let positions = this._getHandlePositions(this._getScrollPos());
+    if (!samePositions(this._prevHandlePositions, positions)) {
+      this._positionHandles(positions);
+    }
+  },
+
+  // Position handles, allow for re-position, in case user drags handle
+  // to invalid position, then releases, we can put it back where it started
   // positions is an array of objects with data about handle positions,
   // which we get from _getHandlePositions.
   _positionHandles: function sh_positionHandles(positions) {
     if (!positions) {
       positions = this._getHandlePositions(this._getScrollPos());
     }
     sendMessageToJava({
       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();
     }
   },