Snap selection handles to text

 Bug 6198286
 Webkit Change: I55604f3ec28586beaec5fd98a27c9f4b8a59a8a5

Change-Id: I3c51ed5f6988d58440badfbe8b076fd83d48ae2a
diff --git a/core/java/android/webkit/QuadF.java b/core/java/android/webkit/QuadF.java
new file mode 100644
index 0000000..e9011e3
--- /dev/null
+++ b/core/java/android/webkit/QuadF.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.webkit;
+
+import android.graphics.PointF;
+
+/**
+ * A quadrilateral, determined by four points, clockwise order. Typically
+ * p1 is "top-left" and p4 is "bottom-left" following webkit's rectangle-to-
+ * FloatQuad conversion.
+ */
+class QuadF {
+    public PointF p1;
+    public PointF p2;
+    public PointF p3;
+    public PointF p4;
+
+    public QuadF() {
+        p1 = new PointF();
+        p2 = new PointF();
+        p3 = new PointF();
+        p4 = new PointF();
+    }
+
+    public void offset(float dx, float dy) {
+        p1.offset(dx, dy);
+        p2.offset(dx, dy);
+        p3.offset(dx, dy);
+        p4.offset(dx, dy);
+    }
+
+    /**
+     * Determines if the quadrilateral contains the given point. This does
+     * not work if the quadrilateral is self-intersecting or if any inner
+     * angle is reflex (greater than 180 degrees).
+     */
+    public boolean containsPoint(float x, float y) {
+        return isPointInTriangle(x, y, p1, p2, p3) ||
+                isPointInTriangle(x, y, p1, p3, p4);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder s = new StringBuilder("QuadF(");
+        s.append(p1.x).append(",").append(p1.y);
+        s.append(" - ");
+        s.append(p2.x).append(",").append(p2.y);
+        s.append(" - ");
+        s.append(p3.x).append(",").append(p3.y);
+        s.append(" - ");
+        s.append(p4.x).append(",").append(p4.y);
+        s.append(")");
+        return s.toString();
+    }
+
+    private static boolean isPointInTriangle(float x0, float y0,
+            PointF r1, PointF r2, PointF r3) {
+        // Use the barycentric technique
+        float x13 = r1.x - r3.x;
+        float y13 = r1.y - r3.y;
+        float x23 = r2.x - r3.x;
+        float y23 = r2.y - r3.y;
+        float x03 = x0 - r3.x;
+        float y03 = y0 - r3.y;
+
+        float determinant = (y23 * x13) - (x23 * y13);
+        float lambda1 = ((y23 * x03) - (x23 * y03))/determinant;
+        float lambda2 = ((x13 * y03) - (y13 * x03))/determinant;
+        float lambda3 = 1 - lambda1 - lambda2;
+        return lambda1 >= 0.0f && lambda2 >= 0.0f && lambda3 >= 0.0f;
+    }
+}
diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java
index 5ae2fe0..7ddff8e 100644
--- a/core/java/android/webkit/WebViewClassic.java
+++ b/core/java/android/webkit/WebViewClassic.java
@@ -43,6 +43,7 @@
 import android.graphics.PaintFlagsDrawFilter;
 import android.graphics.Picture;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Region;
@@ -56,9 +57,7 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.Message;
-import android.os.StrictMode;
 import android.os.SystemClock;
 import android.provider.Settings;
 import android.security.KeyChain;
@@ -758,22 +757,21 @@
             this.setContentView(mContentView);
         }
 
