diff options
author | Gilles Debunne <debunne@google.com> | 2010-08-19 17:59:08 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2010-08-19 17:59:08 -0700 |
commit | d018a0ce72124f668d859b19fe3e73f5637d3c7c (patch) | |
tree | 48f1c45f7207fd3af0358e62b4b900ac47e09b6c /core | |
parent | 70c9ffbc838271f0ea27a4780eb146287de53ef6 (diff) | |
parent | b0d6ba1ec4f71b96cab7d1ff62b846d5cf162c4f (diff) | |
download | frameworks_base-d018a0ce72124f668d859b19fe3e73f5637d3c7c.zip frameworks_base-d018a0ce72124f668d859b19fe3e73f5637d3c7c.tar.gz frameworks_base-d018a0ce72124f668d859b19fe3e73f5637d3c7c.tar.bz2 |
Merge "Text selection without trackball." into gingerbread
Diffstat (limited to 'core')
34 files changed, 1047 insertions, 588 deletions
diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java index bb98bce..13cb5e6 100644 --- a/core/java/android/text/Selection.java +++ b/core/java/android/text/Selection.java @@ -417,8 +417,8 @@ public class Selection { } } - private static final class START implements NoCopySpan { }; - private static final class END implements NoCopySpan { }; + private static final class START implements NoCopySpan { } + private static final class END implements NoCopySpan { } /* * Public constants diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index 9af42cc..79a0c37 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -16,30 +16,38 @@ package android.text.method; -import android.util.Log; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; import android.view.KeyEvent; -import android.graphics.Rect; -import android.text.*; -import android.widget.TextView; -import android.view.View; -import android.view.ViewConfiguration; import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; +import android.widget.TextView.CursorController; // XXX this doesn't extend MetaKeyKeyListener because the signatures // don't match. Need to figure that out. Meanwhile the meta keys // won't work in fields that don't take input. -public class -ArrowKeyMovementMethod -implements MovementMethod -{ +public class ArrowKeyMovementMethod implements MovementMethod { + /** + * An optional controller for the cursor. + * Use {@link #setCursorController(CursorController)} to set this field. + */ + protected CursorController mCursorController; + + private boolean isCap(Spannable buffer) { + return ((MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_SHIFT_ON) == 1) || + (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0)); + } + + private boolean isAlt(Spannable buffer) { + return MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_ALT_ON) == 1; + } + private boolean up(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -60,12 +68,8 @@ implements MovementMethod } private boolean down(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -86,12 +90,8 @@ implements MovementMethod } private boolean left(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -110,12 +110,8 @@ implements MovementMethod } private boolean right(TextView widget, Spannable buffer) { - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_SHIFT_ON) == 1) || - (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0); - boolean alt = MetaKeyKeyListener.getMetaState(buffer, - KeyEvent.META_ALT_ON) == 1; + boolean cap = isCap(buffer); + boolean alt = isAlt(buffer); Layout layout = widget.getLayout(); if (cap) { @@ -133,35 +129,6 @@ implements MovementMethod } } - private int getOffset(int x, int y, TextView widget){ - // Converts the absolute X,Y coordinates to the character offset for the - // character whose position is closest to the specified - // horizontal position. - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); - - // Clamp the position to inside of the view. - if (x < 0) { - x = 0; - } else if (x >= (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); - - 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); - } + int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + + 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(); - } 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)); + Selection.extendSelection(buffer, offset); } 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; - } - - /* 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; - } - - 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; - } + private boolean onTouchEventCursor(TextView widget, Spannable buffer, MotionEvent event) { + if (widget.isFocused() && !widget.didTouchFocusSelect()) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_MOVE: + widget.cancelLongPress(); - return start == findWordStart(text, two) && - end == findWordEnd(text, two); - } + // 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; - // 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 42ad10e..a19a78e 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 840d7c1..6ad33dd 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<AbsSavedState>() { 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 47061c1..1328525 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 b8623e7..c9662ff 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 27e0e94..73b1d6d 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<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { 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<TextView> 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<TextView>(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<TextView> mView; + private final WeakReference<TextView> 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 <code>offset</code>. + * 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; + // 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; } - if (end - start > 48) { - return null; + 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); } - return TextUtils.substring(mTransformed, start, end); - } + 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; - - menu.add(0, ID_COPY, 0, name). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('c'); - menu.setHeaderTitle(com.android.internal.R.string. - editTextMenuTitle); - } + if (mIsInTextSelectionMode) { + MenuHandler handler = new MenuHandler(); + + if (canCut()) { + menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('x'); } - return; - } + if (canCopy()) { + menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('c'); + } - MenuHandler handler = new MenuHandler(); + if (canPaste()) { + menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('v'); + } - if (canSelectAll()) { - menu.add(0, ID_SELECT_ALL, 0, - com.android.internal.R.string.selectAll). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('a'); + 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); + } + } - boolean selection = getSelectionStart() != getSelectionEnd(); + //return; + } + */ + MenuHandler handler = new MenuHandler(); - 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); + if (canSelectText()) { + menu.add(0, ID_START_SELECTING_TEXT, 0, com.android.internal.R.string.selectText). + setOnMenuItemClickListener(handler); added = true; - } else { - menu.add(0, ID_START_SELECTING_TEXT, 0, - com.android.internal.R.string.selectText). - setOnMenuItemClickListener(handler); + } + + if (canSelectAll()) { + menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). + setOnMenuItemClickListener(handler). + setAlphabeticShortcut('a'); added = true; } - } - if (canCut()) { - int name; - if (selection) { - name = com.android.internal.R.string.cut; - } else { - name = com.android.internal.R.string.cutAll; - } + if (mText instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); - menu.add(0, ID_CUT, 0, name). - setOnMenuItemClickListener(handler). - setAlphabeticShortcut('x'); - added = true; - } + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); - if (canCopy()) { - int name; - if (selection) { - name = com.android.internal.R.string.copy; - } else { - name = com.android.internal.R.string.copyAll; + 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'); } - 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); + if (isInputMethodTarget()) { + menu.add(1, ID_SWITCH_INPUT_METHOD, 0, com.android.internal.R.string.inputMethod). + 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(); - - if (!isFocused()) { - selStart = 0; - selEnd = mText.length(); - } + int min = 0; + int max = mText.length(); - int min = Math.min(selStart, selEnd); - int max = Math.max(selStart, selEnd); + if (isFocused()) { + final int selStart = getSelectionStart(); + final int selEnd = getSelectionEnd(); - 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<TextWatcher> 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 Binary files differnew file mode 100644 index 0000000..80d48ab --- /dev/null +++ b/core/res/res/drawable-hdpi/text_select_handle.png diff --git a/core/res/res/drawable-mdpi/text_select_handle.png b/core/res/res/drawable-mdpi/text_select_handle.png Binary files differnew file mode 100644 index 0000000..93a5a15 --- /dev/null +++ b/core/res/res/drawable-mdpi/text_select_handle.png diff --git a/core/res/res/values-cs/strings.xml b/core/res/res/values-cs/strings.xml index c442fee..245643b 100644 --- a/core/res/res/values-cs/strings.xml +++ b/core/res/res/values-cs/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Označit text"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Zastavit označování textu"</string> <string name="cut" msgid="3092569408438626261">"Vyjmout"</string> - <string name="cutAll" msgid="2436383270024931639">"Vyjmout vše"</string> <string name="copy" msgid="2681946229533511987">"Kopírovat"</string> - <string name="copyAll" msgid="2590829068100113057">"Kopírovat vše"</string> <string name="paste" msgid="5629880836805036433">"Vložit"</string> <string name="copyUrl" msgid="2538211579596067402">"Kopírovat adresu URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Metoda zadávání dat"</string> diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml index 3ea8266..e7e8019 100644 --- a/core/res/res/values-da/strings.xml +++ b/core/res/res/values-da/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Marker tekst"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Hold op med at markere tekst"</string> <string name="cut" msgid="3092569408438626261">"Klip"</string> - <string name="cutAll" msgid="2436383270024931639">"Klip alle"</string> <string name="copy" msgid="2681946229533511987">"Kopier"</string> - <string name="copyAll" msgid="2590829068100113057">"Kopier alle"</string> <string name="paste" msgid="5629880836805036433">"Indsæt"</string> <string name="copyUrl" msgid="2538211579596067402">"Kopier webadresse"</string> <string name="inputMethod" msgid="1653630062304567879">"Inputmetode"</string> diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml index bf3efd6..edbe4c4 100644 --- a/core/res/res/values-de/strings.xml +++ b/core/res/res/values-de/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Text auswählen"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Textauswahl beenden"</string> <string name="cut" msgid="3092569408438626261">"Ausschneiden"</string> - <string name="cutAll" msgid="2436383270024931639">"Alles ausschneiden"</string> <string name="copy" msgid="2681946229533511987">"Kopieren"</string> - <string name="copyAll" msgid="2590829068100113057">"Alles kopieren"</string> <string name="paste" msgid="5629880836805036433">"Einfügen"</string> <string name="copyUrl" msgid="2538211579596067402">"URL kopieren"</string> <string name="inputMethod" msgid="1653630062304567879">"Eingabemethode"</string> diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml index 2c61e1f..cfa36d2 100644 --- a/core/res/res/values-el/strings.xml +++ b/core/res/res/values-el/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Επιλογή κειμένου"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Διακοπή επιλογής κειμένου"</string> <string name="cut" msgid="3092569408438626261">"Αποκοπή"</string> - <string name="cutAll" msgid="2436383270024931639">"Αποκοπή όλων"</string> <string name="copy" msgid="2681946229533511987">"Αντιγραφή"</string> - <string name="copyAll" msgid="2590829068100113057">"Αντιγραφή όλων"</string> <string name="paste" msgid="5629880836805036433">"Επικόλληση"</string> <string name="copyUrl" msgid="2538211579596067402">"Αντιγραφή διεύθυνσης URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Μέθοδος εισόδου"</string> diff --git a/core/res/res/values-en-rAU/strings.xml b/core/res/res/values-en-rAU/strings.xml index 3de378b..1cbfbba 100644 --- a/core/res/res/values-en-rAU/strings.xml +++ b/core/res/res/values-en-rAU/strings.xml @@ -1079,11 +1079,9 @@ <skip /> <!-- no translation found for cut (5845613239192595662) --> <skip /> - <!-- no translation found for cutAll (4474519683293791451) --> <skip /> <!-- no translation found for copy (8603721575469529820) --> <skip /> - <!-- no translation found for copyAll (4777548804630476932) --> <skip /> <!-- no translation found for paste (6458036735811828538) --> <skip /> diff --git a/core/res/res/values-en-rSG/strings.xml b/core/res/res/values-en-rSG/strings.xml index 2ec6b0b..09a8490 100644 --- a/core/res/res/values-en-rSG/strings.xml +++ b/core/res/res/values-en-rSG/strings.xml @@ -1074,11 +1074,9 @@ <skip /> <!-- no translation found for cut (5845613239192595662) --> <skip /> - <!-- no translation found for cutAll (4474519683293791451) --> <skip /> <!-- no translation found for copy (8603721575469529820) --> <skip /> - <!-- no translation found for copyAll (4777548804630476932) --> <skip /> <!-- no translation found for paste (6458036735811828538) --> <skip /> diff --git a/core/res/res/values-en-rUS/strings.xml b/core/res/res/values-en-rUS/strings.xml index 05f30fc..fdc0d69 100644 --- a/core/res/res/values-en-rUS/strings.xml +++ b/core/res/res/values-en-rUS/strings.xml @@ -1115,11 +1115,9 @@ <skip /> <!-- no translation found for cut (5845613239192595662) --> <skip /> - <!-- no translation found for cutAll (4474519683293791451) --> <skip /> <!-- no translation found for copy (8603721575469529820) --> <skip /> - <!-- no translation found for copyAll (4777548804630476932) --> <skip /> <!-- no translation found for paste (6458036735811828538) --> <skip /> diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml index afc2862..dbf32b6 100644 --- a/core/res/res/values-es-rUS/strings.xml +++ b/core/res/res/values-es-rUS/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Seleccionar texto"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Detener la selección de texto"</string> <string name="cut" msgid="3092569408438626261">"Cortar"</string> - <string name="cutAll" msgid="2436383270024931639">"Cortar llamada"</string> <string name="copy" msgid="2681946229533511987">"Copiar"</string> - <string name="copyAll" msgid="2590829068100113057">"Copiar todo"</string> <string name="paste" msgid="5629880836805036433">"Pegar"</string> <string name="copyUrl" msgid="2538211579596067402">"Copiar URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Método de entrada"</string> diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml index 045b6a1..425fca3 100644 --- a/core/res/res/values-es/strings.xml +++ b/core/res/res/values-es/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Seleccionar texto"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Detener selección de texto"</string> <string name="cut" msgid="3092569408438626261">"Cortar"</string> - <string name="cutAll" msgid="2436383270024931639">"Cortar todo"</string> <string name="copy" msgid="2681946229533511987">"Copiar"</string> - <string name="copyAll" msgid="2590829068100113057">"Copiar todo"</string> <string name="paste" msgid="5629880836805036433">"Pegar"</string> <string name="copyUrl" msgid="2538211579596067402">"Copiar URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Método de introducción de texto"</string> diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml index 3ab7814..2dacb30 100644 --- a/core/res/res/values-fr/strings.xml +++ b/core/res/res/values-fr/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Sélectionner le texte"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Arrêter sélection de texte"</string> <string name="cut" msgid="3092569408438626261">"Couper"</string> - <string name="cutAll" msgid="2436383270024931639">"Tout couper"</string> <string name="copy" msgid="2681946229533511987">"Copier"</string> - <string name="copyAll" msgid="2590829068100113057">"Tout copier"</string> <string name="paste" msgid="5629880836805036433">"Coller"</string> <string name="copyUrl" msgid="2538211579596067402">"Copier l\'URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Mode de saisie"</string> diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml index 6e50d21..c7aac82 100644 --- a/core/res/res/values-it/strings.xml +++ b/core/res/res/values-it/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Seleziona testo"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Termina selezione testo"</string> <string name="cut" msgid="3092569408438626261">"Taglia"</string> - <string name="cutAll" msgid="2436383270024931639">"Taglia tutto"</string> <string name="copy" msgid="2681946229533511987">"Copia"</string> - <string name="copyAll" msgid="2590829068100113057">"Copia tutto"</string> <string name="paste" msgid="5629880836805036433">"Incolla"</string> <string name="copyUrl" msgid="2538211579596067402">"Copia URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Metodo inserimento"</string> diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml index f1e98da..25e977d 100644 --- a/core/res/res/values-ja/strings.xml +++ b/core/res/res/values-ja/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"テキストを選択"</string> <string name="stopSelectingText" msgid="4157931463872320996">"テキストの選択を終了"</string> <string name="cut" msgid="3092569408438626261">"切り取り"</string> - <string name="cutAll" msgid="2436383270024931639">"すべて切り取り"</string> <string name="copy" msgid="2681946229533511987">"コピー"</string> - <string name="copyAll" msgid="2590829068100113057">"すべてコピー"</string> <string name="paste" msgid="5629880836805036433">"貼り付け"</string> <string name="copyUrl" msgid="2538211579596067402">"URLをコピー"</string> <string name="inputMethod" msgid="1653630062304567879">"入力方法"</string> diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml index 712100e..faf589e 100644 --- a/core/res/res/values-ko/strings.xml +++ b/core/res/res/values-ko/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"텍스트 선택"</string> <string name="stopSelectingText" msgid="4157931463872320996">"텍스트 선택 중지"</string> <string name="cut" msgid="3092569408438626261">"잘라내기"</string> - <string name="cutAll" msgid="2436383270024931639">"모두 잘라내기"</string> <string name="copy" msgid="2681946229533511987">"복사"</string> - <string name="copyAll" msgid="2590829068100113057">"모두 복사"</string> <string name="paste" msgid="5629880836805036433">"붙여넣기"</string> <string name="copyUrl" msgid="2538211579596067402">"URL 복사"</string> <string name="inputMethod" msgid="1653630062304567879">"입력 방법"</string> diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml index f09039d..2c23d53 100644 --- a/core/res/res/values-nb/strings.xml +++ b/core/res/res/values-nb/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Merk tekst"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Slutt å merke tekst"</string> <string name="cut" msgid="3092569408438626261">"Klipp ut"</string> - <string name="cutAll" msgid="2436383270024931639">"Klipp ut alt"</string> <string name="copy" msgid="2681946229533511987">"Kopier"</string> - <string name="copyAll" msgid="2590829068100113057">"Kopier alt"</string> <string name="paste" msgid="5629880836805036433">"Lim inn"</string> <string name="copyUrl" msgid="2538211579596067402">"Kopier URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Inndatametode"</string> diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml index 17a2fe4..10a8e74 100644 --- a/core/res/res/values-nl/strings.xml +++ b/core/res/res/values-nl/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Tekst selecteren"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Stoppen met tekst selecteren"</string> <string name="cut" msgid="3092569408438626261">"Knippen"</string> - <string name="cutAll" msgid="2436383270024931639">"Alles knippen"</string> <string name="copy" msgid="2681946229533511987">"Kopiëren"</string> - <string name="copyAll" msgid="2590829068100113057">"Alles kopiëren"</string> <string name="paste" msgid="5629880836805036433">"Plakken"</string> <string name="copyUrl" msgid="2538211579596067402">"URL kopiëren"</string> <string name="inputMethod" msgid="1653630062304567879">"Invoermethode"</string> diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml index 87e045d..70c3170 100644 --- a/core/res/res/values-pl/strings.xml +++ b/core/res/res/values-pl/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Zaznacz tekst"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Zatrzymaj wybieranie tekstu"</string> <string name="cut" msgid="3092569408438626261">"Wytnij"</string> - <string name="cutAll" msgid="2436383270024931639">"Wytnij wszystko"</string> <string name="copy" msgid="2681946229533511987">"Kopiuj"</string> - <string name="copyAll" msgid="2590829068100113057">"Kopiuj wszystko"</string> <string name="paste" msgid="5629880836805036433">"Wklej"</string> <string name="copyUrl" msgid="2538211579596067402">"Kopiuj adres URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Metoda wprowadzania"</string> diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml index 3fb8d55..9a8c6e9 100644 --- a/core/res/res/values-pt-rPT/strings.xml +++ b/core/res/res/values-pt-rPT/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Seleccionar texto"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Parar selecção de texto"</string> <string name="cut" msgid="3092569408438626261">"Cortar"</string> - <string name="cutAll" msgid="2436383270024931639">"Cortar tudo"</string> <string name="copy" msgid="2681946229533511987">"Copiar"</string> - <string name="copyAll" msgid="2590829068100113057">"Copiar tudo"</string> <string name="paste" msgid="5629880836805036433">"Colar"</string> <string name="copyUrl" msgid="2538211579596067402">"Copiar URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Método de entrada"</string> diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml index 81ceaf3..2119539 100644 --- a/core/res/res/values-pt/strings.xml +++ b/core/res/res/values-pt/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Selecionar texto"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Parar seleção de texto"</string> <string name="cut" msgid="3092569408438626261">"Recortar"</string> - <string name="cutAll" msgid="2436383270024931639">"Recortar tudo"</string> <string name="copy" msgid="2681946229533511987">"Copiar"</string> - <string name="copyAll" msgid="2590829068100113057">"Copiar tudo"</string> <string name="paste" msgid="5629880836805036433">"Colar"</string> <string name="copyUrl" msgid="2538211579596067402">"Copiar URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Método de entrada"</string> diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml index 4176886..a99107e 100644 --- a/core/res/res/values-ru/strings.xml +++ b/core/res/res/values-ru/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Выбрать текст"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Остановить выделение текста"</string> <string name="cut" msgid="3092569408438626261">"Вырезать"</string> - <string name="cutAll" msgid="2436383270024931639">"Вырезать все"</string> <string name="copy" msgid="2681946229533511987">"Копировать"</string> - <string name="copyAll" msgid="2590829068100113057">"Копировать все"</string> <string name="paste" msgid="5629880836805036433">"Вставить"</string> <string name="copyUrl" msgid="2538211579596067402">"Копировать URL"</string> <string name="inputMethod" msgid="1653630062304567879">"Способ ввода"</string> diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml index 4c78f4f..b3e415d 100644 --- a/core/res/res/values-sv/strings.xml +++ b/core/res/res/values-sv/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Markera text"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Sluta välja text"</string> <string name="cut" msgid="3092569408438626261">"Klipp ut"</string> - <string name="cutAll" msgid="2436383270024931639">"Klipp ut alla"</string> <string name="copy" msgid="2681946229533511987">"Kopiera"</string> - <string name="copyAll" msgid="2590829068100113057">"Kopiera alla"</string> <string name="paste" msgid="5629880836805036433">"Klistra in"</string> <string name="copyUrl" msgid="2538211579596067402">"Kopiera webbadress"</string> <string name="inputMethod" msgid="1653630062304567879">"Indatametod"</string> diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml index f2fd734..a6bc41e 100644 --- a/core/res/res/values-tr/strings.xml +++ b/core/res/res/values-tr/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"Metin seç"</string> <string name="stopSelectingText" msgid="4157931463872320996">"Metin seçmeyi durdur"</string> <string name="cut" msgid="3092569408438626261">"Kes"</string> - <string name="cutAll" msgid="2436383270024931639">"Tümünü kes"</string> <string name="copy" msgid="2681946229533511987">"Kopyala"</string> - <string name="copyAll" msgid="2590829068100113057">"Tümünü kopyala"</string> <string name="paste" msgid="5629880836805036433">"Yapıştır"</string> <string name="copyUrl" msgid="2538211579596067402">"URL\'yi kopyala"</string> <string name="inputMethod" msgid="1653630062304567879">"Giriş yöntemi"</string> diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml index a0fb721..aa3f12d 100644 --- a/core/res/res/values-zh-rCN/strings.xml +++ b/core/res/res/values-zh-rCN/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"选择文字"</string> <string name="stopSelectingText" msgid="4157931463872320996">"停止选择文字"</string> <string name="cut" msgid="3092569408438626261">"剪切"</string> - <string name="cutAll" msgid="2436383270024931639">"全部剪切"</string> <string name="copy" msgid="2681946229533511987">"复制"</string> - <string name="copyAll" msgid="2590829068100113057">"全部复制"</string> <string name="paste" msgid="5629880836805036433">"粘贴"</string> <string name="copyUrl" msgid="2538211579596067402">"复制网址"</string> <string name="inputMethod" msgid="1653630062304567879">"输入法"</string> diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml index d5dd857..5bf0342 100644 --- a/core/res/res/values-zh-rTW/strings.xml +++ b/core/res/res/values-zh-rTW/strings.xml @@ -706,9 +706,7 @@ <string name="selectText" msgid="3889149123626888637">"選取文字"</string> <string name="stopSelectingText" msgid="4157931463872320996">"停止選取文字"</string> <string name="cut" msgid="3092569408438626261">"剪下"</string> - <string name="cutAll" msgid="2436383270024931639">"全部剪下"</string> <string name="copy" msgid="2681946229533511987">"複製"</string> - <string name="copyAll" msgid="2590829068100113057">"全部複製"</string> <string name="paste" msgid="5629880836805036433">"貼上"</string> <string name="copyUrl" msgid="2538211579596067402">"複製網址"</string> <string name="inputMethod" msgid="1653630062304567879">"輸入方式"</string> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index bd65fee..1df424b 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1245,7 +1245,7 @@ <public type="anim" name="cycle_interpolator" id="0x010a000c" /> <!-- =============================================================== - Resources introduced in kraken. + Resources introduced in Gingerbread. =============================================================== --> <public type="attr" name="logo" id="0x010102be" /> @@ -1273,5 +1273,4 @@ <public-padding type="dimen" name="kraken_resource_pad" end="0x01050010" /> <public-padding type="color" name="kraken_resource_pad" end="0x01060020" /> <public-padding type="array" name="kraken_resource_pad" end="0x01070010" /> - </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 62fd169..52abe45 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1854,23 +1854,17 @@ <string name="selectAll">Select all</string> <!-- Item on EditText context menu. This action is used to start selecting text in the edit field. --> - <string name="selectText">Select text</string> + <string name="selectText">Select word</string> - <!-- Item on EditText context menu. This action is used to start selecting text in the edit field. --> + <!-- Item on EditText context menu. This action is used to stop selecting text in the edit field. --> <string name="stopSelectingText">Stop selecting text</string> <!-- Item on EditText context menu. This action is used to cut selected the text into the clipboard. --> <string name="cut">Cut</string> - <!-- Item on EditText context menu. This action is used to cut all the text into the clipboard. --> - <string name="cutAll">Cut all</string> - <!-- Item on EditText context menu. This action is used to cut selected the text into the clipboard. --> <string name="copy">Copy</string> - <!-- Item on EditText context menu. This action is used to copy all the text into the clipboard. --> - <string name="copyAll">Copy all</string> - <!-- Item on EditText context menu. This action is used t o paste from the clipboard into the eidt field --> <string name="paste">Paste</string> |