Bug 795045 - Part 3: Lock handle to caret position and add scrolling support. r=wesj
authorBrian Nicholson <bnicholson@mozilla.com>
Tue, 18 Dec 2012 22:58:10 -0800
changeset 125612 7f3b0fc0e464
parent 125611 9af5a57f905a
child 125613 b757786d6828
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswesj
bugs795045
milestone20.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 795045 - Part 3: Lock handle to caret position and add scrolling support. r=wesj
mobile/android/base/TextSelectionHandle.java
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/TextSelectionHandle.java
+++ b/mobile/android/base/TextSelectionHandle.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
 
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 
 class TextSelectionHandle extends ImageView implements View.OnTouchListener {
@@ -30,16 +31,18 @@ class TextSelectionHandle extends ImageV
     private final int mShadow;
 
     private float mLeft;
     private float mTop;
     private boolean mIsRTL; 
     private PointF mGeckoPoint;
     private float mTouchStartX;
     private float mTouchStartY;
+    private int mLayerViewX;
+    private int mLayerViewY;
 
     private RelativeLayout.LayoutParams mLayoutParams;
 
     private static final int IMAGE_LEVEL_LTR = 0;
     private static final int IMAGE_LEVEL_RTL = 1;
 
     private GeckoApp mActivity;
 
@@ -66,16 +69,21 @@ class TextSelectionHandle extends ImageV
         mShadow = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_shadow);
     }
 
     public boolean onTouch(View v, MotionEvent event) {
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN: {
                 mTouchStartX = event.getX();
                 mTouchStartY = event.getY();
+
+                int[] rect = new int[2];
+                mActivity.getLayerView().getLocationOnScreen(rect);
+                mLayerViewX = rect[0];
+                mLayerViewY = rect[1];
                 break;
             }
             case MotionEvent.ACTION_UP: {
                 mTouchStartX = 0;
                 mTouchStartY = 0;
 
                 // Reposition handles to line up with ends of selection
                 JSONObject args = new JSONObject();
@@ -83,26 +91,30 @@ class TextSelectionHandle extends ImageV
                     args.put("handleType", mHandleType.toString());
                 } catch (Exception e) {
                     Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Position");
                 }
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Position", args.toString()));
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
-                move(event.getX(), event.getY());
+                move(event.getRawX(), event.getRawY());
                 break;
             }
         }
         return true;
     }
 
     private void move(float newX, float newY) {
-        mLeft = mLeft + newX - mTouchStartX;
-        mTop = mTop + newY - mTouchStartY;
+        // newX and newY are absolute coordinates, so we need to adjust them to
+        // account for other views on the screen (such as the URL bar). We also
+        // need to include the offset amount of the touch location relative to
+        // the top left of the handle (mTouchStartX and mTouchStartY).
+        mLeft = newX - mLayerViewX - mTouchStartX;
+        mTop = newY - mLayerViewY - mTouchStartY;
 
         LayerView layerView = mActivity.getLayerView();
         if (layerView == null) {
             Log.e(LOGTAG, "Can't move selection because layerView is null");
             return;
         }
         // Send x coordinate on the right side of the start handle, left side of the end handle.
         float left = mLeft + adjustLeftForHandle();
@@ -115,17 +127,23 @@ class TextSelectionHandle extends ImageV
             args.put("handleType", mHandleType.toString());
             args.put("x", (int) geckoPoint.x);
             args.put("y", (int) geckoPoint.y);
         } catch (Exception e) {
             Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Move");
         }
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Move", args.toString()));
 