-        public void show(Rect cursorRect, int windowLeft, int windowTop) {
+        public void show(Point cursorBottom, Point cursorTop,
+                int windowLeft, int windowTop) {
             measureContent();
 
             int width = mContentView.getMeasuredWidth();
             int height = mContentView.getMeasuredHeight();
-            int y = cursorRect.top - height;
+            int y = cursorTop.y - height;
+            int x = cursorTop.x - (width / 2);
             if (y < windowTop) {
                 // There's not enough room vertically, move it below the
                 // handle.
-                // The selection handle is vertically offset by 1/4 of the
-                // line height.
                 ensureSelectionHandles();
-                y = cursorRect.bottom - (cursorRect.height() / 4) +
-                        mSelectHandleCenter.getIntrinsicHeight();
+                y = cursorBottom.y + mSelectHandleCenter.getIntrinsicHeight();
+                x = cursorBottom.x - (width / 2);
             }
-            int x = cursorRect.centerX() - (width / 2);
             if (x < windowLeft) {
                 x = windowLeft;
             }
@@ -1151,12 +1149,18 @@
     private Drawable mSelectHandleLeft;
     private Drawable mSelectHandleRight;
     private Drawable mSelectHandleCenter;
-    private Rect mSelectCursorBase = new Rect();
+    private Point mSelectHandleLeftOffset;
+    private Point mSelectHandleRightOffset;
+    private Point mSelectHandleCenterOffset;
+    private Point mSelectCursorBase = new Point();
     private int mSelectCursorBaseLayerId;
-    private Rect mSelectCursorExtent = new Rect();
+    private QuadF mSelectCursorBaseTextQuad = new QuadF();
+    private Point mSelectCursorExtent = new Point();
     private int mSelectCursorExtentLayerId;
-    private Rect mSelectDraggingCursor;
-    private Point mSelectDraggingOffset = new Point();
+    private QuadF mSelectCursorExtentTextQuad = new QuadF();
+    private Point mSelectDraggingCursor;
+    private Point mSelectDraggingOffset;
+    private QuadF mSelectDraggingTextQuad;
     private boolean mIsCaretSelection;
     static final int HANDLE_ID_START = 0;
     static final int HANDLE_ID_END = 1;
@@ -3894,9 +3898,11 @@
         if (mSelectingText) {
             if (mSelectCursorBaseLayerId == mCurrentScrollingLayerId) {
                 mSelectCursorBase.offset(dx, dy);
+                mSelectCursorBaseTextQuad.offset(dx, dy);
             }
             if (mSelectCursorExtentLayerId == mCurrentScrollingLayerId) {
                 mSelectCursorExtent.offset(dx, dy);
+                mSelectCursorExtentTextQuad.offset(dx, dy);
             }
         }
         if (mAutoCompletePopup != null &&
@@ -4774,6 +4780,13 @@
                     com.android.internal.R.drawable.text_select_handle_left);
             mSelectHandleRight = mContext.getResources().getDrawable(
                     com.android.internal.R.drawable.text_select_handle_right);
+            mSelectHandleCenterOffset = new Point(0,
+                    -mSelectHandleCenter.getIntrinsicHeight());
+            mSelectHandleLeftOffset = new Point(0,
+                    -mSelectHandleLeft.getIntrinsicHeight());
+            mSelectHandleRightOffset = new Point(
+                    -mSelectHandleLeft.getIntrinsicWidth() / 2,
+                    -mSelectHandleRight.getIntrinsicHeight());
         }
     }
 
