Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 97af673e authored by Keisuke Kuroyanagi's avatar Keisuke Kuroyanagi
Browse files

Move mouse selection handling logic to Editor.

- Remove mouse selection related logic from
ArrowKeyMovementMethod and Touch.
- Extend drag accelerator to support character based
selection by dragging and use it for mouse drag selection.
- Enable word based drag accelerator even if the clicked
position is not on a word.
- Disable y-coordinate adjustment for word based drag
accelerator when the user is using a mouse.

Bug: 19544351

Change-Id: I1b29a65be3d321ac472f8c5b70ab6fee4e895964
parent 85cdc25f
Loading
Loading
Loading
Loading
+5 −21
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@@ -222,34 +221,26 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
        return lineEnd(widget, buffer);
    }

    private static boolean isTouchSelecting(boolean isMouse, Spannable buffer) {
        return isMouse ? Touch.isActivelySelecting(buffer) : isSelecting(buffer);
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        int initialScrollX = -1;
        int initialScrollY = -1;
        final int action = event.getAction();
        final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);

        if (action == MotionEvent.ACTION_UP) {
            initialScrollX = Touch.getInitialScrollX(widget, buffer);
            initialScrollY = Touch.getInitialScrollY(widget, buffer);
        }

        boolean wasTouchSelecting = isTouchSelecting(isMouse, buffer);
        boolean wasTouchSelecting = isSelecting(buffer);
        boolean handled = Touch.onTouchEvent(widget, buffer, event);

        if (widget.didTouchFocusSelect() && !isMouse) {
        if (widget.didTouchFocusSelect()) {
            return handled;
        }
        if (action == MotionEvent.ACTION_DOWN) {
            // Capture the mouse pointer down location to ensure selection starts
            // right under the mouse (and is not influenced by cursor location).
            // The code below needs to run for mouse events.
            // For touch events, the code should run only when selection is active.
            if (isMouse || isTouchSelecting(isMouse, buffer)) {
            if (isSelecting(buffer)) {
                if (!widget.isFocused()) {
                    if (!widget.requestFocus()) {
                        return handled;
@@ -265,15 +256,8 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
            }
        } else if (widget.isFocused()) {
            if (action == MotionEvent.ACTION_MOVE) {
                // Cursor can be active at any location in the text while mouse pointer can start
                // selection from a totally different location. Use LAST_TAP_DOWN span to ensure
                // text selection will start from mouse pointer location.
                if (isSelecting(buffer) && handled) {
                    final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
                if (isMouse && Touch.isSelectionStarted(buffer)) {
                    Selection.setSelection(buffer, startOffset);
                }

                if (isTouchSelecting(isMouse, buffer) && handled) {
                    // Before selecting, make sure we've moved out of the "slop".
                    // handled will be true, if we're in select mode AND we're
                    // OUT of the slop
+1 −49
Original line number Diff line number Diff line
@@ -119,18 +119,12 @@ public class Touch {
            ds = buffer.getSpans(0, buffer.length(), DragState.class);

            if (ds.length > 0) {
                ds[0].mIsSelectionStarted = false;

                if (ds[0].mFarEnough == false) {
                    int slop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();

                    if (Math.abs(event.getX() - ds[0].mX) >= slop ||
                        Math.abs(event.getY() - ds[0].mY) >= slop) {
                        ds[0].mFarEnough = true;
                        if (event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
                            ds[0].mIsActivelySelecting = true;
                            ds[0].mIsSelectionStarted = true;
                        }
                    }
                }

@@ -142,13 +136,9 @@ public class Touch {
                            || MetaKeyKeyListener.getMetaState(buffer,
                                    MetaKeyKeyListener.META_SELECTING) != 0;

                    if (!event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
                        ds[0].mIsActivelySelecting = false;
                    }

                    float dx;
                    float dy;
                    if (cap && event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
                    if (cap) {
                        // if we're selecting, we want the scroll to go in
                        // the direction of the drag
                        dx = event.getX() - ds[0].mX;
@@ -160,7 +150,6 @@ public class Touch {
                    ds[0].mX = event.getX();
                    ds[0].mY = event.getY();

                    int nx = widget.getScrollX() + (int) dx;
                    int ny = widget.getScrollY() + (int) dy;

                    int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
@@ -172,10 +161,6 @@ public class Touch {
                    int oldX = widget.getScrollX();
                    int oldY = widget.getScrollY();

                    if (!event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
                        scrollTo(widget, layout, nx, ny);
                    }

                    // If we actually scrolled, then cancel the up action.
                    if (oldX != widget.getScrollX() || oldY != widget.getScrollY()) {
                        widget.cancelLongPress();
@@ -207,37 +192,6 @@ public class Touch {
        return ds.length > 0 ? ds[0].mScrollY : -1;
    }

    /**
     * Checks if selection is still active.
     * This is useful for extending Selection span on buffer.
     * @param buffer The text buffer.
     * @return true if buffer has been marked for selection.
     *
     * @hide
     */
    static boolean isActivelySelecting(Spannable buffer) {
        DragState[] ds;
        ds = buffer.getSpans(0, buffer.length(), DragState.class);

        return ds.length > 0 && ds[0].mIsActivelySelecting;
    }

    /**
     * Checks if selection has begun (are we out of slop?).
     * Note: DragState.mIsSelectionStarted goes back to false with the very next event.
     * This is useful for starting Selection span on buffer.
     * @param buffer The text buffer.
     * @return true if selection has started on the buffer.
     *
     * @hide
     */
    static boolean isSelectionStarted(Spannable buffer) {
        DragState[] ds;
        ds = buffer.getSpans(0, buffer.length(), DragState.class);

        return ds.length > 0 && ds[0].mIsSelectionStarted;
    }

    private static class DragState implements NoCopySpan {
        public float mX;
        public float mY;
@@ -245,8 +199,6 @@ public class Touch {
        public int mScrollY;
        public boolean mFarEnough;
        public boolean mUsed;
        public boolean mIsActivelySelecting;
        public boolean mIsSelectionStarted;

        public DragState(float x, float y, int scrollX, int scrollY) {
            mX = x;
+139 −82
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ import android.view.ActionMode.Callback;
import android.view.DisplayListCanvas;
import android.view.DragEvent;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@@ -805,8 +806,8 @@ public class Editor {
        final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);

        // Safety check in case standard touch event handling has been bypassed
        if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
        if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
        if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
        if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;

        int selectionStart, selectionEnd;

@@ -1773,7 +1774,8 @@ public class Editor {
        stopTextActionMode();
        mPreserveDetachedSelection = false;

        getSelectionController().enterDrag();
        getSelectionController().enterDrag(
                SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
        return true;
    }

@@ -4492,14 +4494,22 @@ public class Editor {

        // Where the user first starts the drag motion.
        private int mStartOffset = -1;
        // Indicates whether the user is selecting text and using the drag accelerator.
        private boolean mDragAcceleratorActive;

        private boolean mHaventMovedEnoughToStartDrag;
        // The line that a selection happened most recently with the drag accelerator.
        private int mLineSelectionIsOn = -1;
        // Whether the drag accelerator has selected past the initial line.
        private boolean mSwitchedLines = false;

        // Indicates the drag accelerator mode that the user is currently using.
        private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
        // Drag accelerator is inactive.
        private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
        // Character based selection by dragging. Only for mouse.
        private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
        // Word based selection by dragging. Enabled after long pressing or double tapping.
        private static final int DRAG_ACCELERATOR_MODE_WORD = 2;

        SelectionModifierCursorController() {
            resetTouchOffsets();
        }
@@ -4510,7 +4520,6 @@ public class Editor {
            }
            initDrawables();
            initHandles();
            hideInsertionPointCursorController();
        }

        private void initDrawables() {
@@ -4548,10 +4557,10 @@ public class Editor {
            if (mEndHandle != null) mEndHandle.hide();
        }

        public void enterDrag() {
        public void enterDrag(int dragAcceleratorMode) {
            // Just need to init the handles / hide insertion cursor.
            show();
            mDragAcceleratorActive = true;
            mDragAcceleratorMode = dragAcceleratorMode;
            // Start location of selection.
            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
                    mLastDownPositionY);
@@ -4563,6 +4572,7 @@ public class Editor {
            // the user to continue dragging across the screen to select text; TextView will
            // scroll as necessary.
            mTextView.getParent().requestDisallowInterceptTouchEvent(true);
            mTextView.cancelLongPress();
        }

        public void onTouchEvent(MotionEvent event) {
@@ -4570,6 +4580,7 @@ public class Editor {
            // selection and tap can move cursor from this tap position.
            final float eventX = event.getX();
            final float eventY = event.getY();
            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    if (extractedTextModeWillBeStarted()) {
@@ -4593,7 +4604,7 @@ public class Editor {
                                boolean stayedInArea =
                                        distanceSquared < doubleTapSlop * doubleTapSlop;

                                if (stayedInArea && isPositionOnText(eventX, eventY)) {
                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
                                    selectCurrentWordAndStartDrag();
                                    mDiscardNextActionUp = true;
                                }
@@ -4639,36 +4650,118 @@ public class Editor {
                        }
                    }

                    if (isMouse && !isDragAcceleratorActive()) {
                        final int offset = mTextView.getOffsetForPosition(eventX, eventY);
                        if (mStartOffset != offset) {
                            // Start character based drag accelerator.
                            if (mTextActionMode != null) {
                                mTextActionMode.finish();
                            }
                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
                            mDiscardNextActionUp = true;
                            mHaventMovedEnoughToStartDrag = false;
                        }
                    }

                    if (mStartHandle != null && mStartHandle.isShowing()) {
                        // Don't do the drag if the handles are showing already.
                        break;
                    }

                    if (mStartOffset != -1 && mTextView.getLayout() != null) {
                        if (!mHaventMovedEnoughToStartDrag) {
                    updateSelection(event);
                    break;

                case MotionEvent.ACTION_UP:
                    if (!isDragAcceleratorActive()) {
                        break;
                    }
                    updateSelection(event);

                    // No longer dragging to select text, let the parent intercept events.
                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);

                    int startOffset = mTextView.getSelectionStart();
                    int endOffset = mTextView.getSelectionEnd();

                    // Since we don't let drag handles pass once they're visible, we need to
                    // make sure the start / end locations are correct because the user *can*
                    // switch directions during the initial drag.
                    if (endOffset < startOffset) {
                        int tmp = endOffset;
                        endOffset = startOffset;
                        startOffset = tmp;

                        // Also update the selection with the right offsets in this case.
                        Selection.setSelection((Spannable) mTextView.getText(),
                                startOffset, endOffset);
                    }
                    if (startOffset != endOffset) {
                        mStartHandle.showAtLocation(startOffset);
                        mEndHandle.showAtLocation(endOffset);
                        startSelectionActionMode();
                    }

                    // No longer the first dragging motion, reset.
                    resetDragAcceleratorState();
                    break;
            }
        }

        private void updateSelection(MotionEvent event) {
            if (mTextView.getLayout() != null) {
                switch (mDragAcceleratorMode) {
                    case DRAG_ACCELERATOR_MODE_CHARACTER:
                        updateCharacterBasedSelection(event);
                        break;
                    case DRAG_ACCELERATOR_MODE_WORD:
                        updateWordBasedSelection(event);
                        break;
                }
            }
        }

        private void updateCharacterBasedSelection(MotionEvent event) {
            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
            Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
        }

        private void updateWordBasedSelection(MotionEvent event) {
            if (mHaventMovedEnoughToStartDrag) {
                return;
            }
            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
            final ViewConfiguration viewConfig = ViewConfiguration.get(
                    mTextView.getContext());
            final float eventX = event.getX();
            final float eventY = event.getY();
            final int currLine;
            if (isMouse) {
                // No need to offset the y coordinate for mouse input.
                currLine = mTextView.getLineAtCoordinate(eventY);
            } else {
                float y = eventY;
                if (mSwitchedLines) {
                    // Offset the finger by the same vertical offset as the handles.
                    // This improves visibility of the content being selected by
                    // shifting the finger below the content, this is applied once
                    // the user has switched lines.
                    final int touchSlop = viewConfig.getScaledTouchSlop();
                    final float fingerOffset = (mStartHandle != null)
                            ? mStartHandle.getIdealVerticalOffset()
                            : touchSlop;
                    y = eventY - fingerOffset;
                }

                            final int currLine = getCurrentLineAdjustedForSlop(
                                    mTextView.getLayout(),
                                    mLineSelectionIsOn, y);
                currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
                        y);
                if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
                    // Break early here, we want to offset the finger position from
                    // the selection highlight, once the user moved their finger
                    // to a different line we should apply the offset and *not* switch
                    // lines until recomputing the position with the finger offset.
                    mSwitchedLines = true;
                                break;
                    return;
                }
            }

            int startOffset;
@@ -4687,46 +4780,6 @@ public class Editor {
            Selection.setSelection((Spannable) mTextView.getText(),
                    startOffset, offset);
        }
                    }
                    break;

                case MotionEvent.ACTION_UP:
                    if (mDragAcceleratorActive) {
                        // No longer dragging to select text, let the parent intercept events.
                        mTextView.getParent().requestDisallowInterceptTouchEvent(false);

                        show();
                        int startOffset = mTextView.getSelectionStart();
                        int endOffset = mTextView.getSelectionEnd();

                        // Since we don't let drag handles pass once they're visible, we need to
                        // make sure the start / end locations are correct because the user *can*
                        // switch directions during the initial drag.
                        if (endOffset < startOffset) {
                            int tmp = endOffset;
                            endOffset = startOffset;
                            startOffset = tmp;

                            // Also update the selection with the right offsets in this case.
                            Selection.setSelection((Spannable) mTextView.getText(),
                                    startOffset, endOffset);
                        }

                        // Need to do this to display the handles.
                        mStartHandle.showAtLocation(startOffset);
                        mEndHandle.showAtLocation(endOffset);

                        // No longer the first dragging motion, reset.
                        startSelectionActionMode();

                        mDragAcceleratorActive = false;
                        mStartOffset = -1;
                        mSwitchedLines = false;
                    }
                    break;
            }
        }

        /**
         * @param event
         */
@@ -4749,8 +4802,12 @@ public class Editor {

        public void resetTouchOffsets() {
            mMinTouchOffset = mMaxTouchOffset = -1;
            resetDragAcceleratorState();
        }

        private void resetDragAcceleratorState() {
            mStartOffset = -1;
            mDragAcceleratorActive = false;
            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
            mSwitchedLines = false;
        }

@@ -4765,7 +4822,7 @@ public class Editor {
         * @return true if the user is selecting text using the drag accelerator.
         */
        public boolean isDragAcceleratorActive() {
            return mDragAcceleratorActive;
            return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
        }

        public void onTouchModeChanged(boolean isInTouchMode) {