From b0d6ba1ec4f71b96cab7d1ff62b846d5cf162c4f Mon Sep 17 00:00:00 2001 From: Gilles Debunne Date: Tue, 17 Aug 2010 20:01:42 -0700 Subject: [PATCH] Text selection without trackball. Backported from HC. Squashed commit of the following: commit af214a595c7a9fdd11a2dc384f7d4665abf751c0 Fixes in TextView's selection. commit eb9fd59ebe6500a66c2003d46b5802299970ae8d TextView with Selection Contextual Mode commit 4c4c338ef355b369ce4b57d6c6fba7ee8f9dddf4 Cosmetic changes around TextView. commit d4b4b054e87480d984ad18766f5e76553e3080d8 Double and one and a half tap removed from TextView. commit 897c2847ba1fca8ef01eadadd1bc3de007af3ee5 Selection handlers in TextView commit 832be74a5394649e28927484d9a86c6d53b430e7 New cursor controller in TextViews. Change-Id: I01cc64736e2abea605317ee53907a1713617fc17 --- api/current.xml | 125 ++ core/java/android/text/Selection.java | 4 +- .../text/method/ArrowKeyMovementMethod.java | 348 ++--- core/java/android/text/method/Touch.java | 7 +- core/java/android/view/AbsSavedState.java | 4 +- core/java/android/view/MotionEvent.java | 2 +- core/java/android/view/View.java | 11 +- core/java/android/widget/TextView.java | 1226 +++++++++++++---- .../res/drawable-hdpi/text_select_handle.png | Bin 0 -> 11672 bytes .../res/drawable-mdpi/text_select_handle.png | Bin 0 -> 6605 bytes core/res/res/values-cs/strings.xml | 2 - core/res/res/values-da/strings.xml | 2 - core/res/res/values-de/strings.xml | 2 - core/res/res/values-el/strings.xml | 2 - core/res/res/values-en-rAU/strings.xml | 2 - core/res/res/values-en-rSG/strings.xml | 2 - core/res/res/values-en-rUS/strings.xml | 2 - core/res/res/values-es-rUS/strings.xml | 2 - core/res/res/values-es/strings.xml | 2 - core/res/res/values-fr/strings.xml | 2 - core/res/res/values-it/strings.xml | 2 - core/res/res/values-ja/strings.xml | 2 - core/res/res/values-ko/strings.xml | 2 - core/res/res/values-nb/strings.xml | 2 - core/res/res/values-nl/strings.xml | 2 - core/res/res/values-pl/strings.xml | 2 - core/res/res/values-pt-rPT/strings.xml | 2 - core/res/res/values-pt/strings.xml | 2 - core/res/res/values-ru/strings.xml | 2 - core/res/res/values-sv/strings.xml | 2 - core/res/res/values-tr/strings.xml | 2 - core/res/res/values-zh-rCN/strings.xml | 2 - core/res/res/values-zh-rTW/strings.xml | 2 - core/res/res/values/public.xml | 3 +- core/res/res/values/strings.xml | 10 +- graphics/java/android/graphics/Rect.java | 6 +- 36 files changed, 1189 insertions(+), 603 deletions(-) create mode 100644 core/res/res/drawable-hdpi/text_select_handle.png create mode 100644 core/res/res/drawable-mdpi/text_select_handle.png diff --git a/api/current.xml b/api/current.xml index 61f5e164bc11a..06e9bfd0b7d60 100644 --- a/api/current.xml +++ b/api/current.xml @@ -166341,6 +166341,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = (widget.getWidth()-widget.getTotalPaddingRight())) { - x = widget.getWidth()-widget.getTotalPaddingRight() - 1; - } - if (y < 0) { - y = 0; - } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) { - y = widget.getHeight()-widget.getTotalPaddingBottom() - 1; - } - - x += widget.getScrollX(); - y += widget.getScrollY(); - - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - - int offset = layout.getOffsetForHorizontal(line, x); - return offset; - } - public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { if (executeDown(widget, buffer, keyCode)) { MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); @@ -193,10 +160,9 @@ implements MovementMethod break; case KeyEvent.KEYCODE_DPAD_CENTER: - if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) { - if (widget.showContextMenu()) { + if ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) && + (widget.showContextMenu())) { handled = true; - } } } @@ -214,8 +180,7 @@ implements MovementMethod public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) { int code = event.getKeyCode(); - if (code != KeyEvent.KEYCODE_UNKNOWN - && event.getAction() == KeyEvent.ACTION_MULTIPLE) { + if (code != KeyEvent.KEYCODE_UNKNOWN && event.getAction() == KeyEvent.ACTION_MULTIPLE) { int repeat = event.getRepeatCount(); boolean handled = false; while ((--repeat) > 0) { @@ -226,13 +191,22 @@ implements MovementMethod return false; } - public boolean onTrackballEvent(TextView widget, Spannable text, - MotionEvent event) { + public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) { + if (mCursorController != null) { + mCursorController.hide(); + } return false; } - public boolean onTouchEvent(TextView widget, Spannable buffer, - MotionEvent event) { + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + if (mCursorController != null) { + return onTouchEventCursor(widget, buffer, event); + } else { + return onTouchEventStandard(widget, buffer, event); + } + } + + private boolean onTouchEventStandard(TextView widget, Spannable buffer, MotionEvent event) { int initialScrollX = -1, initialScrollY = -1; if (event.getAction() == MotionEvent.ACTION_UP) { initialScrollX = Touch.getInitialScrollX(widget, buffer); @@ -243,53 +217,20 @@ implements MovementMethod if (widget.isFocused() && !widget.didTouchFocusSelect()) { if (event.getAction() == MotionEvent.ACTION_DOWN) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - int x = (int) event.getX(); - int y = (int) event.getY(); - int offset = getOffset(x, y, widget); - + boolean cap = isCap(buffer); if (cap) { - buffer.setSpan(LAST_TAP_DOWN, offset, offset, - Spannable.SPAN_POINT_POINT); + int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + + buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); // Disallow intercepting of the touch events, so that // users can scroll and select at the same time. // without this, users would get booted out of select // mode once the view detected it needed to scroll. widget.getParent().requestDisallowInterceptTouchEvent(true); - } else { - OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), - OnePointFiveTapState.class); - - if (tap.length > 0) { - if (event.getEventTime() - tap[0].mWhen <= - ViewConfiguration.getDoubleTapTimeout() && - sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) { - - tap[0].active = true; - MetaKeyKeyListener.startSelecting(widget, buffer); - widget.getParent().requestDisallowInterceptTouchEvent(true); - buffer.setSpan(LAST_TAP_DOWN, offset, offset, - Spannable.SPAN_POINT_POINT); - } - - tap[0].mWhen = event.getEventTime(); - } else { - OnePointFiveTapState newtap = new OnePointFiveTapState(); - newtap.mWhen = event.getEventTime(); - newtap.active = false; - buffer.setSpan(newtap, 0, buffer.length(), - Spannable.SPAN_INCLUSIVE_INCLUSIVE); - } } } else if (event.getAction() == MotionEvent.ACTION_MOVE) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); + boolean cap = isCap(buffer); if (cap && handled) { // Before selecting, make sure we've moved out of the "slop". @@ -297,45 +238,15 @@ implements MovementMethod // OUT of the slop // Turn long press off while we're selecting. User needs to - // re-tap on the selection to enable longpress + // re-tap on the selection to enable long press widget.cancelLongPress(); // Update selection as we're moving the selection area. // Get the current touch position - int x = (int) event.getX(); - int y = (int) event.getY(); - int offset = getOffset(x, y, widget); + int offset = widget.getOffset((int) event.getX(), (int) event.getY()); - final OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), - OnePointFiveTapState.class); - - if (tap.length > 0 && tap[0].active) { - // Get the last down touch position (the position at which the - // user started the selection) - int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN); - - // Compute the selection boundaries - int spanstart; - int spanend; - if (offset >= lastDownOffset) { - // Expand from word start of the original tap to new word - // end, since we are selecting "forwards" - spanstart = findWordStart(buffer, lastDownOffset); - spanend = findWordEnd(buffer, offset); - } else { - // Expand to from new word start to word end of the original - // tap since we are selecting "backwards". - // The spanend will always need to be associated with the touch - // up position, so that refining the selection with the - // trackball will work as expected. - spanstart = findWordEnd(buffer, lastDownOffset); - spanend = findWordStart(buffer, offset); - } - Selection.setSelection(buffer, spanstart, spanend); - } else { - Selection.extendSelection(buffer, offset); - } + Selection.extendSelection(buffer, offset); return true; } } else if (event.getAction() == MotionEvent.ACTION_UP) { @@ -344,70 +255,17 @@ implements MovementMethod // the current scroll offset to avoid the scroll jumping later // to show it. if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || - (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { + (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { widget.moveCursorToVisibleOffset(); return true; } - int x = (int) event.getX(); - int y = (int) event.getY(); - int off = getOffset(x, y, widget); - - // XXX should do the same adjust for x as we do for the line. - - OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(), - OnePointFiveTapState.class); - if (onepointfivetap.length > 0 && onepointfivetap[0].active && - Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) { - // If we've set select mode, because there was a onepointfivetap, - // but there was no ensuing swipe gesture, undo the select mode - // and remove reference to the last onepointfivetap. - MetaKeyKeyListener.stopSelecting(widget, buffer); - for (int i=0; i < onepointfivetap.length; i++) { - buffer.removeSpan(onepointfivetap[i]); - } + int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + if (isCap(buffer)) { buffer.removeSpan(LAST_TAP_DOWN); - } - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - - DoubleTapState[] tap = buffer.getSpans(0, buffer.length(), - DoubleTapState.class); - boolean doubletap = false; - - if (tap.length > 0) { - if (event.getEventTime() - tap[0].mWhen <= - ViewConfiguration.getDoubleTapTimeout() && - sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { - - doubletap = true; - } - - tap[0].mWhen = event.getEventTime(); + Selection.extendSelection(buffer, offset); } else { - DoubleTapState newtap = new DoubleTapState(); - newtap.mWhen = event.getEventTime(); - buffer.setSpan(newtap, 0, buffer.length(), - Spannable.SPAN_INCLUSIVE_INCLUSIVE); - } - - if (cap) { - buffer.removeSpan(LAST_TAP_DOWN); - if (onepointfivetap.length > 0 && onepointfivetap[0].active) { - // If we selecting something with the onepointfivetap-and - // swipe gesture, stop it on finger up. - MetaKeyKeyListener.stopSelecting(widget, buffer); - } else { - Selection.extendSelection(buffer, off); - } - } else if (doubletap) { - Selection.setSelection(buffer, - findWordStart(buffer, off), - findWordEnd(buffer, off)); - } else { - Selection.setSelection(buffer, off); + Selection.setSelection(buffer, offset); } MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); @@ -420,73 +278,36 @@ implements MovementMethod return handled; } - private static class DoubleTapState implements NoCopySpan { - long mWhen; - } + private boolean onTouchEventCursor(TextView widget, Spannable buffer, MotionEvent event) { + if (widget.isFocused() && !widget.didTouchFocusSelect()) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_MOVE: + widget.cancelLongPress(); - /* We check for a onepointfive tap. This is similar to - * doubletap gesture (where a finger goes down, up, down, up, in a short - * time period), except in the onepointfive tap, a users finger only needs - * to go down, up, down in a short time period. We detect this type of tap - * to implement the onepointfivetap-and-swipe selection gesture. - * This gesture allows users to select a segment of text without going - * through the "select text" option in the context menu. - */ - private static class OnePointFiveTapState implements NoCopySpan { - long mWhen; - boolean active; - } + // Offset the current touch position (from controller to cursor) + final float x = event.getX() + mCursorController.getOffsetX(); + final float y = event.getY() + mCursorController.getOffsetY(); + int offset = widget.getOffset((int) x, (int) y); + mCursorController.updatePosition(offset); + return true; - private static boolean sameWord(CharSequence text, int one, int two) { - int start = findWordStart(text, one); - int end = findWordEnd(text, one); - - if (end == start) { - return false; - } - - return start == findWordStart(text, two) && - end == findWordEnd(text, two); - } - - // TODO: Unify with TextView.getWordForDictionary() - private static int findWordStart(CharSequence text, int start) { - for (; start > 0; start--) { - char c = text.charAt(start - 1); - int type = Character.getType(c); - - if (c != '\'' && - type != Character.UPPERCASE_LETTER && - type != Character.LOWERCASE_LETTER && - type != Character.TITLECASE_LETTER && - type != Character.MODIFIER_LETTER && - type != Character.DECIMAL_DIGIT_NUMBER) { - break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mCursorController = null; + return true; } } - - return start; + return false; } - // TODO: Unify with TextView.getWordForDictionary() - private static int findWordEnd(CharSequence text, int end) { - int len = text.length(); - - for (; end < len; end++) { - char c = text.charAt(end); - int type = Character.getType(c); - - if (c != '\'' && - type != Character.UPPERCASE_LETTER && - type != Character.LOWERCASE_LETTER && - type != Character.TITLECASE_LETTER && - type != Character.MODIFIER_LETTER && - type != Character.DECIMAL_DIGIT_NUMBER) { - break; - } - } - - return end; + /** + * Defines the cursor controller. + * + * When set, this object can be used to handle events, that can be translated in cursor updates. + * @param cursorController A cursor controller implementation + */ + public void setCursorController(CursorController cursorController) { + mCursorController = cursorController; } public boolean canSelectArbitrarily() { @@ -525,8 +346,9 @@ implements MovementMethod } public static MovementMethod getInstance() { - if (sInstance == null) + if (sInstance == null) { sInstance = new ArrowKeyMovementMethod(); + } return sInstance; } diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java index 42ad10e706ef9..a19a78e469881 100644 --- a/core/java/android/text/method/Touch.java +++ b/core/java/android/text/method/Touch.java @@ -17,14 +17,13 @@ package android.text.method; import android.text.Layout; -import android.text.NoCopySpan; import android.text.Layout.Alignment; +import android.text.NoCopySpan; import android.text.Spannable; -import android.util.Log; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.widget.TextView; -import android.view.KeyEvent; public class Touch { private Touch() { } @@ -99,7 +98,7 @@ public class Touch { MotionEvent event) { DragState[] ds; - switch (event.getAction()) { + switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: ds = buffer.getSpans(0, buffer.length(), DragState.class); diff --git a/core/java/android/view/AbsSavedState.java b/core/java/android/view/AbsSavedState.java index 840d7c170bd8a..6ad33dd25d45c 100644 --- a/core/java/android/view/AbsSavedState.java +++ b/core/java/android/view/AbsSavedState.java @@ -54,7 +54,7 @@ public abstract class AbsSavedState implements Parcelable { */ protected AbsSavedState(Parcel source) { // FIXME need class loader - Parcelable superState = (Parcelable) source.readParcelable(null); + Parcelable superState = source.readParcelable(null); mSuperState = superState != null ? superState : EMPTY_STATE; } @@ -75,7 +75,7 @@ public abstract class AbsSavedState implements Parcelable { = new Parcelable.Creator() { public AbsSavedState createFromParcel(Parcel in) { - Parcelable superState = (Parcelable) in.readParcelable(null); + Parcelable superState = in.readParcelable(null); if (superState != null) { throw new IllegalStateException("superState must be null"); } diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 47061c1710e93..132852520f94a 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -772,7 +772,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { * * @param pointerId The identifier of the pointer to be found. * @return Returns either the index of the pointer (for use with - * {@link #getX(int) et al.), or -1 if there is no data available for + * {@link #getX(int)} et al.), or -1 if there is no data available for * that pointer identifier. */ public final int findPointerIndex(int pointerId) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index b8623e73366ee..c9662ff81ef49 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -2413,11 +2413,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * Call this view's OnLongClickListener, if it is defined. Invokes the context menu - * if the OnLongClickListener did not consume the event. + * Call this view's OnLongClickListener, if it is defined. Invokes the context menu if the + * OnLongClickListener did not consume the event. * - * @return True there was an assigned OnLongClickListener that was called, false - * otherwise is returned. + * @return True if one of the above receivers consumed the event, false otherwise. */ public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); @@ -4218,6 +4217,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Show the context menu for this view. It is not safe to hold on to the * menu after returning from this method. * + * You should normally not overload this method. Overload + * {@link #onCreateContextMenu(ContextMenu)} or define an + * {@link OnCreateContextMenuListener} to add items to the context menu. + * * @param menu The context menu to populate */ public void createContextMenu(ContextMenu menu) { diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 27e0e94d603e0..73b1d6d677627 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -23,6 +23,7 @@ import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; @@ -61,6 +62,7 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.method.ArrowKeyMovementMethod; import android.text.method.DateKeyListener; import android.text.method.DateTimeKeyListener; import android.text.method.DialerKeyListener; @@ -89,10 +91,11 @@ import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewDebug; +import android.view.ViewGroup.LayoutParams; import android.view.ViewRoot; import android.view.ViewTreeObserver; -import android.view.ViewGroup.LayoutParams; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AnimationUtils; @@ -185,7 +188,7 @@ import java.util.ArrayList; */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { - static final String TAG = "TextView"; + static final String LOG_TAG = "TextView"; static final boolean DEBUG_EXTRACT = false; private static int PRIORITY = 100; @@ -321,6 +324,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener this(context, attrs, com.android.internal.R.attr.textViewStyle); } + @SuppressWarnings("deprecation") public TextView(Context context, AttributeSet attrs, int defStyle) { @@ -695,9 +699,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener try { setInputExtras(a.getResourceId(attr, 0)); } catch (XmlPullParserException e) { - Log.w("TextView", "Failure reading input extras", e); + Log.w(LOG_TAG, "Failure reading input extras", e); } catch (IOException e) { - Log.w("TextView", "Failure reading input extras", e); + Log.w(LOG_TAG, "Failure reading input extras", e); } break; } @@ -714,7 +718,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (inputMethod != null) { - Class c; + Class c; try { c = Class.forName(inputMethod.toString()); @@ -923,6 +927,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setFocusable(focusable); setClickable(clickable); setLongClickable(longClickable); + + prepareCursorControllers(); } private void setTypefaceByIndex(int typefaceIndex, int styleIndex) { @@ -1128,6 +1134,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(mText); fixFocusableAndClickableSettings(); + + // SelectionModifierCursorController depends on canSelectText, which depends on mMovement + prepareCursorControllers(); } private void fixFocusableAndClickableSettings() { @@ -2335,6 +2344,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return str + "}"; } + @SuppressWarnings("hiding") public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public SavedState createFromParcel(Parcel in) { @@ -2369,8 +2379,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int end = 0; if (mText != null) { - start = Selection.getSelectionStart(mText); - end = Selection.getSelectionEnd(mText); + start = getSelectionStart(); + end = getSelectionEnd(); if (start >= 0 || end >= 0) { // Or save state if there is a selection save = true; @@ -2442,7 +2452,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener restored = "(restored) "; } - Log.e("TextView", "Saved cursor position " + ss.selStart + + Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + "/" + ss.selEnd + " out of range for " + restored + "text " + mText); } else { @@ -2694,6 +2704,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (needEditableForNotification) { sendAfterTextChanged((Editable) text); } + + // SelectionModifierCursorController depends on canSelectText, which depends on text + prepareCursorControllers(); } /** @@ -2756,6 +2769,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mChars[off + mStart]; } + @Override public String toString() { return new String(mChars, mStart, mLength); } @@ -2981,7 +2995,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { input = TextKeyListener.getInstance(); } - mInputType = type; + setRawInputType(type); if (direct) mInput = input; else { setKeyListenerOnly(input); @@ -3198,7 +3212,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @param create If true, the extras will be created if they don't already * exist. Otherwise, null will be returned if none have been created. - * @see #setInputExtras(int)View + * @see #setInputExtras(int) * @see EditorInfo#extras * @attr ref android.R.styleable#TextView_editorExtras */ @@ -3312,7 +3326,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private static class ErrorPopup extends PopupWindow { private boolean mAbove = false; - private TextView mView; + private final TextView mView; ErrorPopup(TextView v, int width, int height) { super(v, width, height); @@ -3585,7 +3599,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void invalidateCursor() { - int where = Selection.getSelectionEnd(mText); + int where = getSelectionEnd(); invalidateCursor(where, where, where); } @@ -3661,7 +3675,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean changed = false; if (mMovement != null) { - int curs = Selection.getSelectionEnd(mText); + /* This code also provides auto-scrolling when a cursor is moved using a + * CursorController (insertion point or selection limits). + * For selection, ensure start or end is visible depending on controller's state. + */ + int curs = getSelectionEnd(); + if (mSelectionModifierCursorController != null) { + SelectionModifierCursorController selectionController = + (SelectionModifierCursorController) mSelectionModifierCursorController; + if (selectionController.isSelectionStartDragged()) { + curs = getSelectionStart(); + } + } /* * TODO: This should really only keep the end in view if @@ -3680,6 +3705,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener changed = bringTextIntoView(); } + if (mShouldStartTextSelectionMode) { + startTextSelectionMode(); + mShouldStartTextSelectionMode = false; + } mPreDrawState = PREDRAW_DONE; return !changed; } @@ -3954,8 +3983,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // XXX This is not strictly true -- a program could set the // selection manually if it really wanted to. if (mMovement != null && (isFocused() || isPressed())) { - selStart = Selection.getSelectionStart(mText); - selEnd = Selection.getSelectionEnd(mText); + selStart = getSelectionStart(); + selEnd = getSelectionEnd(); if (mCursorVisible && selStart >= 0 && isEnabled()) { if (mHighlightPath == null) @@ -4061,6 +4090,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ canvas.restore(); + + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.draw(canvas); + } + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.draw(canvas); + } } @Override @@ -4345,6 +4381,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return super.onKeyUp(keyCode, event); } + hideControllers(); + switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: /* @@ -4475,8 +4513,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener outAttrs.hintText = mHint; if (mText instanceof Editable) { InputConnection ic = new EditableInputConnection(this); - outAttrs.initialSelStart = Selection.getSelectionStart(mText); - outAttrs.initialSelEnd = Selection.getSelectionEnd(mText); + outAttrs.initialSelStart = getSelectionStart(); + outAttrs.initialSelEnd = getSelectionEnd(); outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType); return ic; } @@ -4560,8 +4598,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener outText.flags |= ExtractedText.FLAG_SINGLE_LINE; } outText.startOffset = 0; - outText.selectionStart = Selection.getSelectionStart(content); - outText.selectionEnd = Selection.getSelectionEnd(content); + outText.selectionStart = getSelectionStart(); + outText.selectionEnd = getSelectionEnd(); return true; } return false; @@ -4578,7 +4616,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (req != null) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { - if (DEBUG_EXTRACT) Log.v(TAG, "Retrieving extracted start=" + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start=" + ims.mChangedStart + " end=" + ims.mChangedEnd + " delta=" + ims.mChangedDelta); if (ims.mChangedStart < 0 && !contentChanged) { @@ -4586,7 +4624,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, ims.mChangedDelta, ims.mTmpExtracted)) { - if (DEBUG_EXTRACT) Log.v(TAG, "Reporting extracted start=" + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start=" + ims.mTmpExtracted.partialStartOffset + " end=" + ims.mTmpExtracted.partialEndOffset + ": " + ims.mTmpExtracted.text); @@ -4736,7 +4774,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener void updateAfterEdit() { invalidate(); - int curs = Selection.getSelectionStart(mText); + int curs = getSelectionStart(); if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) { @@ -4878,7 +4916,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener w, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad); } - // Log.e("aaa", "Boring: " + mTransformed); mSavedLayout = (BoringLayout) mLayout; } else if (shouldEllipsize && boring.width <= w) { @@ -4904,7 +4941,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mLayout = new StaticLayout(mTransformed, mTextPaint, w, alignment, mSpacingMult, mSpacingAdd, mIncludePad); - // Log.e("aaa", "Boring but wide: " + mTransformed); } } else if (shouldEllipsize) { mLayout = new StaticLayout(mTransformed, @@ -5002,6 +5038,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } + + // CursorControllers need a non-null mLayout + prepareCursorControllers(); } private boolean compressText(float width) { @@ -5473,7 +5512,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // FIXME: Is it okay to truncate this, or should we round? final int x = (int)mLayout.getPrimaryHorizontal(offset); final int top = mLayout.getLineTop(line); - final int bottom = mLayout.getLineTop(line+1); + final int bottom = mLayout.getLineTop(line + 1); int left = (int) FloatMath.floor(mLayout.getLineLeft(line)); int right = (int) FloatMath.ceil(mLayout.getLineRight(line)); @@ -5610,8 +5649,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // viewport coordinates, but requestRectangleOnScreen() // is in terms of content coordinates. - Rect r = new Rect(); - getInterestingRect(r, x, top, bottom, line); + Rect r = new Rect(x, top, x + 1, bottom); + getInterestingRect(r, line); r.offset(mScrollX, mScrollY); if (requestRectangleOnScreen(r)) { @@ -5627,13 +5666,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * to the user. This will not move the cursor if it represents more than * one character (a selection range). This will only work if the * TextView contains spannable text; otherwise it will do nothing. + * + * @return True if the cursor was actually moved, false otherwise. */ public boolean moveCursorToVisibleOffset() { if (!(mText instanceof Spannable)) { return false; } - int start = Selection.getSelectionStart(mText); - int end = Selection.getSelectionEnd(mText); + int start = getSelectionStart(); + int end = getSelectionEnd(); if (start != end) { return false; } @@ -5643,7 +5684,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int line = mLayout.getLineForOffset(start); final int top = mLayout.getLineTop(line); - final int bottom = mLayout.getLineTop(line+1); + final int bottom = mLayout.getLineTop(line + 1); final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); int vslack = (bottom - top) / 2; if (vslack > vspace / 4) @@ -5689,22 +5730,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void getInterestingRect(Rect r, int h, int top, int bottom, - int line) { + private void getInterestingRect(Rect r, int line) { + convertFromViewportToContentCoordinates(r); + + // Rectangle can can be expanded on first and last line to take + // padding into account. + // TODO Take left/right padding into account too? + if (line == 0) r.top -= getExtendedPaddingTop(); + if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom(); + } + + private void convertFromViewportToContentCoordinates(Rect r) { int paddingTop = getExtendedPaddingTop(); if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { paddingTop += getVerticalOffset(false); } - top += paddingTop; - bottom += paddingTop; - h += getCompoundPaddingLeft(); + r.top += paddingTop; + r.bottom += paddingTop; - if (line == 0) - top -= getExtendedPaddingTop(); - if (line == mLayout.getLineCount() - 1) - bottom += getExtendedPaddingBottom(); + int paddingLeft = getCompoundPaddingLeft(); + r.left += paddingLeft; + r.right += paddingLeft; - r.set(h, top, h+1, bottom); r.offset(-mScrollX, -mScrollY); } @@ -5872,6 +5919,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else if (mBlink != null) { mBlink.removeCallbacks(mBlink); } + + // InsertionPointCursorController depends on mCursorVisible + prepareCursorControllers(); } private boolean canMarquee() { @@ -5930,7 +5980,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private final WeakReference mView; private byte mStatus = MARQUEE_STOPPED; - private float mScrollUnit; + private final float mScrollUnit; private float mMaxScroll; float mMaxFadeScroll; private float mGhostStart; @@ -5942,7 +5992,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Marquee(TextView v) { final float density = v.getContext().getResources().getDisplayMetrics().density; - mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / (float) MARQUEE_RESOLUTION; + mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION; mView = new WeakReference(v); } @@ -6175,6 +6225,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sendOnTextChanged(buffer, start, before, after); onTextChanged(buffer, start, before, after); + hideControllers(); } /** @@ -6286,7 +6337,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } else { - if (DEBUG_EXTRACT) Log.v(TAG, "Span change outside of batch: " + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: " + oldStart + "-" + oldEnd + "," + newStart + "-" + newEnd + what); ims.mContentChanged = true; @@ -6302,7 +6353,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void beforeTextChanged(CharSequence buffer, int start, int before, int after) { - if (DEBUG_EXTRACT) Log.v(TAG, "beforeTextChanged start=" + start + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); if (AccessibilityManager.getInstance(mContext).isEnabled() @@ -6315,7 +6366,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onTextChanged(CharSequence buffer, int start, int before, int after) { - if (DEBUG_EXTRACT) Log.v(TAG, "onTextChanged start=" + start + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); TextView.this.handleTextChanged(buffer, start, before, after); @@ -6328,7 +6379,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } public void afterTextChanged(Editable buffer) { - if (DEBUG_EXTRACT) Log.v(TAG, "afterTextChanged: " + buffer); + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer); TextView.this.sendAfterTextChanged(buffer); if (MetaKeyKeyListener.getMetaState(buffer, @@ -6339,19 +6390,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) { - if (DEBUG_EXTRACT) Log.v(TAG, "onSpanChanged s=" + s + " e=" + e + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e + " st=" + st + " en=" + en + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, st, e, en); } public void onSpanAdded(Spannable buf, Object what, int s, int e) { - if (DEBUG_EXTRACT) Log.v(TAG, "onSpanAdded s=" + s + " e=" + e + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, -1, s, -1, e); } public void onSpanRemoved(Spannable buf, Object what, int s, int e) { - if (DEBUG_EXTRACT) Log.v(TAG, "onSpanRemoved s=" + s + " e=" + e + if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e + " what=" + what + ": " + buf); TextView.this.spanChange(buf, what, s, -1, e, -1); } @@ -6455,12 +6506,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mError != null) { showError(); } + + // We cannot start the selection mode immediately. The layout may be null here and is + // needed by the cursor controller. Layout creation is deferred up to drawing. The + // selection action mode will be started in onPreDraw(). + if (selStart != selEnd) { + mShouldStartTextSelectionMode = true; + } } else { if (mError != null) { hideError(); } // Don't leave us in the middle of a batch edit. onEndBatchEdit(); + + hideInsertionPointCursorController(); + terminateTextSelectionMode(); } startStopMarquee(focused); @@ -6527,24 +6588,52 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } class CommitSelectionReceiver extends ResultReceiver { - int mNewStart; - int mNewEnd; + private final int mPrevStart, mPrevEnd; + private final int mNewStart, mNewEnd; - CommitSelectionReceiver() { + public CommitSelectionReceiver(int mPrevStart, int mPrevEnd, int mNewStart, int mNewEnd) { super(getHandler()); + this.mPrevStart = mPrevStart; + this.mPrevEnd = mPrevEnd; + this.mNewStart = mNewStart; + this.mNewEnd = mNewEnd; } + @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - if (resultCode != InputMethodManager.RESULT_SHOWN) { - final int len = mText.length(); - if (mNewStart > len) { - mNewStart = len; + int start = mNewStart; + int end = mNewEnd; + + // Move the cursor to the new position, unless this tap was actually + // use to show the IMM. Leave cursor unchanged in that case. + if (resultCode == InputMethodManager.RESULT_SHOWN) { + start = mPrevStart; + end = mPrevEnd; + } else { + if ((mPrevStart != mPrevEnd) && (start == end)) { + if ((start >= mPrevStart) && (start <= mPrevEnd)) { + // Tapping inside the selection does nothing + Selection.setSelection((Spannable) mText, mPrevStart, mPrevEnd); + return; + } else { + // Tapping outside stops selection mode, if any + stopTextSelectionMode(); + } } - if (mNewEnd > len) { - mNewEnd = len; + + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.show(); } - Selection.setSelection((Spannable)mText, mNewStart, mNewEnd); } + + final int len = mText.length(); + if (start > len) { + start = len; + } + if (end > len) { + end = len; + } + Selection.setSelection((Spannable)mText, start, end); } } @@ -6557,7 +6646,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTouchFocusSelected = false; mScrolled = false; } - + final boolean superResult = super.onTouchEvent(event); /* @@ -6572,42 +6661,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if ((mMovement != null || onCheckIsTextEditor()) && mText instanceof Spannable && mLayout != null) { - boolean handled = false; + int oldSelStart = getSelectionStart(); + int oldSelEnd = getSelectionEnd(); - int oldSelStart = Selection.getSelectionStart(mText); - int oldSelEnd = Selection.getSelectionEnd(mText); + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.onTouchEvent(event); + } + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.onTouchEvent(event); + } + + boolean handled = false; if (mMovement != null) { handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); } - if (mText instanceof Editable && onCheckIsTextEditor()) { + if (isTextEditable()) { if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - // This is going to be gross... if tapping on the text view - // causes the IME to be displayed, we don't want the selection - // to change. But the selection has already changed, and - // we won't know right away whether the IME is getting - // displayed, so... + final int newSelStart = getSelectionStart(); + final int newSelEnd = getSelectionEnd(); - int newSelStart = Selection.getSelectionStart(mText); - int newSelEnd = Selection.getSelectionEnd(mText); CommitSelectionReceiver csr = null; if (newSelStart != oldSelStart || newSelEnd != oldSelEnd) { - csr = new CommitSelectionReceiver(); - csr.mNewStart = newSelStart; - csr.mNewEnd = newSelEnd; + csr = new CommitSelectionReceiver(oldSelStart, oldSelEnd, + newSelStart, newSelEnd); } - if (imm.showSoftInput(this, 0, csr) && csr != null) { - // The IME might get shown -- revert to the old - // selection, and change to the new when we finally - // find out of it is okay. - Selection.setSelection((Spannable)mText, oldSelStart, oldSelEnd); - handled = true; - } + handled |= imm.showSoftInput(this, 0, csr) && (csr != null); } } @@ -6619,6 +6703,34 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } + private void prepareCursorControllers() { + // TODO Add an extra android:cursorController flag to disable the controller? + if (mCursorVisible && mLayout != null) { + if (mInsertionPointCursorController == null) { + mInsertionPointCursorController = new InsertionPointCursorController(); + } + } else { + mInsertionPointCursorController = null; + } + + if (canSelectText() && mLayout != null) { + if (mSelectionModifierCursorController == null) { + mSelectionModifierCursorController = new SelectionModifierCursorController(); + } + } else { + // Stop selection mode if the controller becomes unavailable. + stopTextSelectionMode(); + mSelectionModifierCursorController = null; + } + } + + /** + * @return True iff this TextView contains a text that can be edited. + */ + private boolean isTextEditable() { + return mText instanceof Editable && onCheckIsTextEditor(); + } + /** * Returns true, only while processing a touch gesture, if the initial * touch down event caused focus to move to the text view and as a result @@ -6652,7 +6764,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private static class Blink extends Handler implements Runnable { - private WeakReference mView; + private final WeakReference mView; private boolean mCancelled; public Blink(TextView v) { @@ -6669,8 +6781,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener TextView tv = mView.get(); if (tv != null && tv.isFocused()) { - int st = Selection.getSelectionStart(tv.mText); - int en = Selection.getSelectionEnd(tv.mText); + int st = tv.getSelectionStart(); + int en = tv.getSelectionEnd(); if (st == en && st >= 0 && en >= 0) { if (tv.mLayout != null) { @@ -6851,21 +6963,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private boolean canSelectAll() { - if (mText instanceof Spannable && mText.length() != 0 && - mMovement != null && mMovement.canSelectArbitrarily()) { - return true; - } - - return false; + return canSelectText() && mText.length() != 0; } private boolean canSelectText() { - if (mText instanceof Spannable && mText.length() != 0 && - mMovement != null && mMovement.canSelectArbitrarily()) { - return true; - } - - return false; + // prepareCursorController() relies on this method. + // If you change this condition, make sure prepareCursorController is called anywhere + // the value of this condition might be changed. + return (mText instanceof Spannable && + mMovement != null && + mMovement.canSelectArbitrarily()); } private boolean canCut() { @@ -6895,23 +7002,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private boolean canPaste() { - if (mText instanceof Editable && mInput != null && - getSelectionStart() >= 0 && getSelectionEnd() >= 0) { - ClipboardManager clip = (ClipboardManager)getContext() - .getSystemService(Context.CLIPBOARD_SERVICE); - if (clip.hasText()) { - return true; - } - } - - return false; + return (mText instanceof Editable && + mInput != null && + getSelectionStart() >= 0 && + getSelectionEnd() >= 0 && + ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)). + hasText()); } /** - * Returns a word to add to the dictionary from the context menu, - * or null if there is no cursor or no word at the cursor. + * Returns the offsets delimiting the 'word' located at position offset. + * + * @param offset An offset in the text. + * @return The offsets for the start and end of the word located at offset. + * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits. + * Returns a negative value if no valid word was found. */ - private String getWordForDictionary() { + private long getWordLimitsAt(int offset) { /* * Quick return if the input type is one where adding words * to the dictionary doesn't make any sense. @@ -6920,7 +7027,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (klass == InputType.TYPE_CLASS_NUMBER || klass == InputType.TYPE_CLASS_PHONE || klass == InputType.TYPE_CLASS_DATETIME) { - return null; + return -1; } int variation = mInputType & InputType.TYPE_MASK_VARIATION; @@ -6929,13 +7036,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - return null; + return -1; } - int end = getSelectionEnd(); + int end = offset; if (end < 0) { - return null; + return -1; } int start = end; @@ -6969,6 +7076,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + if (start == end) { + return -1; + } + + if (end - start > 48) { + return -1; + } + boolean hasLetter = false; for (int i = start; i < end; i++) { if (Character.isLetter(mTransformed.charAt(i))) { @@ -6976,21 +7091,61 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; } } + if (!hasLetter) { - return null; + return -1; } - if (start == end) { - return null; - } - - if (end - start > 48) { - return null; - } - - return TextUtils.substring(mTransformed, start, end); + // Two ints packed in a long + return (((long) start) << 32) | end; } + + private void selectCurrentWord() { + // In case selection mode is started after an orientation change or after a select all, + // use the current selection instead of creating one + if (hasSelection()) { + return; + } + int selectionStart, selectionEnd; + + SelectionModifierCursorController selectionModifierCursorController = + ((SelectionModifierCursorController) mSelectionModifierCursorController); + int minOffset = selectionModifierCursorController.getMinTouchOffset(); + int maxOffset = selectionModifierCursorController.getMaxTouchOffset(); + + long wordLimits = getWordLimitsAt(minOffset); + if (wordLimits >= 0) { + selectionStart = (int) (wordLimits >>> 32); + } else { + selectionStart = Math.max(minOffset - 5, 0); + } + + wordLimits = getWordLimitsAt(maxOffset); + if (wordLimits >= 0) { + selectionEnd = (int) (wordLimits & 0x00000000FFFFFFFFL); + } else { + selectionEnd = Math.min(maxOffset + 5, mText.length()); + } + + Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); + } + + private String getWordForDictionary() { + int offset = ((SelectionModifierCursorController) mSelectionModifierCursorController). + getMinTouchOffset(); + + long wordLimits = getWordLimitsAt(offset); + if (wordLimits >= 0) { + int start = (int) (wordLimits >>> 32); + int end = (int) (wordLimits & 0x00000000FFFFFFFFL); + return mTransformed.subSequence(start, end).toString(); + } else { + return null; + } + + } + @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { if (!isShown()) { @@ -7032,114 +7187,98 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener super.onCreateContextMenu(menu); boolean added = false; - if (!isFocused()) { - if (isFocusable() && mInput != null) { - if (canCopy()) { - MenuHandler handler = new MenuHandler(); - int name = com.android.internal.R.string.copyAll; + if (mIsInTextSelectionMode) { + MenuHandler handler = new MenuHandler(); + + if (canCut()) { + menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('x'); + } - menu.add(0, ID_COPY, 0, name). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('c'); - menu.setHeaderTitle(com.android.internal.R.string. - editTextMenuTitle); + if (canCopy()) { + menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('c'); + } + + if (canPaste()) { + menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('v'); + } + + menu.add(0, ID_STOP_SELECTING_TEXT, 0, com.android.internal.R.string.stopSelectingText). + setOnMenuItemClickListener(handler); + + added = true; + } else { + /* + if (!isFocused()) { + if (isFocusable() && mInput != null) { + if (canCopy()) { + MenuHandler handler = new MenuHandler(); + menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('c'); + menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle); + } + } + + //return; + } + */ + MenuHandler handler = new MenuHandler(); + + if (canSelectText()) { + menu.add(0, ID_START_SELECTING_TEXT, 0, com.android.internal.R.string.selectText). + setOnMenuItemClickListener(handler); + added = true; + } + + if (canSelectAll()) { + menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('a'); + added = true; + } + + if (mText instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + URLSpan[] urls = ((Spanned) mText).getSpans(min, max, + URLSpan.class); + if (urls.length == 1) { + menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl). + setOnMenuItemClickListener(handler); + added = true; } } + + if (canPaste()) { + menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('v'); + } - return; - } - - MenuHandler handler = new MenuHandler(); - - if (canSelectAll()) { - menu.add(0, ID_SELECT_ALL, 0, - com.android.internal.R.string.selectAll). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('a'); - added = true; - } - - boolean selection = getSelectionStart() != getSelectionEnd(); - - if (canSelectText()) { - if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) { - menu.add(0, ID_STOP_SELECTING_TEXT, 0, - com.android.internal.R.string.stopSelectingText). - setOnMenuItemClickListener(handler); - added = true; - } else { - menu.add(0, ID_START_SELECTING_TEXT, 0, - com.android.internal.R.string.selectText). - setOnMenuItemClickListener(handler); + if (isInputMethodTarget()) { + menu.add(1, ID_SWITCH_INPUT_METHOD, 0, com.android.internal.R.string.inputMethod). + setOnMenuItemClickListener(handler); added = true; } - } - if (canCut()) { - int name; - if (selection) { - name = com.android.internal.R.string.cut; - } else { - name = com.android.internal.R.string.cutAll; - } - - menu.add(0, ID_CUT, 0, name). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('x'); - added = true; - } - - if (canCopy()) { - int name; - if (selection) { - name = com.android.internal.R.string.copy; - } else { - name = com.android.internal.R.string.copyAll; - } - - menu.add(0, ID_COPY, 0, name). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('c'); - added = true; - } - - if (canPaste()) { - menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('v'); - added = true; - } - - if (mText instanceof Spanned) { - int selStart = getSelectionStart(); - int selEnd = getSelectionEnd(); - - int min = Math.min(selStart, selEnd); - int max = Math.max(selStart, selEnd); - - URLSpan[] urls = ((Spanned) mText).getSpans(min, max, - URLSpan.class); - if (urls.length == 1) { - menu.add(0, ID_COPY_URL, 0, - com.android.internal.R.string.copyUrl). - setOnMenuItemClickListener(handler); - added = true; - } - } - - if (isInputMethodTarget()) { - menu.add(1, ID_SWITCH_INPUT_METHOD, 0, com.android.internal.R.string.inputMethod). - setOnMenuItemClickListener(handler); - added = true; - } - - String word = getWordForDictionary(); - if (word != null) { - menu.add(1, ID_ADD_TO_DICTIONARY, 0, + String word = getWordForDictionary(); + if (word != null) { + menu.add(1, ID_ADD_TO_DICTIONARY, 0, getContext().getString(com.android.internal.R.string.addToDictionary, word)). - setOnMenuItemClickListener(handler); - added = true; + setOnMenuItemClickListener(handler); + added = true; + } } if (added) { @@ -7156,6 +7295,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return imm != null && imm.isActive(this); } + // Context menu entries private static final int ID_SELECT_ALL = android.R.id.selectAll; private static final int ID_START_SELECTING_TEXT = android.R.id.startSelectingText; private static final int ID_STOP_SELECTING_TEXT = android.R.id.stopSelectingText; @@ -7181,22 +7321,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * or {@link android.R.id#switchInputMethod}. */ public boolean onTextContextMenuItem(int id) { - int selStart = getSelectionStart(); - int selEnd = getSelectionEnd(); + int min = 0; + int max = mText.length(); - if (!isFocused()) { - selStart = 0; - selEnd = mText.length(); - } + if (isFocused()) { + final int selStart = getSelectionStart(); + final int selEnd = getSelectionEnd(); - int min = Math.min(selStart, selEnd); - int max = Math.max(selStart, selEnd); - - if (min < 0) { - min = 0; - } - if (max < 0) { - max = 0; + min = Math.max(0, Math.min(selStart, selEnd)); + max = Math.max(0, Math.max(selStart, selEnd)); } ClipboardManager clip = (ClipboardManager)getContext() @@ -7204,63 +7337,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener switch (id) { case ID_SELECT_ALL: - Selection.setSelection((Spannable) mText, 0, - mText.length()); + Selection.setSelection((Spannable) mText, 0, mText.length()); + startTextSelectionMode(); return true; case ID_START_SELECTING_TEXT: - MetaKeyKeyListener.startSelecting(this, (Spannable) mText); + startTextSelectionMode(); return true; case ID_STOP_SELECTING_TEXT: - MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); - Selection.setSelection((Spannable) mText, getSelectionEnd()); + stopTextSelectionMode(); return true; - case ID_CUT: - MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); - - if (min == max) { - min = 0; - max = mText.length(); - } - + case ID_CUT: clip.setText(mTransformed.subSequence(min, max)); ((Editable) mText).delete(min, max); + stopTextSelectionMode(); return true; case ID_COPY: - MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); - - if (min == max) { - min = 0; - max = mText.length(); - } - clip.setText(mTransformed.subSequence(min, max)); + stopTextSelectionMode(); return true; case ID_PASTE: - MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); - CharSequence paste = clip.getText(); if (paste != null) { Selection.setSelection((Spannable) mText, max); ((Editable) mText).replace(min, max, paste); + stopTextSelectionMode(); } - return true; case ID_COPY_URL: - MetaKeyKeyListener.stopSelecting(this, (Spannable) mText); - - URLSpan[] urls = ((Spanned) mText).getSpans(min, max, - URLSpan.class); + URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class); if (urls.length == 1) { clip.setText(urls[0].getURL()); } - return true; case ID_SWITCH_INPUT_METHOD: @@ -7279,13 +7393,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(i); } - return true; } return false; } + @Override public boolean performLongClick() { if (super.performLongClick()) { mEatTouchRelease = true; @@ -7295,7 +7409,572 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - @ViewDebug.ExportedProperty(category = "text") + private void startTextSelectionMode() { + if (mSelectionModifierCursorController == null) { + Log.w(LOG_TAG, "TextView has no selection controller. Action mode cancelled."); + return; + } + + if (!requestFocus()) { + return; + } + + selectCurrentWord(); + mSelectionModifierCursorController.show(); + mIsInTextSelectionMode = true; + } + + /** + * Same as {@link #stopTextSelectionMode()}, except that there is no cursor controller + * fade out animation. Needed since the drawable and their alpha values are shared by all + * TextViews. Switching from one TextView to another would fade the cursor controllers in the + * new one otherwise. + */ + private void terminateTextSelectionMode() { + stopTextSelectionMode(); + if (mSelectionModifierCursorController != null) { + SelectionModifierCursorController selectionModifierCursorController = + (SelectionModifierCursorController) mSelectionModifierCursorController; + selectionModifierCursorController.cancelFadeOutAnimation(); + } + } + + private void stopTextSelectionMode() { + if (mIsInTextSelectionMode) { + Selection.setSelection((Spannable) mText, getSelectionEnd()); + if (mSelectionModifierCursorController != null) { + mSelectionModifierCursorController.hide(); + } + + mIsInTextSelectionMode = false; + } + } + + /** + * A CursorController instance can be used to control a cursor in the text. + * + * It can be passed to an {@link ArrowKeyMovementMethod} which can intercepts events + * and send them to this object instead of the cursor. + */ + public interface CursorController { + /* Cursor fade-out animation duration, in milliseconds. */ + static final int FADE_OUT_DURATION = 400; + + /** + * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. + * See also {@link #hide()}. + */ + public void show(); + + /** + * Hide the cursor controller from screen. + * See also {@link #show()}. + */ + public void hide(); + + /** + * Update the controller's position. + */ + public void updatePosition(int offset); + + /** + * The controller and the cursor's positions can be link by a fixed offset, + * computed when the controller is touched, and then maintained as it moves + * @return Horizontal offset between the controller and the cursor. + */ + public float getOffsetX(); + + /** + * @return Vertical offset between the controller and the cursor. + */ + public float getOffsetY(); + + /** + * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller + * a chance to become active and/or visible. + * @param event The touch event + */ + public void onTouchEvent(MotionEvent event); + + /** + * Draws a visual representation of the controller on the canvas. + * + * Called at the end of {@link #draw(Canvas)}, in the content coordinates system. + * @param canvas The Canvas used by this TextView. + */ + public void draw(Canvas canvas); + } + + private class Handle { + Drawable mDrawable; + // Vertical extension of the touch region + int mTopExtension, mBottomExtension; + // Position of the virtual finger position on screen + int mHopSpotVertcalPosition; + + Handle(Drawable drawable) { + mDrawable = drawable; + } + + void positionAtCursor(final int offset, boolean bottom) { + final int drawableWidth = mDrawable.getIntrinsicWidth(); + final int drawableHeight = mDrawable.getIntrinsicHeight(); + final int line = mLayout.getLineForOffset(offset); + final int lineTop = mLayout.getLineTop(line); + final int lineBottom = mLayout.getLineBottom(line); + + mHopSpotVertcalPosition = lineTop + (bottom ? (3 * (lineBottom - lineTop)) / 4 : + (lineBottom - lineTop) / 4); + + final Rect bounds = sCursorControllerTempRect; + bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - drawableWidth / 2.0); + bounds.top = (bottom ? lineBottom : lineTop) - drawableHeight / 2; + + mTopExtension = bottom ? 0 : drawableHeight / 2; + mBottomExtension = drawableHeight; + + // Extend touch region up when editing the last line of text (or a single line) so that + // it is easier to grab. + if (line == mLayout.getLineCount() - 1) { + mTopExtension = (lineBottom - lineTop) - drawableHeight / 2; + } + + bounds.right = bounds.left + drawableWidth; + bounds.bottom = bounds.top + drawableHeight; + + int boundTopBefore = bounds.top; + convertFromViewportToContentCoordinates(bounds); + mHopSpotVertcalPosition += bounds.top - boundTopBefore; + mDrawable.setBounds(bounds); + postInvalidate(); + } + + boolean hasFingerOn(float x, float y) { + // Simulate a 'fat finger' to ease grabbing of the controller. + // Expands according to controller image size instead of using dip distance. + // Assumes controller imager has a sensible size, proportionnal to screen density. + final int drawableWidth = mDrawable.getIntrinsicWidth(); + final Rect fingerRect = sCursorControllerTempRect; + fingerRect.set((int) (x - drawableWidth / 2.0), + (int) (y - mBottomExtension), + (int) (x + drawableWidth / 2.0), + (int) (y + mTopExtension)); + return Rect.intersects(mDrawable.getBounds(), fingerRect); + } + + void postInvalidate() { + final Rect bounds = mDrawable.getBounds(); + TextView.this.postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + void postInvalidateDelayed(long delay) { + final Rect bounds = mDrawable.getBounds(); + TextView.this.postInvalidateDelayed(delay, bounds.left, bounds.top, + bounds.right, bounds.bottom); + } + } + + class InsertionPointCursorController implements CursorController { + private static final int DELAY_BEFORE_FADE_OUT = 2100; + + // Whether or not the cursor control is currently visible + private boolean mIsVisible = false; + // Starting time of the fade timer + private long mFadeOutTimerStart; + // The cursor controller image + private final Handle mHandle; + // Used to detect a tap (vs drag) on the controller + private long mOnDownTimerStart; + // Offset between finger hot point on cursor controller and actual cursor + private float mOffsetX, mOffsetY; + + InsertionPointCursorController() { + Resources res = mContext.getResources(); + mHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); + } + + public void show() { + updateDrawablePosition(); + // Has to be done after updatePosition, so that previous position invalidate + // in only done if necessary. + mIsVisible = true; + } + + public void hide() { + if (mIsVisible) { + long time = System.currentTimeMillis(); + // Start fading out, only if not already in progress + if (time - mFadeOutTimerStart < DELAY_BEFORE_FADE_OUT) { + mFadeOutTimerStart = time - DELAY_BEFORE_FADE_OUT; + mHandle.postInvalidate(); + } + } + } + + public void draw(Canvas canvas) { + if (mIsVisible) { + int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); + if (time <= DELAY_BEFORE_FADE_OUT) { + mHandle.postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time); + } else { + time -= DELAY_BEFORE_FADE_OUT; + if (time <= FADE_OUT_DURATION) { + final int alpha = (int) + ((255.0 * (FADE_OUT_DURATION - time)) / FADE_OUT_DURATION); + mHandle.mDrawable.setAlpha(alpha); + mHandle.postInvalidateDelayed(30); + } else { + mHandle.mDrawable.setAlpha(0); + mIsVisible = false; + } + } + mHandle.mDrawable.draw(canvas); + } + } + + public void updatePosition(int offset) { + if (offset == getSelectionStart()) { + return; // No change, no need to redraw + } + Selection.setSelection((Spannable) mText, offset); + updateDrawablePosition(); + } + + private void updateDrawablePosition() { + if (mIsVisible) { + // Clear previous cursor controller before bounds are updated + mHandle.postInvalidate(); + } + + final int offset = getSelectionStart(); + + if (offset < 0) { + // Should never happen, safety check. + Log.w(LOG_TAG, "Update cursor controller position called with no cursor"); + mIsVisible = false; + return; + } + + mHandle.positionAtCursor(offset, true); + + mFadeOutTimerStart = System.currentTimeMillis(); + mHandle.mDrawable.setAlpha(255); + } + + public void onTouchEvent(MotionEvent event) { + if (isFocused() && isTextEditable() && mIsVisible) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN : { + final float x = event.getX(); + final float y = event.getY(); + + if (mHandle.hasFingerOn(x, y)) { + show(); + + if (mMovement instanceof ArrowKeyMovementMethod) { + ((ArrowKeyMovementMethod)mMovement).setCursorController(this); + } + + if (mParent != null) { + // Prevent possible scrollView parent from scrolling, so that + // we can use auto-scrolling. + mParent.requestDisallowInterceptTouchEvent(true); + } + + final Rect bounds = mHandle.mDrawable.getBounds(); + mOffsetX = (bounds.left + bounds.right) / 2.0f - x; + mOffsetY = mHandle.mHopSpotVertcalPosition - y; + + mOnDownTimerStart = event.getEventTime(); + } + break; + } + + case MotionEvent.ACTION_UP : { + int time = (int) (event.getEventTime() - mOnDownTimerStart); + + if (time <= ViewConfiguration.getTapTimeout()) { + // A tap on the controller (not a drag) will move the cursor + int offset = getOffset((int) event.getX(), (int) event.getY()); + Selection.setSelection((Spannable) mText, offset); + + // Modified by cancelLongPress and prevents the cursor from changing + mScrolled = false; + } + break; + } + } + } + } + + public float getOffsetX() { + return mOffsetX; + } + + public float getOffsetY() { + return mOffsetY; + } + } + + class SelectionModifierCursorController implements CursorController { + // Whether or not the selection controls are currently visible + private boolean mIsVisible = false; + // Whether that start or the end of selection controller is dragged + private boolean mStartIsDragged = false; + // Starting time of the fade timer + private long mFadeOutTimerStart; + // The cursor controller images + private final Handle mStartHandle, mEndHandle; + // Offset between finger hot point on active cursor controller and actual cursor + private float mOffsetX, mOffsetY; + // The offsets of that last touch down event. Remembered to start selection there. + private int mMinTouchOffset, mMaxTouchOffset; + + SelectionModifierCursorController() { + Resources res = mContext.getResources(); + mStartHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); + mEndHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle)); + } + + public void show() { + updateDrawablesPositions(); + // Has to be done after updatePosition, so that previous position invalidate + // in only done if necessary. + mIsVisible = true; + mFadeOutTimerStart = -1; + hideInsertionPointCursorController(); + } + + public void hide() { + if (mIsVisible && (mFadeOutTimerStart < 0)) { + mFadeOutTimerStart = System.currentTimeMillis(); + mStartHandle.postInvalidate(); + mEndHandle.postInvalidate(); + } + } + + public void cancelFadeOutAnimation() { + mIsVisible = false; + mStartHandle.postInvalidate(); + mEndHandle.postInvalidate(); + } + + public void draw(Canvas canvas) { + if (mIsVisible) { + if (mFadeOutTimerStart >= 0) { + int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart); + if (time <= FADE_OUT_DURATION) { + final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION; + mStartHandle.mDrawable.setAlpha(alpha); + mEndHandle.mDrawable.setAlpha(alpha); + mStartHandle.postInvalidateDelayed(30); + mEndHandle.postInvalidateDelayed(30); + } else { + mStartHandle.mDrawable.setAlpha(0); + mEndHandle.mDrawable.setAlpha(0); + mIsVisible = false; + } + } + mStartHandle.mDrawable.draw(canvas); + mEndHandle.mDrawable.draw(canvas); + } + } + + public void updatePosition(int offset) { + int selectionStart = getSelectionStart(); + int selectionEnd = getSelectionEnd(); + + // Handle the case where start and end are swapped, making sure start <= end + if (mStartIsDragged) { + if (offset <= selectionEnd) { + if (selectionStart == offset) { + return; // no change, no need to redraw; + } + selectionStart = offset; + } else { + selectionStart = selectionEnd; + selectionEnd = offset; + mStartIsDragged = false; + } + } else { + if (offset >= selectionStart) { + if (selectionEnd == offset) { + return; // no change, no need to redraw; + } + selectionEnd = offset; + } else { + selectionEnd = selectionStart; + selectionStart = offset; + mStartIsDragged = true; + } + } + + Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); + updateDrawablesPositions(); + } + + private void updateDrawablesPositions() { + if (mIsVisible) { + // Clear previous cursor controller before bounds are updated + mStartHandle.postInvalidate(); + mEndHandle.postInvalidate(); + } + + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + if ((selectionStart < 0) || (selectionEnd < 0)) { + // Should never happen, safety check. + Log.w(LOG_TAG, "Update selection controller position called with no cursor"); + mIsVisible = false; + return; + } + + boolean oneLineSelection = mLayout.getLineForOffset(selectionStart) == mLayout.getLineForOffset(selectionEnd); + mStartHandle.positionAtCursor(selectionStart, oneLineSelection); + mEndHandle.positionAtCursor(selectionEnd, true); + + mStartHandle.mDrawable.setAlpha(255); + mEndHandle.mDrawable.setAlpha(255); + } + + public void onTouchEvent(MotionEvent event) { + if (isTextEditable()) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + final int x = (int) event.getX(); + final int y = (int) event.getY(); + + // Remember finger down position, to be able to start selection from there + mMinTouchOffset = mMaxTouchOffset = getOffset(x, y); + + if (mIsVisible) { + if (mMovement instanceof ArrowKeyMovementMethod) { + boolean isOnStart = mStartHandle.hasFingerOn(x, y); + boolean isOnEnd = mEndHandle.hasFingerOn(x, y); + if (isOnStart || isOnEnd) { + if (mParent != null) { + // Prevent possible scrollView parent from scrolling, so + // that we can use auto-scrolling. + mParent.requestDisallowInterceptTouchEvent(true); + } + + // In case both controllers are under finger (very small + // selection region), arbitrarily pick end controller. + mStartIsDragged = !isOnEnd; + final Handle draggedHandle = mStartIsDragged ? mStartHandle : mEndHandle; + final Rect bounds = draggedHandle.mDrawable.getBounds(); + mOffsetX = (bounds.left + bounds.right) / 2.0f - x; + mOffsetY = draggedHandle.mHopSpotVertcalPosition - y; + + ((ArrowKeyMovementMethod)mMovement).setCursorController(this); + } + } + } + break; + + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_POINTER_UP: + // Handle multi-point gestures. Keep min and max offset positions. + // Only activated for devices that correctly handle multi-touch. + if (mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { + updateMinAndMaxOffsets(event); + } + break; + } + } + } + + /** + * @param event + */ + private void updateMinAndMaxOffsets(MotionEvent event) { + int pointerCount = event.getPointerCount(); + for (int index = 0; index < pointerCount; index++) { + final int x = (int) event.getX(index); + final int y = (int) event.getY(index); + int offset = getOffset(x, y); + if (offset < mMinTouchOffset) mMinTouchOffset = offset; + if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; + } + } + + public int getMinTouchOffset() { + return mMinTouchOffset; + } + + public int getMaxTouchOffset() { + return mMaxTouchOffset; + } + + public float getOffsetX() { + return mOffsetX; + } + + public float getOffsetY() { + return mOffsetY; + } + + /** + * @return true iff this controller is currently used to move the selection start. + */ + public boolean isSelectionStartDragged() { + return mIsVisible && mStartIsDragged; + } + } + + private void hideInsertionPointCursorController() { + if (mInsertionPointCursorController != null) { + mInsertionPointCursorController.hide(); + } + } + + private void hideControllers() { + hideInsertionPointCursorController(); + stopTextSelectionMode(); + } + + /** + * Get the offset character closest to the specified absolute position. + * + * @param x The horizontal absolute position of a point on screen + * @param y The vertical absolute position of a point on screen + * @return the character offset for the character whose position is closest to the specified + * position. Returns -1 if there is no layout. + * + * @hide + */ + public int getOffset(int x, int y) { + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + + // Clamp the position to inside of the view. + if (x < 0) { + x = 0; + } else if (x >= (getWidth() - getTotalPaddingRight())) { + x = getWidth()-getTotalPaddingRight() - 1; + } + if (y < 0) { + y = 0; + } else if (y >= (getHeight() - getTotalPaddingBottom())) { + y = getHeight()-getTotalPaddingBottom() - 1; + } + + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + if (layout != null) { + final int line = layout.getLineForVertical(y); + final int offset = layout.getOffsetForHorizontal(line, x); + return offset; + } else { + return -1; + } + } + + @ViewDebug.ExportedProperty private CharSequence mText; private CharSequence mTransformed; private BufferType mBufferType = BufferType.NORMAL; @@ -7313,16 +7992,25 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private ArrayList mListeners = null; // display attributes - private TextPaint mTextPaint; + private final TextPaint mTextPaint; private boolean mUserSetTextScaleX; - private Paint mHighlightPaint; - private int mHighlightColor = 0xFFBBDDFF; + private final Paint mHighlightPaint; + private int mHighlightColor = 0xCC475925; private Layout mLayout; private long mShowCursor; private Blink mBlink; private boolean mCursorVisible = true; + // Cursor Controllers. Null when disabled. + private CursorController mInsertionPointCursorController; + private CursorController mSelectionModifierCursorController; + private boolean mShouldStartTextSelectionMode = false; + private boolean mIsInTextSelectionMode = false; + // Created once and shared by different CursorController helper methods. + // Only one cursor controller is active at any time which prevent race conditions. + private static Rect sCursorControllerTempRect = new Rect(); + private boolean mSelectAllOnFocus = false; private int mGravity = Gravity.TOP | Gravity.LEFT; diff --git a/core/res/res/drawable-hdpi/text_select_handle.png b/core/res/res/drawable-hdpi/text_select_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..80d48ab2ca1fc963ad497e10012790aa28c80816 GIT binary patch literal 11672 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"Označit text" "Zastavit označování textu" "Vyjmout" - "Vyjmout vše" "Kopírovat" - "Kopírovat vše" "Vložit" "Kopírovat adresu URL" "Metoda zadávání dat" diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml index 3ea8266194579..e7e8019f421d5 100644 --- a/core/res/res/values-da/strings.xml +++ b/core/res/res/values-da/strings.xml @@ -706,9 +706,7 @@ "Marker tekst" "Hold op med at markere tekst" "Klip" - "Klip alle" "Kopier" - "Kopier alle" "Indsæt" "Kopier webadresse" "Inputmetode" diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml index bf3efd604ea86..edbe4c41dcf4b 100644 --- a/core/res/res/values-de/strings.xml +++ b/core/res/res/values-de/strings.xml @@ -706,9 +706,7 @@ "Text auswählen" "Textauswahl beenden" "Ausschneiden" - "Alles ausschneiden" "Kopieren" - "Alles kopieren" "Einfügen" "URL kopieren" "Eingabemethode" diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml index 2c61e1f08f75c..cfa36d2f03e0c 100644 --- a/core/res/res/values-el/strings.xml +++ b/core/res/res/values-el/strings.xml @@ -706,9 +706,7 @@ "Επιλογή κειμένου" "Διακοπή επιλογής κειμένου" "Αποκοπή" - "Αποκοπή όλων" "Αντιγραφή" - "Αντιγραφή όλων" "Επικόλληση" "Αντιγραφή διεύθυνσης URL" "Μέθοδος εισόδου" diff --git a/core/res/res/values-en-rAU/strings.xml b/core/res/res/values-en-rAU/strings.xml index 3de378bee2113..1cbfbbaad9e99 100644 --- a/core/res/res/values-en-rAU/strings.xml +++ b/core/res/res/values-en-rAU/strings.xml @@ -1079,11 +1079,9 @@ - - diff --git a/core/res/res/values-en-rSG/strings.xml b/core/res/res/values-en-rSG/strings.xml index 2ec6b0b02c48b..09a84906c0ee2 100644 --- a/core/res/res/values-en-rSG/strings.xml +++ b/core/res/res/values-en-rSG/strings.xml @@ -1074,11 +1074,9 @@ - - diff --git a/core/res/res/values-en-rUS/strings.xml b/core/res/res/values-en-rUS/strings.xml index 05f30fca350f9..fdc0d69615697 100644 --- a/core/res/res/values-en-rUS/strings.xml +++ b/core/res/res/values-en-rUS/strings.xml @@ -1115,11 +1115,9 @@ - - diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml index afc2862e4aad5..dbf32b63e1b1b 100644 --- a/core/res/res/values-es-rUS/strings.xml +++ b/core/res/res/values-es-rUS/strings.xml @@ -706,9 +706,7 @@ "Seleccionar texto" "Detener la selección de texto" "Cortar" - "Cortar llamada" "Copiar" - "Copiar todo" "Pegar" "Copiar URL" "Método de entrada" diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml index 045b6a1776844..425fca3306402 100644 --- a/core/res/res/values-es/strings.xml +++ b/core/res/res/values-es/strings.xml @@ -706,9 +706,7 @@ "Seleccionar texto" "Detener selección de texto" "Cortar" - "Cortar todo" "Copiar" - "Copiar todo" "Pegar" "Copiar URL" "Método de introducción de texto" diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml index 3ab781434c5b3..2dacb3045f3ef 100644 --- a/core/res/res/values-fr/strings.xml +++ b/core/res/res/values-fr/strings.xml @@ -706,9 +706,7 @@ "Sélectionner le texte" "Arrêter sélection de texte" "Couper" - "Tout couper" "Copier" - "Tout copier" "Coller" "Copier l\'URL" "Mode de saisie" diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml index 6e50d21b15e23..c7aac82e63659 100644 --- a/core/res/res/values-it/strings.xml +++ b/core/res/res/values-it/strings.xml @@ -706,9 +706,7 @@ "Seleziona testo" "Termina selezione testo" "Taglia" - "Taglia tutto" "Copia" - "Copia tutto" "Incolla" "Copia URL" "Metodo inserimento" diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml index f1e98da8b6afd..25e977dde8dc9 100644 --- a/core/res/res/values-ja/strings.xml +++ b/core/res/res/values-ja/strings.xml @@ -706,9 +706,7 @@ "テキストを選択" "テキストの選択を終了" "切り取り" - "すべて切り取り" "コピー" - "すべてコピー" "貼り付け" "URLをコピー" "入力方法" diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml index 712100e167204..faf589e270455 100644 --- a/core/res/res/values-ko/strings.xml +++ b/core/res/res/values-ko/strings.xml @@ -706,9 +706,7 @@ "텍스트 선택" "텍스트 선택 중지" "잘라내기" - "모두 잘라내기" "복사" - "모두 복사" "붙여넣기" "URL 복사" "입력 방법" diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml index f09039d159211..2c23d53075e20 100644 --- a/core/res/res/values-nb/strings.xml +++ b/core/res/res/values-nb/strings.xml @@ -706,9 +706,7 @@ "Merk tekst" "Slutt å merke tekst" "Klipp ut" - "Klipp ut alt" "Kopier" - "Kopier alt" "Lim inn" "Kopier URL" "Inndatametode" diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml index 17a2fe458fc1a..10a8e749e30f0 100644 --- a/core/res/res/values-nl/strings.xml +++ b/core/res/res/values-nl/strings.xml @@ -706,9 +706,7 @@ "Tekst selecteren" "Stoppen met tekst selecteren" "Knippen" - "Alles knippen" "Kopiëren" - "Alles kopiëren" "Plakken" "URL kopiëren" "Invoermethode" diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml index 87e045df33791..70c3170b4d9d4 100644 --- a/core/res/res/values-pl/strings.xml +++ b/core/res/res/values-pl/strings.xml @@ -706,9 +706,7 @@ "Zaznacz tekst" "Zatrzymaj wybieranie tekstu" "Wytnij" - "Wytnij wszystko" "Kopiuj" - "Kopiuj wszystko" "Wklej" "Kopiuj adres URL" "Metoda wprowadzania" diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml index 3fb8d552077a1..9a8c6e9556a36 100644 --- a/core/res/res/values-pt-rPT/strings.xml +++ b/core/res/res/values-pt-rPT/strings.xml @@ -706,9 +706,7 @@ "Seleccionar texto" "Parar selecção de texto" "Cortar" - "Cortar tudo" "Copiar" - "Copiar tudo" "Colar" "Copiar URL" "Método de entrada" diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml index 81ceaf3bc7361..211953951cfcd 100644 --- a/core/res/res/values-pt/strings.xml +++ b/core/res/res/values-pt/strings.xml @@ -706,9 +706,7 @@ "Selecionar texto" "Parar seleção de texto" "Recortar" - "Recortar tudo" "Copiar" - "Copiar tudo" "Colar" "Copiar URL" "Método de entrada" diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml index 4176886adbed5..a99107ede5daf 100644 --- a/core/res/res/values-ru/strings.xml +++ b/core/res/res/values-ru/strings.xml @@ -706,9 +706,7 @@ "Выбрать текст" "Остановить выделение текста" "Вырезать" - "Вырезать все" "Копировать" - "Копировать все" "Вставить" "Копировать URL" "Способ ввода" diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml index 4c78f4f30000b..b3e415d50fdf5 100644 --- a/core/res/res/values-sv/strings.xml +++ b/core/res/res/values-sv/strings.xml @@ -706,9 +706,7 @@ "Markera text" "Sluta välja text" "Klipp ut" - "Klipp ut alla" "Kopiera" - "Kopiera alla" "Klistra in" "Kopiera webbadress" "Indatametod" diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml index f2fd734548555..a6bc41e04bcf1 100644 --- a/core/res/res/values-tr/strings.xml +++ b/core/res/res/values-tr/strings.xml @@ -706,9 +706,7 @@ "Metin seç" "Metin seçmeyi durdur" "Kes" - "Tümünü kes" "Kopyala" - "Tümünü kopyala" "Yapıştır" "URL\'yi kopyala" "Giriş yöntemi" diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml index a0fb721536e5c..aa3f12d2d9149 100644 --- a/core/res/res/values-zh-rCN/strings.xml +++ b/core/res/res/values-zh-rCN/strings.xml @@ -706,9 +706,7 @@ "选择文字" "停止选择文字" "剪切" - "全部剪切" "复制" - "全部复制" "粘贴" "复制网址" "输入法" diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml index d5dd857787710..5bf0342ca28ec 100644 --- a/core/res/res/values-zh-rTW/strings.xml +++ b/core/res/res/values-zh-rTW/strings.xml @@ -706,9 +706,7 @@ "選取文字" "停止選取文字" "剪下" - "全部剪下" "複製" - "全部複製" "貼上" "複製網址" "輸入方式" diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index bd65fee453325..1df424bcdcb31 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1245,7 +1245,7 @@ @@ -1273,5 +1273,4 @@ - diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 62fd169e65e31..52abe45e88df8 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1854,23 +1854,17 @@ Select all - Select text + Select word - + Stop selecting text Cut - - Cut all - Copy - - Copy all - Paste diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java index 98ffb8b4318ba..78302244e1c1b 100644 --- a/graphics/java/android/graphics/Rect.java +++ b/graphics/java/android/graphics/Rect.java @@ -75,6 +75,7 @@ public final class Rect implements Parcelable { bottom = r.bottom; } + @Override public boolean equals(Object obj) { Rect r = (Rect) obj; if (r != null) { @@ -84,6 +85,7 @@ public final class Rect implements Parcelable { return false; } + @Override public String toString() { StringBuilder sb = new StringBuilder(32); sb.append("Rect("); sb.append(left); sb.append(", "); @@ -351,7 +353,7 @@ public final class Rect implements Parcelable { * rectangle, return true and set this rectangle to that intersection, * otherwise return false and do not change this rectangle. No check is * performed to see if either rectangle is empty. Note: To just test for - * intersection, use intersects() + * intersection, use {@link #intersects(Rect, Rect)}. * * @param left The left side of the rectangle being intersected with this * rectangle @@ -445,7 +447,7 @@ public final class Rect implements Parcelable { /** * Returns true iff the two specified rectangles intersect. In no event are * either of the rectangles modified. To record the intersection, - * use intersect() or setIntersect(). + * use {@link #intersect(Rect)} or {@link #setIntersect(Rect, Rect)}. * * @param a The first rectangle being tested for intersection * @param b The second rectangle being tested for intersection