@@ -4813,10 +4826,10 @@
      * startX, startY, endX, endY
      */
     private void getSelectionHandles(int[] handles) {
-        handles[0] = mSelectCursorBase.left;
-        handles[1] = mSelectCursorBase.bottom;
-        handles[2] = mSelectCursorExtent.left;
-        handles[3] = mSelectCursorExtent.bottom;
+        handles[0] = mSelectCursorBase.x;
+        handles[1] = mSelectCursorBase.y;
+        handles[2] = mSelectCursorExtent.x;
+        handles[3] = mSelectCursorExtent.y;
         if (!nativeIsBaseFirst(mNativeClass)) {
             int swap = handles[0];
             handles[0] = handles[2];
@@ -5332,17 +5345,66 @@
         ClipboardManager cm = (ClipboardManager)(mContext
                 .getSystemService(Context.CLIPBOARD_SERVICE));
         if (cm.hasPrimaryClip()) {
-            Rect cursorRect = contentToViewRect(mSelectCursorBase);
+            Point cursorPoint = new Point(contentToViewX(mSelectCursorBase.x),
+                    contentToViewY(mSelectCursorBase.y));
+            Point cursorTop = calculateCaretTop();
+            cursorTop.set(contentToViewX(cursorTop.x),
+                    contentToViewY(cursorTop.y));
+
             int[] location = new int[2];
             mWebView.getLocationInWindow(location);
-            cursorRect.offset(location[0] - getScrollX(), location[1] - getScrollY());
+            int offsetX = location[0] - getScrollX();
+            int offsetY = location[1] - getScrollY();
+            cursorPoint.offset(offsetX, offsetY);
+            cursorTop.offset(offsetX, offsetY);
             if (mPasteWindow == null) {
                 mPasteWindow = new PastePopupWindow();
             }
-            mPasteWindow.show(cursorRect, location[0], location[1]);
+            mPasteWindow.show(cursorPoint, cursorTop, location[0], location[1]);
         }
     }
 
+    /**
+     * Given segment AB, this finds the point C along AB that is closest to
+     * point and then returns it scale along AB. The scale factor is AC/AB.
+     *
+     * @param x The x coordinate of the point near segment AB that determines
+     * the scale factor.
+     * @param y The y coordinate of the point near segment AB that determines
+     * the scale factor.
+     * @param a The first point of the line segment.
+     * @param b The second point of the line segment.
+     * @return The scale factor AC/AB, where C is the point on AB closest to
+     *         point.
+     */
+    private static float scaleAlongSegment(int x, int y, PointF a, PointF b) {
+        // The bottom line of the text box is line AB
+        float abX = b.x - a.x;
+        float abY = b.y - a.y;
+        float ab2 = (abX * abX) + (abY * abY);
+
+        // The line from first point in text bounds to bottom is AP
+        float apX = x - a.x;
+        float apY = y - a.y;
+        float abDotAP = (apX * abX) + (apY * abY);
+        float scale = abDotAP / ab2;
+        return scale;
+    }
+
+    /**
+     * Assuming arbitrary shape of a quadralateral forming text bounds, this
+     * calculates the top of a caret.
+     */
+    private Point calculateCaretTop() {
+        float scale = scaleAlongSegment(mSelectCursorBase.x, mSelectCursorBase.y,
+                mSelectCursorBaseTextQuad.p4, mSelectCursorBaseTextQuad.p3);
+        int x = Math.round(scaleCoordinate(scale,
+                mSelectCursorBaseTextQuad.p1.x, mSelectCursorBaseTextQuad.p2.x));
+        int y = Math.round(scaleCoordinate(scale,
+                mSelectCursorBaseTextQuad.p1.y, mSelectCursorBaseTextQuad.p2.y));
+        return new Point(x, y);
+    }
+
     private void hidePasteButton() {
         if (mPasteWindow != null) {
             mPasteWindow.hide();
@@ -5351,9 +5413,54 @@
 
     private void syncSelectionCursors() {
         mSelectCursorBaseLayerId =
-                nativeGetHandleLayerId(mNativeClass, HANDLE_ID_BASE, mSelectCursorBase);
+                nativeGetHandleLayerId(mNativeClass, HANDLE_ID_BASE,
+                        mSelectCursorBase, mSelectCursorBaseTextQuad);
         mSelectCursorExtentLayerId =
-                nativeGetHandleLayerId(mNativeClass, HANDLE_ID_EXTENT, mSelectCursorExtent);
+                nativeGetHandleLayerId(mNativeClass, HANDLE_ID_EXTENT,
+                        mSelectCursorExtent, mSelectCursorExtentTextQuad);
+    }
+
+    private void adjustSelectionCursors() {
+        boolean wasDraggingStart = (mSelectDraggingCursor == mSelectCursorBase);
+        int oldX = mSelectDraggingCursor.x;
+        int oldY = mSelectDraggingCursor.y;
+        int oldStartX = mSelectCursorBase.x;
+        int oldStartY = mSelectCursorBase.y;
+        int oldEndX = mSelectCursorExtent.x;
+        int oldEndY = mSelectCursorExtent.y;
+
+        syncSelectionCursors();
+        boolean dragChanged = oldX != mSelectDraggingCursor.x ||
+                oldY != mSelectDraggingCursor.y;
+        if (dragChanged && !mIsCaretSelection) {
+            boolean draggingStart;
+            if (wasDraggingStart) {
+                float endStart = distanceSquared(oldEndX, oldEndY,
+                        mSelectCursorBase);
+                float endEnd = distanceSquared(oldEndX, oldEndY,
+                        mSelectCursorExtent);
+                draggingStart = endStart > endEnd;
+            } else {
+                float startStart = distanceSquared(oldStartX, oldStartY,
+                        mSelectCursorBase);
+                float startEnd = distanceSquared(oldStartX, oldStartY,
+                        mSelectCursorExtent);
+                draggingStart = startStart > startEnd;
+            }
+            mSelectDraggingCursor = (draggingStart
+                    ? mSelectCursorBase : mSelectCursorExtent);
+            mSelectDraggingTextQuad = (draggingStart
+                    ? mSelectCursorBaseTextQuad : mSelectCursorExtentTextQuad);
+            mSelectDraggingOffset = (draggingStart
+                    ? mSelectHandleLeftOffset : mSelectHandleRightOffset);
+        }
+        mSelectDraggingCursor.set(oldX, oldY);
+    }
+
+    private float distanceSquared(int x, int y, Point p) {
+        float dx = p.x - x;
+        float dy = p.y - y;
+        return (dx * dx) + (dy * dy);
     }
 
     private boolean setupWebkitSelect() {
@@ -5370,14 +5477,14 @@
     private void updateWebkitSelection() {
         int[] handles = null;
         if (mIsCaretSelection) {
-            mSelectCursorExtent.set(mSelectCursorBase);
+            mSelectCursorExtent.set(mSelectCursorBase.x, mSelectCursorBase.y);
         }
         if (mSelectingText) {
             handles = new int[4];
-            handles[0] = mSelectCursorBase.centerX();
-            handles[1] = mSelectCursorBase.centerY();
-            handles[2] = mSelectCursorExtent.centerX();
-            handles[3] = mSelectCursorExtent.centerY();
+            handles[0] = mSelectCursorBase.x;
+            handles[1] = mSelectCursorBase.y;
+            handles[2] = mSelectCursorExtent.x;
+            handles[3] = mSelectCursorExtent.y;
         } else {
             nativeSetTextSelection(mNativeClass, 0);
         }
@@ -5968,12 +6075,15 @@
                     }
                     mSelectionStarted = false;
                     if (mSelectingText) {
+                        ensureSelectionHandles();
                         int shiftedY = y - getTitleHeight() + getScrollY();
                         int shiftedX = x + getScrollX();
                         if (mSelectHandleCenter != null && mSelectHandleCenter.getBounds()
                                 .contains(shiftedX, shiftedY)) {
                             mSelectionStarted = true;
                             mSelectDraggingCursor = mSelectCursorBase;
+                            mSelectDraggingOffset = mSelectHandleCenterOffset;
+                            mSelectDraggingTextQuad = mSelectCursorBaseTextQuad;
                             mPrivateHandler.removeMessages(CLEAR_CARET_HANDLE);
                             hidePasteButton();
                         } else if (mSelectHandleLeft != null
@@ -5981,19 +6091,18 @@
                                     .contains(shiftedX, shiftedY)) {
                                 mSelectionStarted = true;
                                 mSelectDraggingCursor = mSelectCursorBase;
+                                mSelectDraggingOffset = mSelectHandleLeftOffset;
+                                mSelectDraggingTextQuad = mSelectCursorBaseTextQuad;
                         } else if (mSelectHandleRight != null
                                 && mSelectHandleRight.getBounds()
                                 .contains(shiftedX, shiftedY)) {
                             mSelectionStarted = true;
                             mSelectDraggingCursor = mSelectCursorExtent;
+                            mSelectDraggingOffset = mSelectHandleRightOffset;
+                            mSelectDraggingTextQuad = mSelectCursorExtentTextQuad;
                         } else if (mIsCaretSelection) {
                             selectionDone();
                         }
-                        if (mSelectDraggingCursor != null) {
-                            mSelectDraggingOffset.set(
-                                    mSelectDraggingCursor.left - contentX,
-                                    mSelectDraggingCursor.top - contentY);
-                        }
                         if (DebugFlags.WEB_VIEW) {
                             Log.v(LOGTAG, "select=" + contentX + "," + contentY);
                         }
@@ -6073,9 +6182,7 @@
                         parent.requestDisallowInterceptTouchEvent(true);
                     }
                     if (deltaX != 0 || deltaY != 0) {
-                        mSelectDraggingCursor.offsetTo(
-                                contentX + mSelectDraggingOffset.x,
-                                contentY + mSelectDraggingOffset.y);
+                        snapDraggingCursor(contentX, contentY);
                         updateWebkitSelection();
                         mLastTouchX = x;
                         mLastTouchY = y;
@@ -6684,6 +6791,30 @@
         mTouchMode = TOUCH_DONE_MODE;
     }
 
+    private void snapDraggingCursor(int x, int y) {
+        x += viewToContentDimension(mSelectDraggingOffset.x);
+        y += viewToContentDimension(mSelectDraggingOffset.y);
+        if (mSelectDraggingTextQuad.containsPoint(x, y)) {
+            float scale = scaleAlongSegment(x, y,
+                    mSelectDraggingTextQuad.p4, mSelectDraggingTextQuad.p3);
+            // clamp scale to ensure point is on the bottom segment
+            scale = Math.max(0.0f, scale);
+            scale = Math.min(scale, 1.0f);
+            float newX = scaleCoordinate(scale,
+                    mSelectDraggingTextQuad.p4.x, mSelectDraggingTextQuad.p3.x);
+            float newY = scaleCoordinate(scale,
+                    mSelectDraggingTextQuad.p4.y, mSelectDraggingTextQuad.p3.y);
+            mSelectDraggingCursor.set(Math.round(newX), Math.round(newY));
+        } else {
+            mSelectDraggingCursor.set(x, y);
+        }
+    }
+
+    private static float scaleCoordinate(float scale, float coord1, float coord2) {
+        float diff = coord2 - coord1;
+        return coord1 + (scale * diff);
+    }
+
     @Override
     public boolean onGenericMotionEvent(MotionEvent event) {
         if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
@@ -8735,6 +8866,8 @@
                     setupWebkitSelect();
                 } else if (!mSelectionStarted) {
                     syncSelectionCursors();
+                } else {
+                    adjustSelectionCursors();
                 }
                 if (mIsCaretSelection) {
                     resetCaretTimer();
@@ -9398,7 +9531,7 @@
     private static native void nativeSetPauseDrawing(int instance, boolean pause);
     private static native void nativeSetTextSelection(int instance, int selection);
     private static native int nativeGetHandleLayerId(int instance, int handle,
-            Rect cursorLocation);
+            Point cursorLocation, QuadF textQuad);
     private static native boolean nativeIsBaseFirst(int instance);
     private static native void nativeMapLayerRect(int instance, int layerId,
             Rect rect);