-        setLayoutPosition();
+        // If we're positioning a cursor, don't move the handle here. Gecko
+        // will tell us the position of the caret, so we set the handle
+        // position then. This allows us to lock the handle to wherever the
+        // caret appears.
+        if (!mHandleType.equals(HandleType.MIDDLE)) {
+            setLayoutPosition();
+        }
     }
 
     void positionFromGecko(int left, int top, boolean rtl) {
         LayerView layerView = mActivity.getLayerView();
         if (layerView == null) {
             Log.e(LOGTAG, "Can't position handle because layerView is null");
             return;
         }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1699,35 +1699,37 @@ var SelectionHandler = {
         break;
       }
       case "Tab:Selected":
       case "Window:Resize": {
         if (this._activeType == this.TYPE_SELECTION) {
           // Knowing when the page is done drawing is hard, so let's just cancel
           // the selection when the window changes. We should fix this later.
           this.endSelection();
-        } else if (this._activeType == this.TYPE_CURSOR) {
-          //  Hide the cursor if the window changes
-          this.hideThumb();
         }
         break;
       }
       case "after-viewport-change": {
         if (this._activeType == this.TYPE_SELECTION) {
           // Update the cache after the viewport changes (e.g. panning, zooming).
           this.updateCacheForSelection();
         }
         break;
       }
       case "TextSelection:Move": {
         let data = JSON.parse(aData);
         if (this._activeType == this.TYPE_SELECTION)
           this.moveSelection(data.handleType == this.HANDLE_TYPE_START, data.x, data.y);
-        else if (this._activeType == this.TYPE_CURSOR)
+        else if (this._activeType == this.TYPE_CURSOR) {
+          // 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();
+        }
         break;
       }
       case "TextSelection:Position": {
         if (this._activeType == this.TYPE_SELECTION) {
           let data = JSON.parse(aData);
 
           // Reverse the handles if necessary.
           let selectionReversed = this.updateCacheForSelection(data.handleType == this.HANDLE_TYPE_START);
@@ -1931,19 +1933,59 @@ var SelectionHandler = {
         this._sendMouseEvents(this.cache.start.x, this.cache.start.y, false);
 
       // Selects text between the carat and the end handle using a fake shift+click
       this._sendMouseEvents( this.cache.end.x, this.cache.end.y, true);
     }
   },
 
   _sendMouseEvents: function sh_sendMouseEvents(aX, aY, useShift) {
-    // Send mouse event 1px too high to prevent selection from entering the line below where it should be
-    this._cwu.sendMouseEventToWindow("mousedown", aX, aY - 1, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
-    this._cwu.sendMouseEventToWindow("mouseup", aX, aY - 1, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
+    // If we're positioning a cursor in an input field, make sure the handle
+    // stays within the bounds of the field
+    if (this._activeType == this.TYPE_CURSOR) {
+      // Get rect of text inside element
+      let range = document.createRange();
+      range.selectNodeContents(this._target.QueryInterface(Ci.nsIDOMNSEditableElement).editor.rootElement);
+      let textBounds = range.getBoundingClientRect();
+
+      // Get rect of editor
+      let editorBounds = this._cwu.sendQueryContentEvent(this._cwu.QUERY_EDITOR_RECT, 0, 0, 0, 0);
+      let editorRect = new Rect(editorBounds.left, editorBounds.top, editorBounds.width, editorBounds.height);
+
+      // Use intersection of the text rect and the editor rect
+      let rect = new Rect(textBounds.left, textBounds.top, textBounds.width, textBounds.height);
+      rect.restrictTo(editorRect);
+
+      // Clamp vertically and scroll if handle is at bounds. The top and bottom
+      // must be restricted by an additional pixel since clicking on the top
+      // edge of an input field moves the cursor to the beginning of that
+      // field's text (and clicking the bottom moves the cursor to the end).
+      if (aY < rect.y + 1) {
+        aY = rect.y + 1;
+        this.getSelectionController().scrollLine(false);
+      } else if (aY > rect.y + rect.height - 1) {
+        aY = rect.y + rect.height - 1;
+        this.getSelectionController().scrollLine(true);
+      }
+
+      // Clamp horizontally and scroll if handle is at bounds
+      if (aX < rect.x) {
+        aX = rect.x;
+        this.getSelectionController().scrollCharacter(false);
+      } else if (aX > rect.x + rect.width) {
+        aX = rect.x + rect.width;
+        this.getSelectionController().scrollCharacter(true);
+      }
+    } else if (this._activeType == this.TYPE_SELECTION) {
+      // Send mouse event 1px too high to prevent selection from entering the line below where it should be
+      aY -= 1;
+    }
+
+    this._cwu.sendMouseEventToWindow("mousedown", aX, aY, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
+    this._cwu.sendMouseEventToWindow("mouseup", aX, aY, 0, 0, useShift ? Ci.nsIDOMNSEvent.SHIFT_MASK : 0, true);
   },
 
   // aX/aY are in top-level window browser coordinates
   endSelection: function sh_endSelection(aX, aY) {
     if (this._activeType != this.TYPE_SELECTION)
       return "";
  
     this._activeType = this.TYPE_NONE;