diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
new file mode 100644
index 0000000000000..880dc345dcdc2
--- /dev/null
+++ b/core/java/android/widget/Editor.java
@@ -0,0 +1,3750 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.R;
+import android.content.ClipData;
+import android.content.ClipData.Item;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.inputmethodservice.ExtractEditText;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.KeyListener;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.WordIterator;
+import android.text.style.EasyEditSpan;
+import android.text.style.SuggestionRangeSpan;
+import android.text.style.SuggestionSpan;
+import android.text.style.TextAppearanceSpan;
+import android.text.style.URLSpan;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.DisplayList;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.HardwareCanvas;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.View.DragShadowBuilder;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView.Drawables;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.EditableInputConnection;
+
+import java.text.BreakIterator;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+/**
+ * Helper class used by TextView to handle editable text views.
+ *
+ * @hide
+ */
+public class Editor {
+ static final int BLINK = 500;
+ private static final float[] TEMP_POSITION = new float[2];
+ private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
+
+ // Cursor Controllers.
+ InsertionPointCursorController mInsertionPointCursorController;
+ SelectionModifierCursorController mSelectionModifierCursorController;
+ ActionMode mSelectionActionMode;
+ boolean mInsertionControllerEnabled;
+ boolean mSelectionControllerEnabled;
+
+ // Used to highlight a word when it is corrected by the IME
+ CorrectionHighlighter mCorrectionHighlighter;
+
+ InputContentType mInputContentType;
+ InputMethodState mInputMethodState;
+
+ DisplayList[] mTextDisplayLists;
+
+ boolean mFrozenWithFocus;
+ boolean mSelectionMoved;
+ boolean mTouchFocusSelected;
+
+ KeyListener mKeyListener;
+ int mInputType = EditorInfo.TYPE_NULL;
+
+ boolean mDiscardNextActionUp;
+ boolean mIgnoreActionUpEvent;
+
+ long mShowCursor;
+ Blink mBlink;
+
+ boolean mCursorVisible = true;
+ boolean mSelectAllOnFocus;
+ boolean mTextIsSelectable;
+
+ CharSequence mError;
+ boolean mErrorWasChanged;
+ ErrorPopup mErrorPopup;
+ /**
+ * This flag is set if the TextView tries to display an error before it
+ * is attached to the window (so its position is still unknown).
+ * It causes the error to be shown later, when onAttachedToWindow()
+ * is called.
+ */
+ boolean mShowErrorAfterAttach;
+
+ boolean mInBatchEditControllers;
+
+ SuggestionsPopupWindow mSuggestionsPopupWindow;
+ SuggestionRangeSpan mSuggestionRangeSpan;
+ Runnable mShowSuggestionRunnable;
+
+ final Drawable[] mCursorDrawable = new Drawable[2];
+ int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
+
+ private Drawable mSelectHandleLeft;
+ private Drawable mSelectHandleRight;
+ private Drawable mSelectHandleCenter;
+
+ // Global listener that detects changes in the global position of the TextView
+ private PositionListener mPositionListener;
+
+ float mLastDownPositionX, mLastDownPositionY;
+ Callback mCustomSelectionActionModeCallback;
+
+ // Set when this TextView gained focus with some text selected. Will start selection mode.
+ boolean mCreatedWithASelection;
+
+ private EasyEditSpanController mEasyEditSpanController;
+
+ WordIterator mWordIterator;
+ SpellChecker mSpellChecker;
+
+ private Rect mTempRect;
+
+ private TextView mTextView;
+
+ Editor(TextView textView) {
+ mTextView = textView;
+ mEasyEditSpanController = new EasyEditSpanController();
+ mTextView.addTextChangedListener(mEasyEditSpanController);
+ }
+
+ void onAttachedToWindow() {
+ if (mShowErrorAfterAttach) {
+ showError();
+ mShowErrorAfterAttach = false;
+ }
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ // No need to create the controller.
+ // The get method will add the listener on controller creation.
+ if (mInsertionPointCursorController != null) {
+ observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+ }
+ if (mSelectionModifierCursorController != null) {
+ observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+ }
+ updateSpellCheckSpans(0, mTextView.getText().length(),
+ true /* create the spell checker if needed */);
+ }
+
+ void onDetachedFromWindow() {
+ if (mError != null) {
+ hideError();
+ }
+
+ if (mBlink != null) {
+ mBlink.removeCallbacks(mBlink);
+ }
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onDetached();
+ }
+
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onDetached();
+ }
+
+ if (mShowSuggestionRunnable != null) {
+ mTextView.removeCallbacks(mShowSuggestionRunnable);
+ }
+
+ invalidateTextDisplayList();
+
+ if (mSpellChecker != null) {
+ mSpellChecker.closeSession();
+ // Forces the creation of a new SpellChecker next time this window is created.
+ // Will handle the cases where the settings has been changed in the meantime.
+ mSpellChecker = null;
+ }
+
+ hideControllers();
+ }
+
+ private void showError() {
+ if (mTextView.getWindowToken() == null) {
+ mShowErrorAfterAttach = true;
+ return;
+ }
+
+ if (mErrorPopup == null) {
+ LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
+ final TextView err = (TextView) inflater.inflate(
+ com.android.internal.R.layout.textview_hint, null);
+
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+ mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
+ mErrorPopup.setFocusable(false);
+ // The user is entering text, so the input method is needed. We
+ // don't want the popup to be displayed on top of it.
+ mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+ }
+
+ TextView tv = (TextView) mErrorPopup.getContentView();
+ chooseSize(mErrorPopup, mError, tv);
+ tv.setText(mError);
+
+ mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
+ mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
+ }
+
+ public void setError(CharSequence error, Drawable icon) {
+ mError = TextUtils.stringOrSpannedString(error);
+ mErrorWasChanged = true;
+ final Drawables dr = mTextView.mDrawables;
+ if (dr != null) {
+ switch (mTextView.getResolvedLayoutDirection()) {
+ default:
+ case View.LAYOUT_DIRECTION_LTR:
+ mTextView.setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
+ dr.mDrawableBottom);
+ break;
+ case View.LAYOUT_DIRECTION_RTL:
+ mTextView.setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
+ dr.mDrawableBottom);
+ break;
+ }
+ } else {
+ mTextView.setCompoundDrawables(null, null, icon, null);
+ }
+
+ if (mError == null) {
+ if (mErrorPopup != null) {
+ if (mErrorPopup.isShowing()) {
+ mErrorPopup.dismiss();
+ }
+
+ mErrorPopup = null;
+ }
+ } else {
+ if (mTextView.isFocused()) {
+ showError();
+ }
+ }
+ }
+
+ private void hideError() {
+ if (mErrorPopup != null) {
+ if (mErrorPopup.isShowing()) {
+ mErrorPopup.dismiss();
+ }
+ }
+
+ mShowErrorAfterAttach = false;
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the middle of the error icon.
+ */
+ private int getErrorX() {
+ /*
+ * The "25" is the distance between the point and the right edge
+ * of the background
+ */
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+
+ final Drawables dr = mTextView.mDrawables;
+ return mTextView.getWidth() - mErrorPopup.getWidth() - mTextView.getPaddingRight() -
+ (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the bottom of the error icon.
+ */
+ private int getErrorY() {
+ /*
+ * Compound, not extended, because the icon is not clipped
+ * if the text height is smaller.
+ */
+ final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
+ int vspace = mTextView.getBottom() - mTextView.getTop() -
+ mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
+
+ final Drawables dr = mTextView.mDrawables;
+ int icontop = compoundPaddingTop +
+ (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
+
+ /*
+ * The "2" is the distance between the point and the top edge
+ * of the background.
+ */
+ final float scale = mTextView.getResources().getDisplayMetrics().density;
+ return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - mTextView.getHeight() -
+ (int) (2 * scale + 0.5f);
+ }
+
+ void createInputContentTypeIfNeeded() {
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ }
+
+ void createInputMethodStateIfNeeded() {
+ if (mInputMethodState == null) {
+ mInputMethodState = new InputMethodState();
+ }
+ }
+
+ boolean isCursorVisible() {
+ // The default value is true, even when there is no associated Editor
+ return mCursorVisible && mTextView.isTextEditable();
+ }
+
+ void prepareCursorControllers() {
+ boolean windowSupportsHandles = false;
+
+ ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
+ if (params instanceof WindowManager.LayoutParams) {
+ WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
+ windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
+ || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
+ }
+
+ boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
+ mInsertionControllerEnabled = enabled && isCursorVisible();
+ mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
+
+ if (!mInsertionControllerEnabled) {
+ hideInsertionPointCursorController();
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onDetached();
+ mInsertionPointCursorController = null;
+ }
+ }
+
+ if (!mSelectionControllerEnabled) {
+ stopSelectionActionMode();
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onDetached();
+ mSelectionModifierCursorController = null;
+ }
+ }
+ }
+
+ private void hideInsertionPointCursorController() {
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.hide();
+ }
+ }
+
+ /**
+ * Hides the insertion controller and stops text selection mode, hiding the selection controller
+ */
+ void hideControllers() {
+ hideCursorControllers();
+ hideSpanControllers();
+ }
+
+ private void hideSpanControllers() {
+ if (mEasyEditSpanController != null) {
+ mEasyEditSpanController.hide();
+ }
+ }
+
+ private void hideCursorControllers() {
+ if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
+ // Should be done before hide insertion point controller since it triggers a show of it
+ mSuggestionsPopupWindow.hide();
+ }
+ hideInsertionPointCursorController();
+ stopSelectionActionMode();
+ }
+
+ /**
+ * Create new SpellCheckSpans on the modified region.
+ */
+ private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
+ if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
+ !(mTextView instanceof ExtractEditText)) {
+ if (mSpellChecker == null && createSpellChecker) {
+ mSpellChecker = new SpellChecker(mTextView);
+ }
+ if (mSpellChecker != null) {
+ mSpellChecker.spellCheck(start, end);
+ }
+ }
+ }
+
+ void onScreenStateChanged(int screenState) {
+ switch (screenState) {
+ case View.SCREEN_STATE_ON:
+ resumeBlink();
+ break;
+ case View.SCREEN_STATE_OFF:
+ suspendBlink();
+ break;
+ }
+ }
+
+ private void suspendBlink() {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ }
+
+ private void resumeBlink() {
+ if (mBlink != null) {
+ mBlink.uncancel();
+ makeBlink();
+ }
+ }
+
+ void adjustInputType(boolean password, boolean passwordInputType,
+ boolean webPasswordInputType, boolean numberPasswordInputType) {
+ // mInputType has been set from inputType, possibly modified by mInputMethod.
+ // Specialize mInputType to [web]password if we have a text class and the original input
+ // type was a password.
+ if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+ if (password || passwordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+ if (webPasswordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
+ }
+ } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
+ if (numberPasswordInputType) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
+ }
+ }
+ }
+
+ private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
+ int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+ int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+ int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.textview_error_popup_default_width);
+ Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
+ Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
+ float max = 0;
+ for (int i = 0; i < l.getLineCount(); i++) {
+ max = Math.max(max, l.getLineWidth(i));
+ }
+
+ /*
+ * Now set the popup size to be big enough for the text plus the border capped
+ * to DEFAULT_MAX_POPUP_WIDTH
+ */
+ pop.setWidth(wid + (int) Math.ceil(max));
+ pop.setHeight(ht + l.getHeight());
+ }
+
+ void setFrame() {
+ if (mErrorPopup != null) {
+ TextView tv = (TextView) mErrorPopup.getContentView();
+ chooseSize(mErrorPopup, mError, tv);
+ mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
+ mErrorPopup.getWidth(), mErrorPopup.getHeight());
+ }
+ }
+
+ /**
+ * Unlike {@link TextView#textCanBeSelected()}, this method is based on the current state
+ * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have
+ * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
+ */
+ private boolean canSelectText() {
+ return hasSelectionController() && mTextView.getText().length() != 0;
+ }
+
+ /**
+ * It would be better to rely on the input type for everything. A password inputType should have
+ * a password transformation. We should hence use isPasswordInputType instead of this method.
+ *
+ * We should:
+ * - Call setInputType in setKeyListener instead of changing the input type directly (which
+ * would install the correct transformation).
+ * - Refuse the installation of a non-password transformation in setTransformation if the input
+ * type is password.
+ *
+ * However, this is like this for legacy reasons and we cannot break existing apps. This method
+ * is useful since it matches what the user can see (obfuscated text or not).
+ *
+ * @return true if the current transformation method is of the password type.
+ */
+ private boolean hasPasswordTransformationMethod() {
+ return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
+ }
+
+ /**
+ * Adjusts selection to the word under last touch offset.
+ * Return true if the operation was successfully performed.
+ */
+ private boolean selectCurrentWord() {
+ if (!canSelectText()) {
+ return false;
+ }
+
+ if (hasPasswordTransformationMethod()) {
+ // Always select all on a password field.
+ // Cut/copy menu entries are not available for passwords, but being able to select all
+ // is however useful to delete or paste to replace the entire content.
+ return mTextView.selectAllText();
+ }
+
+ int inputType = mTextView.getInputType();
+ int klass = inputType & InputType.TYPE_MASK_CLASS;
+ int variation = inputType & InputType.TYPE_MASK_VARIATION;
+
+ // Specific text field types: select the entire text for these
+ if (klass == InputType.TYPE_CLASS_NUMBER ||
+ klass == InputType.TYPE_CLASS_PHONE ||
+ klass == InputType.TYPE_CLASS_DATETIME ||
+ variation == InputType.TYPE_TEXT_VARIATION_URI ||
+ variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
+ variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
+ variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+ return mTextView.selectAllText();
+ }
+
+ long lastTouchOffsets = getLastTouchOffsets();
+ final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
+ final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
+
+ // Safety check in case standard touch event handling has been bypassed
+ if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
+ if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
+
+ int selectionStart, selectionEnd;
+
+ // If a URLSpan (web address, email, phone...) is found at that position, select it.
+ URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
+ getSpans(minOffset, maxOffset, URLSpan.class);
+ if (urlSpans.length >= 1) {
+ URLSpan urlSpan = urlSpans[0];
+ selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
+ selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
+ } else {
+ final WordIterator wordIterator = getWordIterator();
+ wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
+
+ selectionStart = wordIterator.getBeginning(minOffset);
+ selectionEnd = wordIterator.getEnd(maxOffset);
+
+ if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
+ selectionStart == selectionEnd) {
+ // Possible when the word iterator does not properly handle the text's language
+ long range = getCharRange(minOffset);
+ selectionStart = TextUtils.unpackRangeStartFromLong(range);
+ selectionEnd = TextUtils.unpackRangeEndFromLong(range);
+ }
+ }
+
+ Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+ return selectionEnd > selectionStart;
+ }
+
+ void onLocaleChanged() {
+ // Will be re-created on demand in getWordIterator with the proper new locale
+ mWordIterator = null;
+ }
+
+ /**
+ * @hide
+ */
+ public WordIterator getWordIterator() {
+ if (mWordIterator == null) {
+ mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
+ }
+ return mWordIterator;
+ }
+
+ private long getCharRange(int offset) {
+ final int textLength = mTextView.getText().length();
+ if (offset + 1 < textLength) {
+ final char currentChar = mTextView.getText().charAt(offset);
+ final char nextChar = mTextView.getText().charAt(offset + 1);
+ if (Character.isSurrogatePair(currentChar, nextChar)) {
+ return TextUtils.packRangeInLong(offset, offset + 2);
+ }
+ }
+ if (offset < textLength) {
+ return TextUtils.packRangeInLong(offset, offset + 1);
+ }
+ if (offset - 2 >= 0) {
+ final char previousChar = mTextView.getText().charAt(offset - 1);
+ final char previousPreviousChar = mTextView.getText().charAt(offset - 2);
+ if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
+ return TextUtils.packRangeInLong(offset - 2, offset);
+ }
+ }
+ if (offset - 1 >= 0) {
+ return TextUtils.packRangeInLong(offset - 1, offset);
+ }
+ return TextUtils.packRangeInLong(offset, offset);
+ }
+
+ private boolean touchPositionIsInSelection() {
+ int selectionStart = mTextView.getSelectionStart();
+ int selectionEnd = mTextView.getSelectionEnd();
+
+ if (selectionStart == selectionEnd) {
+ return false;
+ }
+
+ if (selectionStart > selectionEnd) {
+ int tmp = selectionStart;
+ selectionStart = selectionEnd;
+ selectionEnd = tmp;
+ Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+ }
+
+ SelectionModifierCursorController selectionController = getSelectionController();
+ int minOffset = selectionController.getMinTouchOffset();
+ int maxOffset = selectionController.getMaxTouchOffset();
+
+ return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
+ }
+
+ private PositionListener getPositionListener() {
+ if (mPositionListener == null) {
+ mPositionListener = new PositionListener();
+ }
+ return mPositionListener;
+ }
+
+ private interface TextViewPositionListener {
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled);
+ }
+
+ private boolean isPositionVisible(int positionX, int positionY) {
+ synchronized (TEMP_POSITION) {
+ final float[] position = TEMP_POSITION;
+ position[0] = positionX;
+ position[1] = positionY;
+ View view = mTextView;
+
+ while (view != null) {
+ if (view != mTextView) {
+ // Local scroll is already taken into account in positionX/Y
+ position[0] -= view.getScrollX();
+ position[1] -= view.getScrollY();
+ }
+
+ if (position[0] < 0 || position[1] < 0 ||
+ position[0] > view.getWidth() || position[1] > view.getHeight()) {
+ return false;
+ }
+
+ if (!view.getMatrix().isIdentity()) {
+ view.getMatrix().mapPoints(position);
+ }
+
+ position[0] += view.getLeft();
+ position[1] += view.getTop();
+
+ final ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ view = (View) parent;
+ } else {
+ // We've reached the ViewRoot, stop iterating
+ view = null;
+ }
+ }
+ }
+
+ // We've been able to walk up the view hierarchy and the position was never clipped
+ return true;
+ }
+
+ private boolean isOffsetVisible(int offset) {
+ Layout layout = mTextView.getLayout();
+ final int line = layout.getLineForOffset(offset);
+ final int lineBottom = layout.getLineBottom(line);
+ final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
+ return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
+ lineBottom + mTextView.viewportToContentVerticalOffset());
+ }
+
+ /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
+ * in the view. Returns false when the position is in the empty space of left/right of text.
+ */
+ private boolean isPositionOnText(float x, float y) {
+ Layout layout = mTextView.getLayout();
+ if (layout == null) return false;
+
+ final int line = mTextView.getLineAtCoordinate(y);
+ x = mTextView.convertToLocalHorizontalCoordinate(x);
+
+ if (x < layout.getLineLeft(line)) return false;
+ if (x > layout.getLineRight(line)) return false;
+ return true;
+ }
+
+ public boolean performLongClick(boolean handled) {
+ // Long press in empty space moves cursor and shows the Paste affordance if available.
+ if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
+ mInsertionControllerEnabled) {
+ final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
+ mLastDownPositionY);
+ stopSelectionActionMode();
+ Selection.setSelection((Spannable) mTextView.getText(), offset);
+ getInsertionController().showWithActionPopup();
+ handled = true;
+ }
+
+ if (!handled && mSelectionActionMode != null) {
+ if (touchPositionIsInSelection()) {
+ // Start a drag
+ final int start = mTextView.getSelectionStart();
+ final int end = mTextView.getSelectionEnd();
+ CharSequence selectedText = mTextView.getTransformedText(start, end);
+ ClipData data = ClipData.newPlainText(null, selectedText);
+ DragLocalState localState = new DragLocalState(mTextView, start, end);
+ mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
+ stopSelectionActionMode();
+ } else {
+ getSelectionController().hide();
+ selectCurrentWord();
+ getSelectionController().show();
+ }
+ handled = true;
+ }
+
+ // Start a new selection
+ if (!handled) {
+ handled = startSelectionActionMode();
+ }
+
+ return handled;
+ }
+
+ private long getLastTouchOffsets() {
+ SelectionModifierCursorController selectionController = getSelectionController();
+ final int minOffset = selectionController.getMinTouchOffset();
+ final int maxOffset = selectionController.getMaxTouchOffset();
+ return TextUtils.packRangeInLong(minOffset, maxOffset);
+ }
+
+ void onFocusChanged(boolean focused, int direction) {
+ mShowCursor = SystemClock.uptimeMillis();
+ ensureEndedBatchEdit();
+
+ if (focused) {
+ int selStart = mTextView.getSelectionStart();
+ int selEnd = mTextView.getSelectionEnd();
+
+ // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
+ // mode for these, unless there was a specific selection already started.
+ final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
+ selEnd == mTextView.getText().length();
+
+ mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
+ !isFocusHighlighted;
+
+ if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
+ // If a tap was used to give focus to that view, move cursor at tap position.
+ // Has to be done before onTakeFocus, which can be overloaded.
+ final int lastTapPosition = getLastTapPosition();
+ if (lastTapPosition >= 0) {
+ Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
+ }
+
+ // Note this may have to be moved out of the Editor class
+ MovementMethod mMovement = mTextView.getMovementMethod();
+ if (mMovement != null) {
+ mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
+ }
+
+ // The DecorView does not have focus when the 'Done' ExtractEditText button is
+ // pressed. Since it is the ViewAncestor's mView, it requests focus before
+ // ExtractEditText clears focus, which gives focus to the ExtractEditText.
+ // This special case ensure that we keep current selection in that case.
+ // It would be better to know why the DecorView does not have focus at that time.
+ if (((mTextView instanceof ExtractEditText) || mSelectionMoved) &&
+ selStart >= 0 && selEnd >= 0) {
+ /*
+ * Someone intentionally set the selection, so let them
+ * do whatever it is that they wanted to do instead of
+ * the default on-focus behavior. We reset the selection
+ * here instead of just skipping the onTakeFocus() call
+ * because some movement methods do something other than
+ * just setting the selection in theirs and we still
+ * need to go through that path.
+ */
+ Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
+ }
+
+ if (mSelectAllOnFocus) {
+ mTextView.selectAllText();
+ }
+
+ mTouchFocusSelected = true;
+ }
+
+ mFrozenWithFocus = false;
+ mSelectionMoved = false;
+
+ if (mError != null) {
+ showError();
+ }
+
+ makeBlink();
+ } else {
+ if (mError != null) {
+ hideError();
+ }
+ // Don't leave us in the middle of a batch edit.
+ mTextView.onEndBatchEdit();
+
+ if (mTextView instanceof ExtractEditText) {
+ // terminateTextSelectionMode removes selection, which we want to keep when
+ // ExtractEditText goes out of focus.
+ final int selStart = mTextView.getSelectionStart();
+ final int selEnd = mTextView.getSelectionEnd();
+ hideControllers();
+ Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
+ } else {
+ hideControllers();
+ downgradeEasyCorrectionSpans();
+ }
+
+ // No need to create the controller
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.resetTouchOffsets();
+ }
+ }
+ }
+
+ /**
+ * Downgrades to simple suggestions all the easy correction spans that are not a spell check
+ * span.
+ */
+ private void downgradeEasyCorrectionSpans() {
+ CharSequence text = mTextView.getText();
+ if (text instanceof Spannable) {
+ Spannable spannable = (Spannable) text;
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
+ spannable.length(), SuggestionSpan.class);
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ int flags = suggestionSpans[i].getFlags();
+ if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
+ && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
+ flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
+ suggestionSpans[i].setFlags(flags);
+ }
+ }
+ }
+ }
+
+ void sendOnTextChanged(int start, int after) {
+ updateSpellCheckSpans(start, start + after, false);
+
+ // Hide the controllers as soon as text is modified (typing, procedural...)
+ // We do not hide the span controllers, since they can be added when a new text is
+ // inserted into the text view (voice IME).
+ hideCursorControllers();
+ }
+
+ private int getLastTapPosition() {
+ // No need to create the controller at that point, no last tap position saved
+ if (mSelectionModifierCursorController != null) {
+ int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
+ if (lastTapPosition >= 0) {
+ // Safety check, should not be possible.
+ if (lastTapPosition > mTextView.getText().length()) {
+ lastTapPosition = mTextView.getText().length();
+ }
+ return lastTapPosition;
+ }
+ }
+
+ return -1;
+ }
+
+ void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (hasWindowFocus) {
+ if (mBlink != null) {
+ mBlink.uncancel();
+ makeBlink();
+ }
+ } else {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ if (mInputContentType != null) {
+ mInputContentType.enterDown = false;
+ }
+ // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
+ hideControllers();
+ if (mSuggestionsPopupWindow != null) {
+ mSuggestionsPopupWindow.onParentLostFocus();
+ }
+
+ // Don't leave us in the middle of a batch edit.
+ mTextView.onEndBatchEdit();
+ }
+ }
+
+ void onTouchEvent(MotionEvent event) {
+ if (hasSelectionController()) {
+ getSelectionController().onTouchEvent(event);
+ }
+
+ if (mShowSuggestionRunnable != null) {
+ mTextView.removeCallbacks(mShowSuggestionRunnable);
+ mShowSuggestionRunnable = null;
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mLastDownPositionX = event.getX();
+ mLastDownPositionY = event.getY();
+
+ // Reset this state; it will be re-set if super.onTouchEvent
+ // causes focus to move to the view.
+ mTouchFocusSelected = false;
+ mIgnoreActionUpEvent = false;
+ }
+ }
+
+ public void beginBatchEdit() {
+ mInBatchEditControllers = true;
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = ++ims.mBatchEditNesting;
+ if (nesting == 1) {
+ ims.mCursorChanged = false;
+ ims.mChangedDelta = 0;
+ if (ims.mContentChanged) {
+ // We already have a pending change from somewhere else,
+ // so turn this into a full update.
+ ims.mChangedStart = 0;
+ ims.mChangedEnd = mTextView.getText().length();
+ } else {
+ ims.mChangedStart = EXTRACT_UNKNOWN;
+ ims.mChangedEnd = EXTRACT_UNKNOWN;
+ ims.mContentChanged = false;
+ }
+ mTextView.onBeginBatchEdit();
+ }
+ }
+ }
+
+ public void endBatchEdit() {
+ mInBatchEditControllers = false;
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = --ims.mBatchEditNesting;
+ if (nesting == 0) {
+ finishBatchEdit(ims);
+ }
+ }
+ }
+
+ void ensureEndedBatchEdit() {
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting != 0) {
+ ims.mBatchEditNesting = 0;
+ finishBatchEdit(ims);
+ }
+ }
+
+ void finishBatchEdit(final InputMethodState ims) {
+ mTextView.onEndBatchEdit();
+
+ if (ims.mContentChanged || ims.mSelectionModeChanged) {
+ mTextView.updateAfterEdit();
+ reportExtractedText();
+ } else if (ims.mCursorChanged) {
+ // Cheezy way to get us to report the current cursor location.
+ mTextView.invalidateCursor();
+ }
+ }
+
+ static final int EXTRACT_NOTHING = -2;
+ static final int EXTRACT_UNKNOWN = -1;
+
+ boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+ return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
+ EXTRACT_UNKNOWN, outText);
+ }
+
+ private boolean extractTextInternal(ExtractedTextRequest request,
+ int partialStartOffset, int partialEndOffset, int delta,
+ ExtractedText outText) {
+ final CharSequence content = mTextView.getText();
+ if (content != null) {
+ if (partialStartOffset != EXTRACT_NOTHING) {
+ final int N = content.length();
+ if (partialStartOffset < 0) {
+ outText.partialStartOffset = outText.partialEndOffset = -1;
+ partialStartOffset = 0;
+ partialEndOffset = N;
+ } else {
+ // Now use the delta to determine the actual amount of text
+ // we need.
+ partialEndOffset += delta;
+ // Adjust offsets to ensure we contain full spans.
+ if (content instanceof Spanned) {
+ Spanned spanned = (Spanned)content;
+ Object[] spans = spanned.getSpans(partialStartOffset,
+ partialEndOffset, ParcelableSpan.class);
+ int i = spans.length;
+ while (i > 0) {
+ i--;
+ int j = spanned.getSpanStart(spans[i]);
+ if (j < partialStartOffset) partialStartOffset = j;
+ j = spanned.getSpanEnd(spans[i]);
+ if (j > partialEndOffset) partialEndOffset = j;
+ }
+ }
+ outText.partialStartOffset = partialStartOffset;
+ outText.partialEndOffset = partialEndOffset - delta;
+
+ if (partialStartOffset > N) {
+ partialStartOffset = N;
+ } else if (partialStartOffset < 0) {
+ partialStartOffset = 0;
+ }
+ if (partialEndOffset > N) {
+ partialEndOffset = N;
+ } else if (partialEndOffset < 0) {
+ partialEndOffset = 0;
+ }
+ }
+ if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
+ outText.text = content.subSequence(partialStartOffset,
+ partialEndOffset);
+ } else {
+ outText.text = TextUtils.substring(content, partialStartOffset,
+ partialEndOffset);
+ }
+ } else {
+ outText.partialStartOffset = 0;
+ outText.partialEndOffset = 0;
+ outText.text = "";
+ }
+ outText.flags = 0;
+ if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
+ outText.flags |= ExtractedText.FLAG_SELECTING;
+ }
+ if (mTextView.isSingleLine()) {
+ outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
+ }
+ outText.startOffset = 0;
+ outText.selectionStart = mTextView.getSelectionStart();
+ outText.selectionEnd = mTextView.getSelectionEnd();
+ return true;
+ }
+ return false;
+ }
+
+ boolean reportExtractedText() {
+ final Editor.InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ final boolean contentChanged = ims.mContentChanged;
+ if (contentChanged || ims.mSelectionModeChanged) {
+ ims.mContentChanged = false;
+ ims.mSelectionModeChanged = false;
+ final ExtractedTextRequest req = ims.mExtracting;
+ if (req != null) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
+ "Retrieving extracted start=" + ims.mChangedStart +
+ " end=" + ims.mChangedEnd +
+ " delta=" + ims.mChangedDelta);
+ if (ims.mChangedStart < 0 && !contentChanged) {
+ ims.mChangedStart = EXTRACT_NOTHING;
+ }
+ if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
+ ims.mChangedDelta, ims.mTmpExtracted)) {
+ if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
+ "Reporting extracted start=" +
+ ims.mTmpExtracted.partialStartOffset +
+ " end=" + ims.mTmpExtracted.partialEndOffset +
+ ": " + ims.mTmpExtracted.text);
+ imm.updateExtractedText(mTextView, req.token, ims.mTmpExtracted);
+ ims.mChangedStart = EXTRACT_UNKNOWN;
+ ims.mChangedEnd = EXTRACT_UNKNOWN;
+ ims.mChangedDelta = 0;
+ ims.mContentChanged = false;
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
+ int cursorOffsetVertical) {
+ final int selectionStart = mTextView.getSelectionStart();
+ final int selectionEnd = mTextView.getSelectionEnd();
+
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting == 0) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (imm.isActive(mTextView)) {
+ boolean reported = false;
+ if (ims.mContentChanged || ims.mSelectionModeChanged) {
+ // We are in extract mode and the content has changed
+ // in some way... just report complete new text to the
+ // input method.
+ reported = reportExtractedText();
+ }
+ if (!reported && highlight != null) {
+ int candStart = -1;
+ int candEnd = -1;
+ if (mTextView.getText() instanceof Spannable) {
+ Spannable sp = (Spannable) mTextView.getText();
+ candStart = EditableInputConnection.getComposingSpanStart(sp);
+ candEnd = EditableInputConnection.getComposingSpanEnd(sp);
+ }
+ imm.updateSelection(mTextView,
+ selectionStart, selectionEnd, candStart, candEnd);
+ }
+ }
+
+ if (imm.isWatchingCursor(mTextView) && highlight != null) {
+ highlight.computeBounds(ims.mTmpRectF, true);
+ ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
+
+ canvas.getMatrix().mapPoints(ims.mTmpOffset);
+ ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
+
+ ims.mTmpRectF.offset(0, cursorOffsetVertical);
+
+ ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
+ (int)(ims.mTmpRectF.top + 0.5),
+ (int)(ims.mTmpRectF.right + 0.5),
+ (int)(ims.mTmpRectF.bottom + 0.5));
+
+ imm.updateCursor(mTextView,
+ ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
+ ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
+ }
+ }
+ }
+
+ if (mCorrectionHighlighter != null) {
+ mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
+ }
+
+ if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
+ drawCursor(canvas, cursorOffsetVertical);
+ // Rely on the drawable entirely, do not draw the cursor line.
+ // Has to be done after the IMM related code above which relies on the highlight.
+ highlight = null;
+ }
+
+ if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
+ drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
+ cursorOffsetVertical);
+ } else {
+ layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
+ }
+ }
+
+ private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
+ Paint highlightPaint, int cursorOffsetVertical) {
+ final int width = mTextView.getWidth();
+ final int height = mTextView.getHeight();
+
+ final long lineRange = layout.getLineRangeForDraw(canvas);
+ int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
+ int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
+ if (lastLine < 0) return;
+
+ layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
+ firstLine, lastLine);
+
+ if (layout instanceof DynamicLayout) {
+ if (mTextDisplayLists == null) {
+ mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
+ }
+
+ DynamicLayout dynamicLayout = (DynamicLayout) layout;
+ int[] blockEnds = dynamicLayout.getBlockEnds();
+ int[] blockIndices = dynamicLayout.getBlockIndices();
+ final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
+
+ final int mScrollX = mTextView.getScrollX();
+ final int mScrollY = mTextView.getScrollY();
+ canvas.translate(mScrollX, mScrollY);
+ int endOfPreviousBlock = -1;
+ int searchStartIndex = 0;
+ for (int i = 0; i < numberOfBlocks; i++) {
+ int blockEnd = blockEnds[i];
+ int blockIndex = blockIndices[i];
+
+ final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
+ if (blockIsInvalid) {
+ blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
+ searchStartIndex);
+ // Dynamic layout internal block indices structure is updated from Editor
+ blockIndices[i] = blockIndex;
+ searchStartIndex = blockIndex + 1;
+ }
+
+ DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
+ if (blockDisplayList == null) {
+ blockDisplayList = mTextDisplayLists[blockIndex] =
+ mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex);
+ } else {
+ if (blockIsInvalid) blockDisplayList.invalidate();
+ }
+
+ if (!blockDisplayList.isValid()) {
+ final HardwareCanvas hardwareCanvas = blockDisplayList.start();
+ try {
+ hardwareCanvas.setViewport(width, height);
+ // The dirty rect should always be null for a display list
+ hardwareCanvas.onPreDraw(null);
+ hardwareCanvas.translate(-mScrollX, -mScrollY);
+ layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd);
+ hardwareCanvas.translate(mScrollX, mScrollY);
+ } finally {
+ hardwareCanvas.onPostDraw();
+ blockDisplayList.end();
+ if (View.USE_DISPLAY_LIST_PROPERTIES) {
+ blockDisplayList.setLeftTopRightBottom(0, 0, width, height);
+ }
+ }
+ }
+
+ ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null,
+ DisplayList.FLAG_CLIP_CHILDREN);
+ endOfPreviousBlock = blockEnd;
+ }
+ canvas.translate(-mScrollX, -mScrollY);
+ } else {
+ // Boring layout is used for empty and hint text
+ layout.drawText(canvas, firstLine, lastLine);
+ }
+ }
+
+ private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
+ int searchStartIndex) {
+ int length = mTextDisplayLists.length;
+ for (int i = searchStartIndex; i < length; i++) {
+ boolean blockIndexFound = false;
+ for (int j = 0; j < numberOfBlocks; j++) {
+ if (blockIndices[j] == i) {
+ blockIndexFound = true;
+ break;
+ }
+ }
+ if (blockIndexFound) continue;
+ return i;
+ }
+
+ // No available index found, the pool has to grow
+ int newSize = ArrayUtils.idealIntArraySize(length + 1);
+ DisplayList[] displayLists = new DisplayList[newSize];
+ System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
+ mTextDisplayLists = displayLists;
+ return length;
+ }
+
+ private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
+ final boolean translate = cursorOffsetVertical != 0;
+ if (translate) canvas.translate(0, cursorOffsetVertical);
+ for (int i = 0; i < mCursorCount; i++) {
+ mCursorDrawable[i].draw(canvas);
+ }
+ if (translate) canvas.translate(0, -cursorOffsetVertical);
+ }
+
+ void invalidateTextDisplayList() {
+ if (mTextDisplayLists != null) {
+ for (int i = 0; i < mTextDisplayLists.length; i++) {
+ if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
+ }
+ }
+ }
+
+ void updateCursorsPositions() {
+ if (mTextView.mCursorDrawableRes == 0) {
+ mCursorCount = 0;
+ return;
+ }
+
+ Layout layout = mTextView.getLayout();
+ final int offset = mTextView.getSelectionStart();
+ final int line = layout.getLineForOffset(offset);
+ final int top = layout.getLineTop(line);
+ final int bottom = layout.getLineTop(line + 1);
+
+ mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
+
+ int middle = bottom;
+ if (mCursorCount == 2) {
+ // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
+ middle = (top + bottom) >> 1;
+ }
+
+ updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset));
+
+ if (mCursorCount == 2) {
+ updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset));
+ }
+ }
+
+ /**
+ * @return true if the selection mode was actually started.
+ */
+ boolean startSelectionActionMode() {
+ if (mSelectionActionMode != null) {
+ // Selection action mode is already started
+ return false;
+ }
+
+ if (!canSelectText() || !mTextView.requestFocus()) {
+ Log.w(TextView.LOG_TAG,
+ "TextView does not support text selection. Action mode cancelled.");
+ return false;
+ }
+
+ if (!mTextView.hasSelection()) {
+ // There may already be a selection on device rotation
+ if (!selectCurrentWord()) {
+ // No word found under cursor or text selection not permitted.
+ return false;
+ }
+ }
+
+ boolean willExtract = extractedTextModeWillBeStarted();
+
+ // Do not start the action mode when extracted text will show up full screen, which would
+ // immediately hide the newly created action bar and would be visually distracting.
+ if (!willExtract) {
+ ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
+ mSelectionActionMode = mTextView.startActionMode(actionModeCallback);
+ }
+
+ final boolean selectionStarted = mSelectionActionMode != null || willExtract;
+ if (selectionStarted && !mTextView.isTextSelectable()) {
+ // Show the IME to be able to replace text, except when selecting non editable text.
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.showSoftInput(mTextView, 0, null);
+ }
+ }
+
+ return selectionStarted;
+ }
+
+ private boolean extractedTextModeWillBeStarted() {
+ if (!(mTextView instanceof ExtractEditText)) {
+ final InputMethodManager imm = InputMethodManager.peekInstance();
+ return imm != null && imm.isFullscreenMode();
+ }
+ return false;
+ }
+
+ /**
+ * @return true if the cursor/current selection overlaps a {@link SuggestionSpan}.
+ */
+ private boolean isCursorInsideSuggestionSpan() {
+ CharSequence text = mTextView.getText();
+ if (!(text instanceof Spannable)) return false;
+
+ SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans(
+ mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class);
+ return (suggestionSpans.length > 0);
+ }
+
+ /**
+ * @return true if the cursor is inside an {@link SuggestionSpan} with
+ * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
+ */
+ private boolean isCursorInsideEasyCorrectionSpan() {
+ Spannable spannable = (Spannable) mTextView.getText();
+ SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
+ mTextView.getSelectionEnd(), SuggestionSpan.class);
+ for (int i = 0; i < suggestionSpans.length; i++) {
+ if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void onTouchUpEvent(MotionEvent event) {
+ boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
+ hideControllers();
+ CharSequence text = mTextView.getText();
+ if (!selectAllGotFocus && text.length() > 0) {
+ // Move cursor
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+ Selection.setSelection((Spannable) text, offset);
+ if (mSpellChecker != null) {
+ // When the cursor moves, the word that was typed may need spell check
+ mSpellChecker.onSelectionChanged();
+ }
+ if (!extractedTextModeWillBeStarted()) {
+ if (isCursorInsideEasyCorrectionSpan()) {
+ mShowSuggestionRunnable = new Runnable() {
+ public void run() {
+ showSuggestions();
+ }
+ };
+ // removeCallbacks is performed on every touch
+ mTextView.postDelayed(mShowSuggestionRunnable,
+ ViewConfiguration.getDoubleTapTimeout());
+ } else if (hasInsertionController()) {
+ getInsertionController().show();
+ }
+ }
+ }
+ }
+
+ protected void stopSelectionActionMode() {
+ if (mSelectionActionMode != null) {
+ // This will hide the mSelectionModifierCursorController
+ mSelectionActionMode.finish();
+ }
+ }
+
+ /**
+ * @return True if this view supports insertion handles.
+ */
+ boolean hasInsertionController() {
+ return mInsertionControllerEnabled;
+ }
+
+ /**
+ * @return True if this view supports selection handles.
+ */
+ boolean hasSelectionController() {
+ return mSelectionControllerEnabled;
+ }
+
+ InsertionPointCursorController getInsertionController() {
+ if (!mInsertionControllerEnabled) {
+ return null;
+ }
+
+ if (mInsertionPointCursorController == null) {
+ mInsertionPointCursorController = new InsertionPointCursorController();
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+ }
+
+ return mInsertionPointCursorController;
+ }
+
+ SelectionModifierCursorController getSelectionController() {
+ if (!mSelectionControllerEnabled) {
+ return null;
+ }
+
+ if (mSelectionModifierCursorController == null) {
+ mSelectionModifierCursorController = new SelectionModifierCursorController();
+
+ final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+ observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+ }
+
+ return mSelectionModifierCursorController;
+ }
+
+ private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
+ if (mCursorDrawable[cursorIndex] == null)
+ mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable(
+ mTextView.mCursorDrawableRes);
+
+ if (mTempRect == null) mTempRect = new Rect();
+ mCursorDrawable[cursorIndex].getPadding(mTempRect);
+ final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
+ horizontal = Math.max(0.5f, horizontal - 0.5f);
+ final int left = (int) (horizontal) - mTempRect.left;
+ mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
+ bottom + mTempRect.bottom);
+ }
+
+ /**
+ * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+ * a dictionnary) from the current input method, provided by it calling
+ * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
+ * implementation flashes the background of the corrected word to provide feedback to the user.
+ *
+ * @param info The auto correct info about the text that was corrected.
+ */
+ public void onCommitCorrection(CorrectionInfo info) {
+ if (mCorrectionHighlighter == null) {
+ mCorrectionHighlighter = new CorrectionHighlighter();
+ } else {
+ mCorrectionHighlighter.invalidate(false);
+ }
+
+ mCorrectionHighlighter.highlight(info);
+ }
+
+ void showSuggestions() {
+ if (mSuggestionsPopupWindow == null) {
+ mSuggestionsPopupWindow = new SuggestionsPopupWindow();
+ }
+ hideControllers();
+ mSuggestionsPopupWindow.show();
+ }
+
+ boolean areSuggestionsShown() {
+ return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
+ }
+
+ void onScrollChanged() {
+ if (mPositionListener != null) {
+ mPositionListener.onScrollChanged();
+ }
+ // Internal scroll affects the clip boundaries
+ invalidateTextDisplayList();
+ }
+
+ /**
+ * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
+ */
+ private boolean shouldBlink() {
+ if (!isCursorVisible() || !mTextView.isFocused()) return false;
+
+ final int start = mTextView.getSelectionStart();
+ if (start < 0) return false;
+
+ final int end = mTextView.getSelectionEnd();
+ if (end < 0) return false;
+
+ return start == end;
+ }
+
+ void makeBlink() {
+ if (shouldBlink()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ if (mBlink == null) mBlink = new Blink();
+ mBlink.removeCallbacks(mBlink);
+ mBlink.postAtTime(mBlink, mShowCursor + BLINK);
+ } else {
+ if (mBlink != null) mBlink.removeCallbacks(mBlink);
+ }
+ }
+
+ private class Blink extends Handler implements Runnable {
+ private boolean mCancelled;
+
+ public void run() {
+ Log.d("GILLES", "blinking !!!");
+ if (mCancelled) {
+ return;
+ }
+
+ removeCallbacks(Blink.this);
+
+ if (shouldBlink()) {
+ if (mTextView.getLayout() != null) {
+ mTextView.invalidateCursorPath();
+ }
+
+ postAtTime(this, SystemClock.uptimeMillis() + BLINK);
+ }
+ }
+
+ void cancel() {
+ if (!mCancelled) {
+ removeCallbacks(Blink.this);
+ mCancelled = true;
+ }
+ }
+
+ void uncancel() {
+ mCancelled = false;
+ }
+ }
+
+ private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
+ TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
+ com.android.internal.R.layout.text_drag_thumbnail, null);
+
+ if (shadowView == null) {
+ throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
+ }
+
+ if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
+ text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
+ }
+ shadowView.setText(text);
+ shadowView.setTextColor(mTextView.getTextColors());
+
+ shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge);
+ shadowView.setGravity(Gravity.CENTER);
+
+ shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ shadowView.measure(size, size);
+
+ shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
+ shadowView.invalidate();
+ return new DragShadowBuilder(shadowView);
+ }
+
+ private static class DragLocalState {
+ public TextView sourceTextView;
+ public int start, end;
+
+ public DragLocalState(TextView sourceTextView, int start, int end) {
+ this.sourceTextView = sourceTextView;
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ void onDrop(DragEvent event) {
+ StringBuilder content = new StringBuilder("");
+ ClipData clipData = event.getClipData();
+ final int itemCount = clipData.getItemCount();
+ for (int i=0; i < itemCount; i++) {
+ Item item = clipData.getItemAt(i);
+ content.append(item.coerceToText(mTextView.getContext()));
+ }
+
+ final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+
+ Object localState = event.getLocalState();
+ DragLocalState dragLocalState = null;
+ if (localState instanceof DragLocalState) {
+ dragLocalState = (DragLocalState) localState;
+ }
+ boolean dragDropIntoItself = dragLocalState != null &&
+ dragLocalState.sourceTextView == mTextView;
+
+ if (dragDropIntoItself) {
+ if (offset >= dragLocalState.start && offset < dragLocalState.end) {
+ // A drop inside the original selection discards the drop.
+ return;
+ }
+ }
+
+ final int originalLength = mTextView.getText().length();
+ long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content);
+ int min = TextUtils.unpackRangeStartFromLong(minMax);
+ int max = TextUtils.unpackRangeEndFromLong(minMax);
+
+ Selection.setSelection((Spannable) mTextView.getText(), max);
+ mTextView.replaceText_internal(min, max, content);
+
+ if (dragDropIntoItself) {
+ int dragSourceStart = dragLocalState.start;
+ int dragSourceEnd = dragLocalState.end;
+ if (max <= dragSourceStart) {
+ // Inserting text before selection has shifted positions
+ final int shift = mTextView.getText().length() - originalLength;
+ dragSourceStart += shift;
+ dragSourceEnd += shift;
+ }
+
+ // Delete original selection
+ mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
+
+ // Make sure we do not leave two adjacent spaces.
+ CharSequence t = mTextView.getTransformedText(dragSourceStart - 1, dragSourceStart + 1);
+ if ( (dragSourceStart == 0 || Character.isSpaceChar(t.charAt(0))) &&
+ (dragSourceStart == mTextView.getText().length() ||
+ Character.isSpaceChar(t.charAt(1))) ) {
+ final int pos = dragSourceStart == mTextView.getText().length() ?
+ dragSourceStart - 1 : dragSourceStart;
+ mTextView.deleteText_internal(pos, pos + 1);
+ }
+ }
+ }
+
+ /**
+ * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
+ * pop-up should be displayed.
+ */
+ class EasyEditSpanController implements TextWatcher {
+
+ private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
+
+ private EasyEditPopupWindow mPopupWindow;
+
+ private EasyEditSpan mEasyEditSpan;
+
+ private Runnable mHidePopup;
+
+ public void hide() {
+ if (mPopupWindow != null) {
+ mPopupWindow.hide();
+ mTextView.removeCallbacks(mHidePopup);
+ }
+ removeSpans(mTextView.getText());
+ mEasyEditSpan = null;
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Intentionally empty
+ }
+
+ public void afterTextChanged(Editable s) {
+ // Intentionally empty
+ }
+
+ /**
+ * Monitors the changes in the text.
+ *
+ *
{@link SpanWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
+ * as the notifications are not sent when a spannable (with spans) is inserted.
+ */
+ public void onTextChanged(CharSequence buffer, int start, int before, int after) {
+ adjustSpans(buffer, start, after);
+
+ if (mTextView.getWindowVisibility() != View.VISIBLE) {
+ // The window is not visible yet, ignore the text change.
+ return;
+ }
+
+ if (mTextView.getLayout() == null) {
+ // The view has not been layout yet, ignore the text change
+ return;
+ }
+
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (!(mTextView instanceof ExtractEditText) && imm != null && imm.isFullscreenMode()) {
+ // The input is in extract mode. We do not have to handle the easy edit in the
+ // original TextView, as the ExtractEditText will do
+ return;
+ }
+
+ // Remove the current easy edit span, as the text changed, and remove the pop-up
+ // (if any)
+ if (mEasyEditSpan != null) {
+ if (buffer instanceof Spannable) {
+ ((Spannable) buffer).removeSpan(mEasyEditSpan);
+ }
+ mEasyEditSpan = null;
+ }
+ if (mPopupWindow != null && mPopupWindow.isShowing()) {
+ mPopupWindow.hide();
+ }
+
+ // Display the new easy edit span (if any).
+ if (buffer instanceof Spanned) {
+ mEasyEditSpan = getSpan((Spanned) buffer);
+ if (mEasyEditSpan != null) {
+ if (mPopupWindow == null) {
+ mPopupWindow = new EasyEditPopupWindow();
+ mHidePopup = new Runnable() {
+ @Override
+ public void run() {
+ hide();
+ }
+ };
+ }
+ mPopupWindow.show(mEasyEditSpan);
+ mTextView.removeCallbacks(mHidePopup);
+ mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
+ }
+ }
+ }
+
+ /**
+ * Adjusts the spans by removing all of them except the last one.
+ */
+ private void adjustSpans(CharSequence buffer, int start, int after) {
+ // This method enforces that only one easy edit span is attached to the text.
+ // A better way to enforce this would be to listen for onSpanAdded, but this method
+ // cannot be used in this scenario as no notification is triggered when a text with
+ // spans is inserted into a text.
+ if (buffer instanceof Spannable) {
+ Spannable spannable = (Spannable) buffer;
+ EasyEditSpan[] spans = spannable.getSpans(start, start + after, EasyEditSpan.class);
+ if (spans.length > 0) {
+ // Assuming there was only one EasyEditSpan before, we only need check to
+ // check for a duplicate if a new one is found in the modified interval
+ spans = spannable.getSpans(0, spannable.length(), EasyEditSpan.class);
+ for (int i = 1; i < spans.length; i++) {
+ spannable.removeSpan(spans[i]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes all the {@link EasyEditSpan} currently attached.
+ */
+ private void removeSpans(CharSequence buffer) {
+ if (buffer instanceof Spannable) {
+ Spannable spannable = (Spannable) buffer;
+ EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
+ EasyEditSpan.class);
+ for (int i = 0; i < spans.length; i++) {
+ spannable.removeSpan(spans[i]);
+ }
+ }
+ }
+
+ private EasyEditSpan getSpan(Spanned spanned) {
+ EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
+ EasyEditSpan.class);
+ if (easyEditSpans.length == 0) {
+ return null;
+ } else {
+ return easyEditSpans[0];
+ }
+ }
+ }
+
+ /**
+ * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
+ * by {@link EasyEditSpanController}.
+ */
+ private class EasyEditPopupWindow extends PinnedPopupWindow
+ implements OnClickListener {
+ private static final int POPUP_TEXT_LAYOUT =
+ com.android.internal.R.layout.text_edit_action_popup_text;
+ private TextView mDeleteTextView;
+ private EasyEditSpan mEasyEditSpan;
+
+ @Override
+ protected void createPopupWindow() {
+ mPopupWindow = new PopupWindow(mTextView.getContext(), null,
+ com.android.internal.R.attr.textSelectHandleWindowStyle);
+ mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopupWindow.setClippingEnabled(true);
+ }
+
+ @Override
+ protected void initContentView() {
+ LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
+ linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ mContentView = linearLayout;
+ mContentView.setBackgroundResource(
+ com.android.internal.R.drawable.text_edit_side_paste_window);
+
+ LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
+ getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LayoutParams wrapContent = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+ mDeleteTextView.setLayoutParams(wrapContent);
+ mDeleteTextView.setText(com.android.internal.R.string.delete);
+ mDeleteTextView.setOnClickListener(this);
+ mContentView.addView(mDeleteTextView);
+ }
+
+ public void show(EasyEditSpan easyEditSpan) {
+ mEasyEditSpan = easyEditSpan;
+ super.show();
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == mDeleteTextView) {
+ Editable editable = (Editable) mTextView.getText();
+ int start = editable.getSpanStart(mEasyEditSpan);
+ int end = editable.getSpanEnd(mEasyEditSpan);
+ if (start >= 0 && end >= 0) {
+ mTextView.deleteText_internal(start, end);
+ }
+ }
+ }
+
+ @Override
+ protected int getTextOffset() {
+ // Place the pop-up at the end of the span
+ Editable editable = (Editable) mTextView.getText();
+ return editable.getSpanEnd(mEasyEditSpan);
+ }
+
+ @Override
+ protected int getVerticalLocalPosition(int line) {
+ return mTextView.getLayout().getLineBottom(line);
+ }
+
+ @Override
+ protected int clipVertically(int positionY) {
+ // As we display the pop-up below the span, no vertical clipping is required.
+ return positionY;
+ }
+ }
+
+ private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
+ // 3 handles
+ // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
+ private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
+ private TextViewPositionListener[] mPositionListeners =
+ new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
+ private boolean mPositionHasChanged = true;
+ // Absolute position of the TextView with respect to its parent window
+ private int mPositionX, mPositionY;
+ private int mNumberOfListeners;
+ private boolean mScrollHasChanged;
+ final int[] mTempCoords = new int[2];
+
+ public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
+ if (mNumberOfListeners == 0) {
+ updatePosition();
+ ViewTreeObserver vto = mTextView.getViewTreeObserver();
+ vto.addOnPreDrawListener(this);
+ }
+
+ int emptySlotIndex = -1;
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ TextViewPositionListener listener = mPositionListeners[i];
+ if (listener == positionListener) {
+ return;
+ } else if (emptySlotIndex < 0 && listener == null) {
+ emptySlotIndex = i;
+ }
+ }
+
+ mPositionListeners[emptySlotIndex] = positionListener;
+ mCanMove[emptySlotIndex] = canMove;
+ mNumberOfListeners++;
+ }
+
+ public void removeSubscriber(TextViewPositionListener positionListener) {
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionListeners[i] == positionListener) {
+ mPositionListeners[i] = null;
+ mNumberOfListeners--;
+ break;
+ }
+ }
+
+ if (mNumberOfListeners == 0) {
+ ViewTreeObserver vto = mTextView.getViewTreeObserver();
+ vto.removeOnPreDrawListener(this);
+ }
+ }
+
+ public int getPositionX() {
+ return mPositionX;
+ }
+
+ public int getPositionY() {
+ return mPositionY;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ updatePosition();
+
+ for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+ if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
+ TextViewPositionListener positionListener = mPositionListeners[i];
+ if (positionListener != null) {
+ positionListener.updatePosition(mPositionX, mPositionY,
+ mPositionHasChanged, mScrollHasChanged);
+ }
+ }
+ }
+
+ mScrollHasChanged = false;
+ return true;
+ }
+
+ private void updatePosition() {
+ mTextView.getLocationInWindow(mTempCoords);
+
+ mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
+
+ mPositionX = mTempCoords[0];
+ mPositionY = mTempCoords[1];
+ }
+
+ public void onScrollChanged() {
+ mScrollHasChanged = true;
+ }
+ }
+
+ private abstract class PinnedPopupWindow implements TextViewPositionListener {
+ protected PopupWindow mPopupWindow;
+ protected ViewGroup mContentView;
+ int mPositionX, mPositionY;
+
+ protected abstract void createPopupWindow();
+ protected abstract void initContentView();
+ protected abstract int getTextOffset();
+ protected abstract int getVerticalLocalPosition(int line);
+ protected abstract int clipVertically(int positionY);
+
+ public PinnedPopupWindow() {
+ createPopupWindow();
+
+ mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+ mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ initContentView();
+
+ LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ mContentView.setLayoutParams(wrapContent);
+
+ mPopupWindow.setContentView(mContentView);
+ }
+
+ public void show() {
+ getPositionListener().addSubscriber(this, false /* offset is fixed */);
+
+ computeLocalPosition();
+
+ final PositionListener positionListener = getPositionListener();
+ updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
+ }
+
+ protected void measureContent() {
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ mContentView.measure(
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
+ View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
+ View.MeasureSpec.AT_MOST));
+ }
+
+ /* The popup window will be horizontally centered on the getTextOffset() and vertically
+ * positioned according to viewportToContentHorizontalOffset.
+ *
+ * This method assumes that mContentView has properly been measured from its content. */
+ private void computeLocalPosition() {
+ measureContent();
+ final int width = mContentView.getMeasuredWidth();
+ final int offset = getTextOffset();
+ mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
+ mPositionX += mTextView.viewportToContentHorizontalOffset();
+
+ final int line = mTextView.getLayout().getLineForOffset(offset);
+ mPositionY = getVerticalLocalPosition(line);
+ mPositionY += mTextView.viewportToContentVerticalOffset();
+ }
+
+ private void updatePosition(int parentPositionX, int parentPositionY) {
+ int positionX = parentPositionX + mPositionX;
+ int positionY = parentPositionY + mPositionY;
+
+ positionY = clipVertically(positionY);
+
+ // Horizontal clipping
+ final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+ final int width = mContentView.getMeasuredWidth();
+ positionX = Math.min(displayMetrics.widthPixels - width, positionX);
+ positionX = Math.max(0, positionX);
+
+ if (isShowing()) {
+ mPopupWindow.update(positionX, positionY, -1, -1);
+ } else {
+ mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+ positionX, positionY);
+ }
+ }
+
+ public void hide() {
+ mPopupWindow.dismiss();
+ getPositionListener().removeSubscriber(this);
+ }
+
+ @Override
+ public void updatePosition(int parentPositionX, int parentPositionY,
+ boolean parentPositionChanged, boolean parentScrolled) {
+ // Either parentPositionChanged or parentScrolled is true, check if still visible
+ if (isShowing() && isOffsetVisible(getTextOffset())) {
+ if (parentScrolled) computeLocalPosition();
+ updatePosition(parentPositionX, parentPositionY);
+ } else {
+ hide();
+ }
+ }
+
+ public boolean isShowing() {
+ return mPopupWindow.isShowing();
+ }
+ }
+
+ private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
+ private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
+ private static final int ADD_TO_DICTIONARY = -1;
+ private static final int DELETE_TEXT = -2;
+ private SuggestionInfo[] mSuggestionInfos;
+ private int mNumberOfSuggestions;
+ private boolean mCursorWasVisibleBeforeSuggestions;
+ private boolean mIsShowingUp = false;
+ private SuggestionAdapter mSuggestionsAdapter;
+ private final Comparator {@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
- * as the notifications are not sent when a spannable (with spans) is inserted.
- */
- public void onTextChange(CharSequence buffer) {
- adjustSpans(mText);
-
- if (getWindowVisibility() != View.VISIBLE) {
- // The window is not visible yet, ignore the text change.
- return;
- }
-
- if (mLayout == null) {
- // The view has not been layout yet, ignore the text change
- return;
- }
-
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (!(TextView.this instanceof ExtractEditText)
- && imm != null && imm.isFullscreenMode()) {
- // The input is in extract mode. We do not have to handle the easy edit in the
- // original TextView, as the ExtractEditText will do
- return;
- }
-
- // Remove the current easy edit span, as the text changed, and remove the pop-up
- // (if any)
- if (mEasyEditSpan != null) {
- if (mText instanceof Spannable) {
- ((Spannable) mText).removeSpan(mEasyEditSpan);
- }
- mEasyEditSpan = null;
- }
- if (mPopupWindow != null && mPopupWindow.isShowing()) {
- mPopupWindow.hide();
- }
-
- // Display the new easy edit span (if any).
- if (buffer instanceof Spanned) {
- mEasyEditSpan = getSpan((Spanned) buffer);
- if (mEasyEditSpan != null) {
- if (mPopupWindow == null) {
- mPopupWindow = new EasyEditPopupWindow();
- mHidePopup = new Runnable() {
- @Override
- public void run() {
- hide();
- }
- };
- }
- mPopupWindow.show(mEasyEditSpan);
- TextView.this.removeCallbacks(mHidePopup);
- TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
- }
- }
- }
-
- /**
- * Adjusts the spans by removing all of them except the last one.
- */
- private void adjustSpans(CharSequence buffer) {
- // This method enforces that only one easy edit span is attached to the text.
- // A better way to enforce this would be to listen for onSpanAdded, but this method
- // cannot be used in this scenario as no notification is triggered when a text with
- // spans is inserted into a text.
- if (buffer instanceof Spannable) {
- Spannable spannable = (Spannable) buffer;
- EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
- EasyEditSpan.class);
- for (int i = 0; i < spans.length - 1; i++) {
- spannable.removeSpan(spans[i]);
- }
- }
- }
-
- /**
- * Removes all the {@link EasyEditSpan} currently attached.
- */
- private void removeSpans(CharSequence buffer) {
- if (buffer instanceof Spannable) {
- Spannable spannable = (Spannable) buffer;
- EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
- EasyEditSpan.class);
- for (int i = 0; i < spans.length; i++) {
- spannable.removeSpan(spans[i]);
- }
- }
- }
-
- private EasyEditSpan getSpan(Spanned spanned) {
- EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
- EasyEditSpan.class);
- if (easyEditSpans.length == 0) {
- return null;
- } else {
- return easyEditSpans[0];
- }
- }
- }
-
- /**
- * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
- * by {@link EasyEditSpanController}.
- */
- private class EasyEditPopupWindow extends PinnedPopupWindow
- implements OnClickListener {
- private static final int POPUP_TEXT_LAYOUT =
- com.android.internal.R.layout.text_edit_action_popup_text;
- private TextView mDeleteTextView;
- private EasyEditSpan mEasyEditSpan;
-
- @Override
- protected void createPopupWindow() {
- mPopupWindow = new PopupWindow(TextView.this.mContext, null,
- com.android.internal.R.attr.textSelectHandleWindowStyle);
- mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
- mPopupWindow.setClippingEnabled(true);
- }
-
- @Override
- protected void initContentView() {
- LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
- linearLayout.setOrientation(LinearLayout.HORIZONTAL);
- mContentView = linearLayout;
- mContentView.setBackgroundResource(
- com.android.internal.R.drawable.text_edit_side_paste_window);
-
- LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
- getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- LayoutParams wrapContent = new LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-
- mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
- mDeleteTextView.setLayoutParams(wrapContent);
- mDeleteTextView.setText(com.android.internal.R.string.delete);
- mDeleteTextView.setOnClickListener(this);
- mContentView.addView(mDeleteTextView);
- }
-
- public void show(EasyEditSpan easyEditSpan) {
- mEasyEditSpan = easyEditSpan;
- super.show();
- }
-
- @Override
- public void onClick(View view) {
- if (view == mDeleteTextView) {
- Editable editable = (Editable) mText;
- int start = editable.getSpanStart(mEasyEditSpan);
- int end = editable.getSpanEnd(mEasyEditSpan);
- if (start >= 0 && end >= 0) {
- deleteText_internal(start, end);
- }
- }
- }
-
- @Override
- protected int getTextOffset() {
- // Place the pop-up at the end of the span
- Editable editable = (Editable) mText;
- return editable.getSpanEnd(mEasyEditSpan);
- }
-
- @Override
- protected int getVerticalLocalPosition(int line) {
- return mLayout.getLineBottom(line);
- }
-
- @Override
- protected int clipVertically(int positionY) {
- // As we display the pop-up below the span, no vertical clipping is required.
- return positionY;
- }
- }
-
private class ChangeWatcher implements TextWatcher, SpanWatcher {
private CharSequence mBeforeText;
- private EasyEditSpanController mEasyEditSpanController;
-
- private ChangeWatcher() {
- mEasyEditSpanController = new EasyEditSpanController();
- }
-
public void beforeTextChanged(CharSequence buffer, int start,
int before, int after) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
@@ -9547,14 +8247,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
TextView.this.sendBeforeTextChanged(buffer, start, before, after);
}
- public void onTextChanged(CharSequence buffer, int start,
- int before, int after) {
+ public void onTextChanged(CharSequence buffer, int start, int before, int after) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
+ " before=" + before + " after=" + after + ": " + buffer);
TextView.this.handleTextChanged(buffer, start, before, after);
- mEasyEditSpanController.onTextChange(buffer);
-
if (AccessibilityManager.getInstance(mContext).isEnabled() &&
(isFocused() || isSelected() && isShown())) {
sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
@@ -9571,8 +8268,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- public void onSpanChanged(Spannable buf,
- Object what, int s, int e, int st, int en) {
+ public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) {
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);
@@ -9589,2264 +8285,5 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
+ " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, -1, e, -1);
}
-
- private void hideControllers() {
- mEasyEditSpanController.hide();
- }
- }
-
- private static class Blink extends Handler implements Runnable {
- private final WeakReferencetrue if the cursor/current selection overlaps a {@link SuggestionSpan}.
- */
- private boolean isCursorInsideSuggestionSpan() {
- if (!(mText instanceof Spannable)) return false;
-
- SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
- getSelectionEnd(), SuggestionSpan.class);
- return (suggestionSpans.length > 0);
- }
-
- /**
- * @return true if the cursor is inside an {@link SuggestionSpan} with
- * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
- */
- private boolean isCursorInsideEasyCorrectionSpan() {
- Spannable spannable = (Spannable) mText;
- SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
- getSelectionEnd(), SuggestionSpan.class);
- for (int i = 0; i < suggestionSpans.length; i++) {
- if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Downgrades to simple suggestions all the easy correction spans that are not a spell check
- * span.
- */
- private void downgradeEasyCorrectionSpans() {
- if (mText instanceof Spannable) {
- Spannable spannable = (Spannable) mText;
- SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
- spannable.length(), SuggestionSpan.class);
- for (int i = 0; i < suggestionSpans.length; i++) {
- int flags = suggestionSpans[i].getFlags();
- if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
- && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
- flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
- suggestionSpans[i].setFlags(flags);
- }
- }
- }
- }
-
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (mMovement != null && mText instanceof Spannable && mLayout != null) {
@@ -7450,44 +7040,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return super.onGenericMotionEvent(event);
}
- private void prepareCursorControllers() {
- if (mEditor == null) return;
-
- boolean windowSupportsHandles = false;
-
- ViewGroup.LayoutParams params = getRootView().getLayoutParams();
- if (params instanceof WindowManager.LayoutParams) {
- WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
- windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
- || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
- }
-
- getEditor().mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
- getEditor().mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
- mLayout != null;
-
- if (!getEditor().mInsertionControllerEnabled) {
- hideInsertionPointCursorController();
- if (getEditor().mInsertionPointCursorController != null) {
- getEditor().mInsertionPointCursorController.onDetached();
- getEditor().mInsertionPointCursorController = null;
- }
- }
-
- if (!getEditor().mSelectionControllerEnabled) {
- stopSelectionActionMode();
- if (getEditor().mSelectionModifierCursorController != null) {
- getEditor().mSelectionModifierCursorController.onDetached();
- getEditor().mSelectionModifierCursorController = null;
- }
- }
- }
-
/**
* @return True iff this TextView contains a text that can be edited, or if this is
* a selectable TextView.
*/
- private boolean isTextEditable() {
+ boolean isTextEditable() {
return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
}
@@ -7522,32 +7079,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mScroller = s;
}
- /**
- * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
- */
- private boolean shouldBlink() {
- if (mEditor == null || !isCursorVisible() || !isFocused()) return false;
-
- final int start = getSelectionStart();
- if (start < 0) return false;
-
- final int end = getSelectionEnd();
- if (end < 0) return false;
-
- return start == end;
- }
-
- private void makeBlink() {
- if (shouldBlink()) {
- getEditor().mShowCursor = SystemClock.uptimeMillis();
- if (getEditor().mBlink == null) getEditor().mBlink = new Blink(this);
- getEditor().mBlink.removeCallbacks(getEditor().mBlink);
- getEditor().mBlink.postAtTime(getEditor().mBlink, getEditor().mShowCursor + BLINK);
- } else {
- if (mEditor != null && getEditor().mBlink != null) getEditor().mBlink.removeCallbacks(getEditor().mBlink);
- }
- }
-
@Override
protected float getLeftFadingEdgeStrength() {
if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
@@ -7726,10 +7257,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
/**
* Unlike {@link #textCanBeSelected()}, this method is based on the current state of the
* TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
- * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
+ * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not sufficient.
*/
private boolean canSelectText() {
- return hasSelectionController() && mText.length() != 0;
+ return mText.length() != 0 && mEditor != null && getEditor().hasSelectionController();
}
/**
@@ -7738,7 +7269,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*
* See also {@link #canSelectText()}.
*/
- private boolean textCanBeSelected() {
+ boolean textCanBeSelected() {
// prepareCursorController() relies on this method.
// If you change this condition, make sure prepareCursorController is called anywhere
// the value of this condition might be changed.
@@ -7746,112 +7277,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return isTextEditable() || (isTextSelectable() && mText instanceof Spannable && isEnabled());
}
- private boolean canCut() {
- if (hasPasswordTransformationMethod()) {
- return false;
- }
-
- if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) {
- return true;
- }
-
- return false;
- }
-
- private boolean canCopy() {
- if (hasPasswordTransformationMethod()) {
- return false;
- }
-
- if (mText.length() > 0 && hasSelection()) {
- return true;
- }
-
- return false;
- }
-
- private boolean canPaste() {
- return (mText instanceof Editable &&
- mEditor != null && getEditor().mKeyListener != null &&
- getSelectionStart() >= 0 &&
- getSelectionEnd() >= 0 &&
- ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
- hasPrimaryClip());
- }
-
- private boolean selectAll() {
- final int length = mText.length();
- Selection.setSelection((Spannable) mText, 0, length);
- return length > 0;
- }
-
- /**
- * Adjusts selection to the word under last touch offset.
- * Return true if the operation was successfully performed.
- */
- private boolean selectCurrentWord() {
- if (!canSelectText()) {
- return false;
- }
-
- if (hasPasswordTransformationMethod()) {
- // Always select all on a password field.
- // Cut/copy menu entries are not available for passwords, but being able to select all
- // is however useful to delete or paste to replace the entire content.
- return selectAll();
- }
-
- int inputType = getInputType();
- int klass = inputType & InputType.TYPE_MASK_CLASS;
- int variation = inputType & InputType.TYPE_MASK_VARIATION;
-
- // Specific text field types: select the entire text for these
- if (klass == InputType.TYPE_CLASS_NUMBER ||
- klass == InputType.TYPE_CLASS_PHONE ||
- klass == InputType.TYPE_CLASS_DATETIME ||
- variation == InputType.TYPE_TEXT_VARIATION_URI ||
- variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
- variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
- variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
- return selectAll();
- }
-
- long lastTouchOffsets = getLastTouchOffsets();
- final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
- final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
-
- // Safety check in case standard touch event handling has been bypassed
- if (minOffset < 0 || minOffset >= mText.length()) return false;
- if (maxOffset < 0 || maxOffset >= mText.length()) return false;
-
- int selectionStart, selectionEnd;
-
- // If a URLSpan (web address, email, phone...) is found at that position, select it.
- URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
- if (urlSpans.length >= 1) {
- URLSpan urlSpan = urlSpans[0];
- selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
- selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
- } else {
- final WordIterator wordIterator = getWordIterator();
- wordIterator.setCharSequence(mText, minOffset, maxOffset);
-
- selectionStart = wordIterator.getBeginning(minOffset);
- selectionEnd = wordIterator.getEnd(maxOffset);
-
- if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
- selectionStart == selectionEnd) {
- // Possible when the word iterator does not properly handle the text's language
- long range = getCharRange(minOffset);
- selectionStart = TextUtils.unpackRangeStartFromLong(range);
- selectionEnd = TextUtils.unpackRangeEndFromLong(range);
- }
- }
-
- Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
- return selectionEnd > selectionStart;
- }
-
/**
* This is a temporary method. Future versions may support multi-locale text.
*
@@ -7877,45 +7302,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
+ * This method is used by the ArrowKeyMovementMethod to jump from one word to the other.
+ * Made available to achieve a consistent behavior.
* @hide
*/
public WordIterator getWordIterator() {
- if (getEditor().mWordIterator == null) {
- getEditor().mWordIterator = new WordIterator(getTextServicesLocale());
+ if (getEditor() != null) {
+ return mEditor.getWordIterator();
+ } else {
+ return null;
}
- return getEditor().mWordIterator;
- }
-
- private long getCharRange(int offset) {
- final int textLength = mText.length();
- if (offset + 1 < textLength) {
- final char currentChar = mText.charAt(offset);
- final char nextChar = mText.charAt(offset + 1);
- if (Character.isSurrogatePair(currentChar, nextChar)) {
- return TextUtils.packRangeInLong(offset, offset + 2);
- }
- }
- if (offset < textLength) {
- return TextUtils.packRangeInLong(offset, offset + 1);
- }
- if (offset - 2 >= 0) {
- final char previousChar = mText.charAt(offset - 1);
- final char previousPreviousChar = mText.charAt(offset - 2);
- if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
- return TextUtils.packRangeInLong(offset - 2, offset);
- }
- }
- if (offset - 1 >= 0) {
- return TextUtils.packRangeInLong(offset - 1, offset);
- }
- return TextUtils.packRangeInLong(offset, offset);
- }
-
- private long getLastTouchOffsets() {
- SelectionModifierCursorController selectionController = getSelectionController();
- final int minOffset = selectionController.getMinTouchOffset();
- final int maxOffset = selectionController.getMaxTouchOffset();
- return TextUtils.packRangeInLong(minOffset, maxOffset);
}
@Override
@@ -8004,11 +7400,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return imm != null && imm.isActive(this);
}
- // Selection context mode
- private static final int ID_SELECT_ALL = android.R.id.selectAll;
- private static final int ID_CUT = android.R.id.cut;
- private static final int ID_COPY = android.R.id.copy;
- private static final int ID_PASTE = android.R.id.paste;
+ static final int ID_SELECT_ALL = android.R.id.selectAll;
+ static final int ID_CUT = android.R.id.cut;
+ static final int ID_COPY = android.R.id.copy;
+ static final int ID_PASTE = android.R.id.paste;
/**
* Called when a context menu option for the text view is selected. Currently
@@ -8033,7 +7428,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
case ID_SELECT_ALL:
// This does not enter text selection mode. Text is highlighted, so that it can be
// bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
- selectAll();
+ selectAllText();
return true;
case ID_PASTE:
@@ -8054,89 +7449,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return false;
}
- private CharSequence getTransformedText(int start, int end) {
+ CharSequence getTransformedText(int start, int end) {
return removeSuggestionSpans(mTransformed.subSequence(start, end));
}
- /**
- * Prepare text so that there are not zero or two spaces at beginning and end of region defined
- * by [min, max] when replacing this region by paste.
- * Note that if there were two spaces (or more) at that position before, they are kept. We just
- * make sure we do not add an extra one from the paste content.
- */
- private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
- if (paste.length() > 0) {
- if (min > 0) {
- final char charBefore = mTransformed.charAt(min - 1);
- final char charAfter = paste.charAt(0);
-
- if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
- // Two spaces at beginning of paste: remove one
- final int originalLength = mText.length();
- deleteText_internal(min - 1, min);
- // Due to filters, there is no guarantee that exactly one character was
- // removed: count instead.
- final int delta = mText.length() - originalLength;
- min += delta;
- max += delta;
- } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
- !Character.isSpaceChar(charAfter) && charAfter != '\n') {
- // No space at beginning of paste: add one
- final int originalLength = mText.length();
- replaceText_internal(min, min, " ");
- // Taking possible filters into account as above.
- final int delta = mText.length() - originalLength;
- min += delta;
- max += delta;
- }
- }
-
- if (max < mText.length()) {
- final char charBefore = paste.charAt(paste.length() - 1);
- final char charAfter = mTransformed.charAt(max);
-
- if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
- // Two spaces at end of paste: remove one
- deleteText_internal(max, max + 1);
- } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
- !Character.isSpaceChar(charAfter) && charAfter != '\n') {
- // No space at end of paste: add one
- replaceText_internal(max, max, " ");
- }
- }
- }
-
- return TextUtils.packRangeInLong(min, max);
- }
-
- private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
- TextView shadowView = (TextView) inflate(mContext,
- com.android.internal.R.layout.text_drag_thumbnail, null);
-
- if (shadowView == null) {
- throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
- }
-
- if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
- text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
- }
- shadowView.setText(text);
- shadowView.setTextColor(getTextColors());
-
- shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
- shadowView.setGravity(Gravity.CENTER);
-
- shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT));
-
- final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
- shadowView.measure(size, size);
-
- shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
- shadowView.invalidate();
- return new DragShadowBuilder(shadowView);
- }
-
@Override
public boolean performLongClick() {
boolean handled = false;
@@ -8145,178 +7461,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
handled = true;
}
- if (mEditor == null) {
- return handled;
- }
-
- // Long press in empty space moves cursor and shows the Paste affordance if available.
- if (!handled && !isPositionOnText(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY) &&
- getEditor().mInsertionControllerEnabled) {
- final int offset = getOffsetForPosition(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY);
- stopSelectionActionMode();
- Selection.setSelection((Spannable) mText, offset);
- getInsertionController().showWithActionPopup();
- handled = true;
- }
-
- if (!handled && getEditor().mSelectionActionMode != null) {
- if (touchPositionIsInSelection()) {
- // Start a drag
- final int start = getSelectionStart();
- final int end = getSelectionEnd();
- CharSequence selectedText = getTransformedText(start, end);
- ClipData data = ClipData.newPlainText(null, selectedText);
- DragLocalState localState = new DragLocalState(this, start, end);
- startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
- stopSelectionActionMode();
- } else {
- getSelectionController().hide();
- selectCurrentWord();
- getSelectionController().show();
- }
- handled = true;
- }
-
- // Start a new selection
- if (!handled) {
- handled = startSelectionActionMode();
+ if (mEditor != null) {
+ handled |= getEditor().performLongClick(handled);
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- getEditor().mDiscardNextActionUp = true;
+ if (mEditor != null) getEditor().mDiscardNextActionUp = true;
}
return handled;
}
- private boolean touchPositionIsInSelection() {
- int selectionStart = getSelectionStart();
- int selectionEnd = getSelectionEnd();
-
- if (selectionStart == selectionEnd) {
- return false;
- }
-
- if (selectionStart > selectionEnd) {
- int tmp = selectionStart;
- selectionStart = selectionEnd;
- selectionEnd = tmp;
- Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
- }
-
- SelectionModifierCursorController selectionController = getSelectionController();
- int minOffset = selectionController.getMinTouchOffset();
- int maxOffset = selectionController.getMaxTouchOffset();
-
- return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
- }
-
- private PositionListener getPositionListener() {
- if (getEditor().mPositionListener == null) {
- getEditor().mPositionListener = new PositionListener();
- }
- return getEditor().mPositionListener;
- }
-
- private interface TextViewPositionListener {
- public void updatePosition(int parentPositionX, int parentPositionY,
- boolean parentPositionChanged, boolean parentScrolled);
- }
-
- private boolean isPositionVisible(int positionX, int positionY) {
- synchronized (TEMP_POSITION) {
- final float[] position = TEMP_POSITION;
- position[0] = positionX;
- position[1] = positionY;
- View view = this;
-
- while (view != null) {
- if (view != this) {
- // Local scroll is already taken into account in positionX/Y
- position[0] -= view.getScrollX();
- position[1] -= view.getScrollY();
- }
-
- if (position[0] < 0 || position[1] < 0 ||
- position[0] > view.getWidth() || position[1] > view.getHeight()) {
- return false;
- }
-
- if (!view.getMatrix().isIdentity()) {
- view.getMatrix().mapPoints(position);
- }
-
- position[0] += view.getLeft();
- position[1] += view.getTop();
-
- final ViewParent parent = view.getParent();
- if (parent instanceof View) {
- view = (View) parent;
- } else {
- // We've reached the ViewRoot, stop iterating
- view = null;
- }
- }
- }
-
- // We've been able to walk up the view hierarchy and the position was never clipped
- return true;
- }
-
- private boolean isOffsetVisible(int offset) {
- final int line = mLayout.getLineForOffset(offset);
- final int lineBottom = mLayout.getLineBottom(line);
- final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
- return isPositionVisible(primaryHorizontal + viewportToContentHorizontalOffset(),
- lineBottom + viewportToContentVerticalOffset());
- }
-
@Override
protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
if (mEditor != null) {
- if (getEditor().mPositionListener != null) {
- getEditor().mPositionListener.onScrollChanged();
- }
- // Internal scroll affects the clip boundaries
- getEditor().invalidateTextDisplayList();
+ getEditor().onScrollChanged();
}
}
- /**
- * Removes the suggestion spans.
- */
- CharSequence removeSuggestionSpans(CharSequence text) {
- if (text instanceof Spanned) {
- Spannable spannable;
- if (text instanceof Spannable) {
- spannable = (Spannable) text;
- } else {
- spannable = new SpannableString(text);
- text = spannable;
- }
-
- SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
- for (int i = 0; i < spans.length; i++) {
- spannable.removeSpan(spans[i]);
- }
- }
- return text;
- }
-
- void showSuggestions() {
- if (getEditor().mSuggestionsPopupWindow == null) {
- getEditor().mSuggestionsPopupWindow = new SuggestionsPopupWindow();
- }
- hideControllers();
- getEditor().mSuggestionsPopupWindow.show();
- }
-
- boolean areSuggestionsShown() {
- return getEditor().mSuggestionsPopupWindow != null && getEditor().mSuggestionsPopupWindow.isShowing();
- }
-
/**
* Return whether or not suggestions are enabled on this TextView. The suggestions are generated
* by the IME or by the spell checker as the user types. This is done by adding
@@ -8390,66 +7554,101 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return mEditor == null ? null : getEditor().mCustomSelectionActionModeCallback;
}
- /**
- *
- * @return true if the selection mode was actually started.
- */
- private boolean startSelectionActionMode() {
- if (getEditor().mSelectionActionMode != null) {
- // Selection action mode is already started
- return false;
- }
-
- if (!canSelectText() || !requestFocus()) {
- Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
- return false;
- }
-
- if (!hasSelection()) {
- // There may already be a selection on device rotation
- if (!selectCurrentWord()) {
- // No word found under cursor or text selection not permitted.
- return false;
- }
- }
-
- boolean willExtract = extractedTextModeWillBeStarted();
-
- // Do not start the action mode when extracted text will show up full screen, which would
- // immediately hide the newly created action bar and would be visually distracting.
- if (!willExtract) {
- ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
- getEditor().mSelectionActionMode = startActionMode(actionModeCallback);
- }
-
- final boolean selectionStarted = getEditor().mSelectionActionMode != null || willExtract;
- if (selectionStarted && !isTextSelectable()) {
- // Show the IME to be able to replace text, except when selecting non editable text.
- final InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) {
- imm.showSoftInput(this, 0, null);
- }
- }
-
- return selectionStarted;
- }
-
- private boolean extractedTextModeWillBeStarted() {
- if (!(this instanceof ExtractEditText)) {
- final InputMethodManager imm = InputMethodManager.peekInstance();
- return imm != null && imm.isFullscreenMode();
- }
- return false;
- }
-
/**
* @hide
*/
protected void stopSelectionActionMode() {
- if (getEditor().mSelectionActionMode != null) {
- // This will hide the mSelectionModifierCursorController
- getEditor().mSelectionActionMode.finish();
+ getEditor().stopSelectionActionMode();
+ }
+
+ boolean canCut() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
}
+
+ if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean canCopy() {
+ if (hasPasswordTransformationMethod()) {
+ return false;
+ }
+
+ if (mText.length() > 0 && hasSelection()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean canPaste() {
+ return (mText instanceof Editable &&
+ mEditor != null && getEditor().mKeyListener != null &&
+ getSelectionStart() >= 0 &&
+ getSelectionEnd() >= 0 &&
+ ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
+ hasPrimaryClip());
+ }
+
+ boolean selectAllText() {
+ final int length = mText.length();
+ Selection.setSelection((Spannable) mText, 0, length);
+ return length > 0;
+ }
+
+ /**
+ * Prepare text so that there are not zero or two spaces at beginning and end of region defined
+ * by [min, max] when replacing this region by paste.
+ * Note that if there were two spaces (or more) at that position before, they are kept. We just
+ * make sure we do not add an extra one from the paste content.
+ */
+ long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
+ if (paste.length() > 0) {
+ if (min > 0) {
+ final char charBefore = mTransformed.charAt(min - 1);
+ final char charAfter = paste.charAt(0);
+
+ if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
+ // Two spaces at beginning of paste: remove one
+ final int originalLength = mText.length();
+ deleteText_internal(min - 1, min);
+ // Due to filters, there is no guarantee that exactly one character was
+ // removed: count instead.
+ final int delta = mText.length() - originalLength;
+ min += delta;
+ max += delta;
+ } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
+ !Character.isSpaceChar(charAfter) && charAfter != '\n') {
+ // No space at beginning of paste: add one
+ final int originalLength = mText.length();
+ replaceText_internal(min, min, " ");
+ // Taking possible filters into account as above.
+ final int delta = mText.length() - originalLength;
+ min += delta;
+ max += delta;
+ }
+ }
+
+ if (max < mText.length()) {
+ final char charBefore = paste.charAt(paste.length() - 1);
+ final char charAfter = mTransformed.charAt(max);
+
+ if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
+ // Two spaces at end of paste: remove one
+ deleteText_internal(max, max + 1);
+ } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
+ !Character.isSpaceChar(charAfter) && charAfter != '\n') {
+ // No space at end of paste: add one
+ replaceText_internal(max, max, " ");
+ }
+ }
+ }
+
+ return TextUtils.packRangeInLong(min, max);
}
/**
@@ -8489,36 +7688,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
LAST_CUT_OR_COPY_TIME = SystemClock.uptimeMillis();
}
- private void hideInsertionPointCursorController() {
- // No need to create the controller to hide it.
- if (getEditor().mInsertionPointCursorController != null) {
- getEditor().mInsertionPointCursorController.hide();
- }
- }
-
- /**
- * Hides the insertion controller and stops text selection mode, hiding the selection controller
- */
- private void hideControllers() {
- hideCursorControllers();
- hideSpanControllers();
- }
-
- private void hideSpanControllers() {
- if (mChangeWatcher != null) {
- mChangeWatcher.hideControllers();
- }
- }
-
- private void hideCursorControllers() {
- if (getEditor().mSuggestionsPopupWindow != null && !getEditor().mSuggestionsPopupWindow.isShowingUp()) {
- // Should be done before hide insertion point controller since it triggers a show of it
- getEditor().mSuggestionsPopupWindow.hide();
- }
- hideInsertionPointCursorController();
- stopSelectionActionMode();
- }
-
/**
* Get the character offset closest to the specified absolute position. A typical use case is to
* pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
@@ -8535,7 +7704,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return offset;
}
- private float convertToLocalHorizontalCoordinate(float x) {
+ float convertToLocalHorizontalCoordinate(float x) {
x -= getTotalPaddingLeft();
// Clamp the position to inside of the view.
x = Math.max(0.0f, x);
@@ -8544,7 +7713,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return x;
}
- private int getLineAtCoordinate(float y) {
+ int getLineAtCoordinate(float y) {
y -= getTotalPaddingTop();
// Clamp the position to inside of the view.
y = Math.max(0.0f, y);
@@ -8558,25 +7727,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return getLayout().getOffsetForHorizontal(line, x);
}
- /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
- * in the view. Returns false when the position is in the empty space of left/right of text.
- */
- private boolean isPositionOnText(float x, float y) {
- if (getLayout() == null) return false;
-
- final int line = getLineAtCoordinate(y);
- x = convertToLocalHorizontalCoordinate(x);
-
- if (x < getLayout().getLineLeft(line)) return false;
- if (x > getLayout().getLineRight(line)) return false;
- return true;
- }
-
@Override
public boolean onDragEvent(DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
- return mEditor != null && hasInsertionController();
+ return mEditor != null && getEditor().hasInsertionController();
case DragEvent.ACTION_DRAG_ENTERED:
TextView.this.requestFocus();
@@ -8588,7 +7743,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return true;
case DragEvent.ACTION_DROP:
- onDrop(event);
+ if (mEditor != null) getEditor().onDrop(event);
return true;
case DragEvent.ACTION_DRAG_ENDED:
@@ -8598,112 +7753,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- private void onDrop(DragEvent event) {
- StringBuilder content = new StringBuilder("");
- ClipData clipData = event.getClipData();
- final int itemCount = clipData.getItemCount();
- for (int i=0; i < itemCount; i++) {
- Item item = clipData.getItemAt(i);
- content.append(item.coerceToText(TextView.this.mContext));
- }
-
- final int offset = getOffsetForPosition(event.getX(), event.getY());
-
- Object localState = event.getLocalState();
- DragLocalState dragLocalState = null;
- if (localState instanceof DragLocalState) {
- dragLocalState = (DragLocalState) localState;
- }
- boolean dragDropIntoItself = dragLocalState != null &&
- dragLocalState.sourceTextView == this;
-
- if (dragDropIntoItself) {
- if (offset >= dragLocalState.start && offset < dragLocalState.end) {
- // A drop inside the original selection discards the drop.
- return;
- }
- }
-
- final int originalLength = mText.length();
- long minMax = prepareSpacesAroundPaste(offset, offset, content);
- int min = TextUtils.unpackRangeStartFromLong(minMax);
- int max = TextUtils.unpackRangeEndFromLong(minMax);
-
- Selection.setSelection((Spannable) mText, max);
- replaceText_internal(min, max, content);
-
- if (dragDropIntoItself) {
- int dragSourceStart = dragLocalState.start;
- int dragSourceEnd = dragLocalState.end;
- if (max <= dragSourceStart) {
- // Inserting text before selection has shifted positions
- final int shift = mText.length() - originalLength;
- dragSourceStart += shift;
- dragSourceEnd += shift;
- }
-
- // Delete original selection
- deleteText_internal(dragSourceStart, dragSourceEnd);
-
- // Make sure we do not leave two adjacent spaces.
- if ((dragSourceStart == 0 ||
- Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
- (dragSourceStart == mText.length() ||
- Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
- final int pos = dragSourceStart == mText.length() ?
- dragSourceStart - 1 : dragSourceStart;
- deleteText_internal(pos, pos + 1);
- }
- }
- }
-
- /**
- * @return True if this view supports insertion handles.
- */
- boolean hasInsertionController() {
- return getEditor().mInsertionControllerEnabled;
- }
-
- /**
- * @return True if this view supports selection handles.
- */
- boolean hasSelectionController() {
- return getEditor().mSelectionControllerEnabled;
- }
-
- InsertionPointCursorController getInsertionController() {
- if (!getEditor().mInsertionControllerEnabled) {
- return null;
- }
-
- if (getEditor().mInsertionPointCursorController == null) {
- getEditor().mInsertionPointCursorController = new InsertionPointCursorController();
-
- final ViewTreeObserver observer = getViewTreeObserver();
- observer.addOnTouchModeChangeListener(getEditor().mInsertionPointCursorController);
- }
-
- return getEditor().mInsertionPointCursorController;
- }
-
- SelectionModifierCursorController getSelectionController() {
- if (!getEditor().mSelectionControllerEnabled) {
- return null;
- }
-
- if (getEditor().mSelectionModifierCursorController == null) {
- getEditor().mSelectionModifierCursorController = new SelectionModifierCursorController();
-
- final ViewTreeObserver observer = getViewTreeObserver();
- observer.addOnTouchModeChangeListener(getEditor().mSelectionModifierCursorController);
- }
-
- return getEditor().mSelectionModifierCursorController;
- }
-
boolean isInBatchEditMode() {
if (mEditor == null) return false;
- final InputMethodState ims = getEditor().mInputMethodState;
+ final Editor.InputMethodState ims = getEditor().mInputMethodState;
if (ims != null) {
return ims.mBatchEditNesting > 0;
}
@@ -8864,7 +7916,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (!(this instanceof EditText)) {
Log.e(LOG_TAG + " EDITOR", "Creating Editor on TextView. " + reason);
}
- mEditor = new Editor();
+ mEditor = new Editor(this);
} else {
if (!(this instanceof EditText)) {
Log.d(LOG_TAG + " EDITOR", "Redundant Editor creation. " + reason);
@@ -9041,151 +8093,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- private static class ErrorPopup extends PopupWindow {
- private boolean mAbove = false;
- private final TextView mView;
- private int mPopupInlineErrorBackgroundId = 0;
- private int mPopupInlineErrorAboveBackgroundId = 0;
-
- ErrorPopup(TextView v, int width, int height) {
- super(v, width, height);
- mView = v;
- // Make sure the TextView has a background set as it will be used the first time it is
- // shown and positionned. Initialized with below background, which should have
- // dimensions identical to the above version for this to work (and is more likely).
- mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
- com.android.internal.R.styleable.Theme_errorMessageBackground);
- mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
- }
-
- void fixDirection(boolean above) {
- mAbove = above;
-
- if (above) {
- mPopupInlineErrorAboveBackgroundId =
- getResourceId(mPopupInlineErrorAboveBackgroundId,
- com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
- } else {
- mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
- com.android.internal.R.styleable.Theme_errorMessageBackground);
- }
-
- mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
- mPopupInlineErrorBackgroundId);
- }
-
- private int getResourceId(int currentId, int index) {
- if (currentId == 0) {
- TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
- R.styleable.Theme);
- currentId = styledAttributes.getResourceId(index, 0);
- styledAttributes.recycle();
- }
- return currentId;
- }
-
- @Override
- public void update(int x, int y, int w, int h, boolean force) {
- super.update(x, y, w, h, force);
-
- boolean above = isAboveAnchor();
- if (above != mAbove) {
- fixDirection(above);
- }
- }
- }
-
- private class CorrectionHighlighter {
- private final Path mPath = new Path();
- private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private int mStart, mEnd;
- private long mFadingStartTime;
- private final static int FADE_OUT_DURATION = 400;
-
- public CorrectionHighlighter() {
- mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
- mPaint.setStyle(Paint.Style.FILL);
- }
-
- public void highlight(CorrectionInfo info) {
- mStart = info.getOffset();
- mEnd = mStart + info.getNewText().length();
- mFadingStartTime = SystemClock.uptimeMillis();
-
- if (mStart < 0 || mEnd < 0) {
- stopAnimation();
- }
- }
-
- public void draw(Canvas canvas, int cursorOffsetVertical) {
- if (updatePath() && updatePaint()) {
- if (cursorOffsetVertical != 0) {
- canvas.translate(0, cursorOffsetVertical);
- }
-
- canvas.drawPath(mPath, mPaint);
-
- if (cursorOffsetVertical != 0) {
- canvas.translate(0, -cursorOffsetVertical);
- }
- invalidate(true); // TODO invalidate cursor region only
- } else {
- stopAnimation();
- invalidate(false); // TODO invalidate cursor region only
- }
- }
-
- private boolean updatePaint() {
- final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
- if (duration > FADE_OUT_DURATION) return false;
-
- final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
- final int highlightColorAlpha = Color.alpha(mHighlightColor);
- final int color = (mHighlightColor & 0x00FFFFFF) +
- ((int) (highlightColorAlpha * coef) << 24);
- mPaint.setColor(color);
- return true;
- }
-
- private boolean updatePath() {
- final Layout layout = TextView.this.mLayout;
- if (layout == null) return false;
-
- // Update in case text is edited while the animation is run
- final int length = mText.length();
- int start = Math.min(length, mStart);
- int end = Math.min(length, mEnd);
-
- mPath.reset();
- TextView.this.mLayout.getSelectionPath(start, end, mPath);
- return true;
- }
-
- private void invalidate(boolean delayed) {
- if (TextView.this.mLayout == null) return;
-
- synchronized (TEMP_RECTF) {
- mPath.computeBounds(TEMP_RECTF, false);
-
- int left = getCompoundPaddingLeft();
- int top = getExtendedPaddingTop() + getVerticalOffset(true);
-
- if (delayed) {
- TextView.this.postInvalidateOnAnimation(
- left + (int) TEMP_RECTF.left, top + (int) TEMP_RECTF.top,
- left + (int) TEMP_RECTF.right, top + (int) TEMP_RECTF.bottom);
- } else {
- TextView.this.postInvalidate((int) TEMP_RECTF.left, (int) TEMP_RECTF.top,
- (int) TEMP_RECTF.right, (int) TEMP_RECTF.bottom);
- }
- }
- }
-
- private void stopAnimation() {
- TextView.this.getEditor().mCorrectionHighlighter = null;
- }
- }
-
private static final class Marquee extends Handler {
// TODO: Add an option to configure this
private static final float MARQUEE_DELTA_MAX = 0.07f;
@@ -9322,217 +8229,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- /**
- * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
- * pop-up should be displayed.
- */
- private class EasyEditSpanController {
-
- private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
-
- private EasyEditPopupWindow mPopupWindow;
-
- private EasyEditSpan mEasyEditSpan;
-
- private Runnable mHidePopup;
-
- private void hide() {
- if (mPopupWindow != null) {
- mPopupWindow.hide();
- TextView.this.removeCallbacks(mHidePopup);
- }
- removeSpans(mText);
- mEasyEditSpan = null;
- }
-
- /**
- * Monitors the changes in the text.
- *
- *