blob: cbe7511f918fe66c13f3e7403226ac604cdf68a5 [file] [log] [blame]
Gilles Debunned88876a2012-03-16 17:34:04 -07001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070019import android.content.UndoManager;
20import android.content.UndoOperation;
21import android.content.UndoOwner;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.text.InputFilter;
25import android.text.SpannableString;
John Reck44fd8d22014-02-26 11:00:11 -080026
Adam Powell057a5852012-05-11 10:28:38 -070027import com.android.internal.util.ArrayUtils;
Adam Lesinski776abc22014-03-07 11:30:59 -050028import com.android.internal.util.GrowingArrayUtils;
Adam Powell057a5852012-05-11 10:28:38 -070029import com.android.internal.widget.EditableInputConnection;
30
Gilles Debunned88876a2012-03-16 17:34:04 -070031import android.R;
Luca Zanolin1b15ba52013-02-20 14:31:37 +000032import android.app.PendingIntent;
33import android.app.PendingIntent.CanceledException;
Gilles Debunned88876a2012-03-16 17:34:04 -070034import android.content.ClipData;
35import android.content.ClipData.Item;
36import android.content.Context;
37import android.content.Intent;
38import android.content.pm.PackageManager;
39import android.content.res.TypedArray;
40import android.graphics.Canvas;
41import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090042import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070043import android.graphics.Paint;
44import android.graphics.Path;
45import android.graphics.Rect;
46import android.graphics.RectF;
47import android.graphics.drawable.Drawable;
48import android.inputmethodservice.ExtractEditText;
49import android.os.Bundle;
50import android.os.Handler;
51import android.os.SystemClock;
52import android.provider.Settings;
53import android.text.DynamicLayout;
54import android.text.Editable;
55import android.text.InputType;
56import android.text.Layout;
57import android.text.ParcelableSpan;
58import android.text.Selection;
59import android.text.SpanWatcher;
60import android.text.Spannable;
61import android.text.SpannableStringBuilder;
62import android.text.Spanned;
63import android.text.StaticLayout;
64import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070065import android.text.method.KeyListener;
66import android.text.method.MetaKeyKeyListener;
67import android.text.method.MovementMethod;
68import android.text.method.PasswordTransformationMethod;
69import android.text.method.WordIterator;
70import android.text.style.EasyEditSpan;
71import android.text.style.SuggestionRangeSpan;
72import android.text.style.SuggestionSpan;
73import android.text.style.TextAppearanceSpan;
74import android.text.style.URLSpan;
75import android.util.DisplayMetrics;
76import android.util.Log;
77import android.view.ActionMode;
78import android.view.ActionMode.Callback;
John Reckf666ad72014-03-14 16:24:57 -070079import android.view.RenderNode;
Gilles Debunned88876a2012-03-16 17:34:04 -070080import android.view.DragEvent;
81import android.view.Gravity;
82import android.view.HardwareCanvas;
John Reck44fd8d22014-02-26 11:00:11 -080083import android.view.HardwareRenderer;
Gilles Debunned88876a2012-03-16 17:34:04 -070084import android.view.LayoutInflater;
85import android.view.Menu;
86import android.view.MenuItem;
87import android.view.MotionEvent;
88import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -070089import android.view.View.DragShadowBuilder;
90import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -070091import android.view.ViewConfiguration;
92import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -070093import android.view.ViewGroup.LayoutParams;
94import android.view.ViewParent;
95import android.view.ViewTreeObserver;
96import android.view.WindowManager;
97import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090098import android.view.inputmethod.CursorAnchorInfo;
99import android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder;
Gilles Debunned88876a2012-03-16 17:34:04 -0700100import android.view.inputmethod.EditorInfo;
101import android.view.inputmethod.ExtractedText;
102import android.view.inputmethod.ExtractedTextRequest;
103import android.view.inputmethod.InputConnection;
104import android.view.inputmethod.InputMethodManager;
105import android.widget.AdapterView.OnItemClickListener;
106import android.widget.TextView.Drawables;
107import android.widget.TextView.OnEditorActionListener;
108
Gilles Debunned88876a2012-03-16 17:34:04 -0700109import java.text.BreakIterator;
110import java.util.Arrays;
111import java.util.Comparator;
112import java.util.HashMap;
113
114/**
115 * Helper class used by TextView to handle editable text views.
116 *
117 * @hide
118 */
119public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700120 private static final String TAG = "Editor";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700121 static final boolean DEBUG_UNDO = false;
Adam Powell057a5852012-05-11 10:28:38 -0700122
Gilles Debunned88876a2012-03-16 17:34:04 -0700123 static final int BLINK = 500;
124 private static final float[] TEMP_POSITION = new float[2];
125 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
126
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700127 UndoManager mUndoManager;
128 UndoOwner mUndoOwner;
129 InputFilter mUndoInputFilter;
130
Gilles Debunned88876a2012-03-16 17:34:04 -0700131 // Cursor Controllers.
132 InsertionPointCursorController mInsertionPointCursorController;
133 SelectionModifierCursorController mSelectionModifierCursorController;
134 ActionMode mSelectionActionMode;
135 boolean mInsertionControllerEnabled;
136 boolean mSelectionControllerEnabled;
137
138 // Used to highlight a word when it is corrected by the IME
139 CorrectionHighlighter mCorrectionHighlighter;
140
141 InputContentType mInputContentType;
142 InputMethodState mInputMethodState;
143
John Reck7558aa72014-03-05 14:59:59 -0800144 private static class TextDisplayList {
John Reckf666ad72014-03-14 16:24:57 -0700145 RenderNode displayList;
John Reck7558aa72014-03-05 14:59:59 -0800146 boolean isDirty;
147 public TextDisplayList(String name) {
148 isDirty = true;
John Reckf666ad72014-03-14 16:24:57 -0700149 displayList = RenderNode.create(name);
John Reck7558aa72014-03-05 14:59:59 -0800150 }
151 boolean needsRecord() { return isDirty || !displayList.isValid(); }
152 }
153 TextDisplayList[] mTextDisplayLists;
Gilles Debunned88876a2012-03-16 17:34:04 -0700154
155 boolean mFrozenWithFocus;
156 boolean mSelectionMoved;
157 boolean mTouchFocusSelected;
158
159 KeyListener mKeyListener;
160 int mInputType = EditorInfo.TYPE_NULL;
161
162 boolean mDiscardNextActionUp;
163 boolean mIgnoreActionUpEvent;
164
165 long mShowCursor;
166 Blink mBlink;
167
168 boolean mCursorVisible = true;
169 boolean mSelectAllOnFocus;
170 boolean mTextIsSelectable;
171
172 CharSequence mError;
173 boolean mErrorWasChanged;
174 ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700175
Gilles Debunned88876a2012-03-16 17:34:04 -0700176 /**
177 * This flag is set if the TextView tries to display an error before it
178 * is attached to the window (so its position is still unknown).
179 * It causes the error to be shown later, when onAttachedToWindow()
180 * is called.
181 */
182 boolean mShowErrorAfterAttach;
183
184 boolean mInBatchEditControllers;
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700185 boolean mShowSoftInputOnFocus = true;
Adam Powell057a5852012-05-11 10:28:38 -0700186 boolean mPreserveDetachedSelection;
187 boolean mTemporaryDetach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700188
189 SuggestionsPopupWindow mSuggestionsPopupWindow;
190 SuggestionRangeSpan mSuggestionRangeSpan;
191 Runnable mShowSuggestionRunnable;
192
193 final Drawable[] mCursorDrawable = new Drawable[2];
194 int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
195
196 private Drawable mSelectHandleLeft;
197 private Drawable mSelectHandleRight;
198 private Drawable mSelectHandleCenter;
199
200 // Global listener that detects changes in the global position of the TextView
201 private PositionListener mPositionListener;
202
203 float mLastDownPositionX, mLastDownPositionY;
204 Callback mCustomSelectionActionModeCallback;
205
206 // Set when this TextView gained focus with some text selected. Will start selection mode.
207 boolean mCreatedWithASelection;
208
Jean Chalardbaf30942013-02-28 16:01:51 -0800209 // The span controller helps monitoring the changes to which the Editor needs to react:
210 // - EasyEditSpans, for which we have some UI to display on attach and on hide
211 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
212 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700213
214 WordIterator mWordIterator;
215 SpellChecker mSpellChecker;
216
217 private Rect mTempRect;
218
219 private TextView mTextView;
220
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900221 final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier();
222
Gilles Debunned88876a2012-03-16 17:34:04 -0700223 Editor(TextView textView) {
224 mTextView = textView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700225 }
226
227 void onAttachedToWindow() {
228 if (mShowErrorAfterAttach) {
229 showError();
230 mShowErrorAfterAttach = false;
231 }
Adam Powell057a5852012-05-11 10:28:38 -0700232 mTemporaryDetach = false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700233
234 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
235 // No need to create the controller.
236 // The get method will add the listener on controller creation.
237 if (mInsertionPointCursorController != null) {
238 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
239 }
240 if (mSelectionModifierCursorController != null) {
Adam Powell057a5852012-05-11 10:28:38 -0700241 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunned88876a2012-03-16 17:34:04 -0700242 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
243 }
244 updateSpellCheckSpans(0, mTextView.getText().length(),
245 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700246
247 if (mTextView.hasTransientState() &&
248 mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
249 // Since transient state is reference counted make sure it stays matched
250 // with our own calls to it for managing selection.
251 // The action mode callback will set this back again when/if the action mode starts.
252 mTextView.setHasTransientState(false);
253
254 // We had an active selection from before, start the selection mode.
255 startSelectionActionMode();
256 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900257
258 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Gilles Debunned88876a2012-03-16 17:34:04 -0700259 }
260
261 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900262 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
263
Gilles Debunned88876a2012-03-16 17:34:04 -0700264 if (mError != null) {
265 hideError();
266 }
267
268 if (mBlink != null) {
269 mBlink.removeCallbacks(mBlink);
270 }
271
272 if (mInsertionPointCursorController != null) {
273 mInsertionPointCursorController.onDetached();
274 }
275
276 if (mSelectionModifierCursorController != null) {
277 mSelectionModifierCursorController.onDetached();
278 }
279
280 if (mShowSuggestionRunnable != null) {
281 mTextView.removeCallbacks(mShowSuggestionRunnable);
282 }
283
John Reck7558aa72014-03-05 14:59:59 -0800284 destroyDisplayListsData();
Gilles Debunned88876a2012-03-16 17:34:04 -0700285
286 if (mSpellChecker != null) {
287 mSpellChecker.closeSession();
288 // Forces the creation of a new SpellChecker next time this window is created.
289 // Will handle the cases where the settings has been changed in the meantime.
290 mSpellChecker = null;
291 }
292
Adam Powell057a5852012-05-11 10:28:38 -0700293 mPreserveDetachedSelection = true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700294 hideControllers();
Adam Powell057a5852012-05-11 10:28:38 -0700295 mPreserveDetachedSelection = false;
296 mTemporaryDetach = false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700297 }
298
John Reck7558aa72014-03-05 14:59:59 -0800299 private void destroyDisplayListsData() {
John Reck7558aa72014-03-05 14:59:59 -0800300 if (mTextDisplayLists != null) {
301 for (int i = 0; i < mTextDisplayLists.length; i++) {
John Reckf666ad72014-03-14 16:24:57 -0700302 RenderNode displayList = mTextDisplayLists[i] != null
John Reck7558aa72014-03-05 14:59:59 -0800303 ? mTextDisplayLists[i].displayList : null;
304 if (displayList != null && displayList.isValid()) {
John Reckbe34f2f2014-03-10 08:58:44 -0700305 displayList.destroyDisplayListData();
John Reck7558aa72014-03-05 14:59:59 -0800306 }
307 }
308 }
309 }
310
Gilles Debunned88876a2012-03-16 17:34:04 -0700311 private void showError() {
312 if (mTextView.getWindowToken() == null) {
313 mShowErrorAfterAttach = true;
314 return;
315 }
316
317 if (mErrorPopup == null) {
318 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
319 final TextView err = (TextView) inflater.inflate(
320 com.android.internal.R.layout.textview_hint, null);
321
322 final float scale = mTextView.getResources().getDisplayMetrics().density;
323 mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
324 mErrorPopup.setFocusable(false);
325 // The user is entering text, so the input method is needed. We
326 // don't want the popup to be displayed on top of it.
327 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
328 }
329
330 TextView tv = (TextView) mErrorPopup.getContentView();
331 chooseSize(mErrorPopup, mError, tv);
332 tv.setText(mError);
333
334 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
335 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
336 }
337
338 public void setError(CharSequence error, Drawable icon) {
339 mError = TextUtils.stringOrSpannedString(error);
340 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800341
Gilles Debunned88876a2012-03-16 17:34:04 -0700342 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800343 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700344 if (mErrorPopup != null) {
345 if (mErrorPopup.isShowing()) {
346 mErrorPopup.dismiss();
347 }
348
349 mErrorPopup = null;
350 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200351 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800352 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800353 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800354 if (mTextView.isFocused()) {
355 showError();
356 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800357 }
358 }
359
360 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800361 Drawables dr = mTextView.mDrawables;
362 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700363 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700364 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800365 dr.setErrorDrawable(icon, mTextView);
366
367 mTextView.resetResolvedDrawables();
368 mTextView.invalidate();
369 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700370 }
371
372 private void hideError() {
373 if (mErrorPopup != null) {
374 if (mErrorPopup.isShowing()) {
375 mErrorPopup.dismiss();
376 }
377 }
378
379 mShowErrorAfterAttach = false;
380 }
381
382 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800383 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700384 * at the middle of the error icon.
385 */
386 private int getErrorX() {
387 /*
388 * The "25" is the distance between the point and the right edge
389 * of the background
390 */
391 final float scale = mTextView.getResources().getDisplayMetrics().density;
392
393 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800394
395 final int layoutDirection = mTextView.getLayoutDirection();
396 int errorX;
397 int offset;
398 switch (layoutDirection) {
399 default:
400 case View.LAYOUT_DIRECTION_LTR:
401 offset = - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
402 errorX = mTextView.getWidth() - mErrorPopup.getWidth() -
403 mTextView.getPaddingRight() + offset;
404 break;
405 case View.LAYOUT_DIRECTION_RTL:
406 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
407 errorX = mTextView.getPaddingLeft() + offset;
408 break;
409 }
410 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700411 }
412
413 /**
414 * Returns the Y offset to make the pointy top of the error point
415 * at the bottom of the error icon.
416 */
417 private int getErrorY() {
418 /*
419 * Compound, not extended, because the icon is not clipped
420 * if the text height is smaller.
421 */
422 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
423 int vspace = mTextView.getBottom() - mTextView.getTop() -
424 mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
425
426 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800427
428 final int layoutDirection = mTextView.getLayoutDirection();
429 int height;
430 switch (layoutDirection) {
431 default:
432 case View.LAYOUT_DIRECTION_LTR:
433 height = (dr != null ? dr.mDrawableHeightRight : 0);
434 break;
435 case View.LAYOUT_DIRECTION_RTL:
436 height = (dr != null ? dr.mDrawableHeightLeft : 0);
437 break;
438 }
439
440 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700441
442 /*
443 * The "2" is the distance between the point and the top edge
444 * of the background.
445 */
446 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800447 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700448 }
449
450 void createInputContentTypeIfNeeded() {
451 if (mInputContentType == null) {
452 mInputContentType = new InputContentType();
453 }
454 }
455
456 void createInputMethodStateIfNeeded() {
457 if (mInputMethodState == null) {
458 mInputMethodState = new InputMethodState();
459 }
460 }
461
462 boolean isCursorVisible() {
463 // The default value is true, even when there is no associated Editor
464 return mCursorVisible && mTextView.isTextEditable();
465 }
466
467 void prepareCursorControllers() {
468 boolean windowSupportsHandles = false;
469
470 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
471 if (params instanceof WindowManager.LayoutParams) {
472 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
473 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
474 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
475 }
476
477 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
478 mInsertionControllerEnabled = enabled && isCursorVisible();
479 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
480
481 if (!mInsertionControllerEnabled) {
482 hideInsertionPointCursorController();
483 if (mInsertionPointCursorController != null) {
484 mInsertionPointCursorController.onDetached();
485 mInsertionPointCursorController = null;
486 }
487 }
488
489 if (!mSelectionControllerEnabled) {
490 stopSelectionActionMode();
491 if (mSelectionModifierCursorController != null) {
492 mSelectionModifierCursorController.onDetached();
493 mSelectionModifierCursorController = null;
494 }
495 }
496 }
497
498 private void hideInsertionPointCursorController() {
499 if (mInsertionPointCursorController != null) {
500 mInsertionPointCursorController.hide();
501 }
502 }
503
504 /**
505 * Hides the insertion controller and stops text selection mode, hiding the selection controller
506 */
507 void hideControllers() {
508 hideCursorControllers();
509 hideSpanControllers();
510 }
511
512 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800513 if (mSpanController != null) {
514 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700515 }
516 }
517
518 private void hideCursorControllers() {
519 if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
520 // Should be done before hide insertion point controller since it triggers a show of it
521 mSuggestionsPopupWindow.hide();
522 }
523 hideInsertionPointCursorController();
524 stopSelectionActionMode();
525 }
526
527 /**
528 * Create new SpellCheckSpans on the modified region.
529 */
530 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900531 // Remove spans whose adjacent characters are text not punctuation
532 mTextView.removeAdjacentSuggestionSpans(start);
533 mTextView.removeAdjacentSuggestionSpans(end);
534
Gilles Debunned88876a2012-03-16 17:34:04 -0700535 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
536 !(mTextView instanceof ExtractEditText)) {
537 if (mSpellChecker == null && createSpellChecker) {
538 mSpellChecker = new SpellChecker(mTextView);
539 }
540 if (mSpellChecker != null) {
541 mSpellChecker.spellCheck(start, end);
542 }
543 }
544 }
545
546 void onScreenStateChanged(int screenState) {
547 switch (screenState) {
548 case View.SCREEN_STATE_ON:
549 resumeBlink();
550 break;
551 case View.SCREEN_STATE_OFF:
552 suspendBlink();
553 break;
554 }
555 }
556
557 private void suspendBlink() {
558 if (mBlink != null) {
559 mBlink.cancel();
560 }
561 }
562
563 private void resumeBlink() {
564 if (mBlink != null) {
565 mBlink.uncancel();
566 makeBlink();
567 }
568 }
569
570 void adjustInputType(boolean password, boolean passwordInputType,
571 boolean webPasswordInputType, boolean numberPasswordInputType) {
572 // mInputType has been set from inputType, possibly modified by mInputMethod.
573 // Specialize mInputType to [web]password if we have a text class and the original input
574 // type was a password.
575 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
576 if (password || passwordInputType) {
577 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
578 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
579 }
580 if (webPasswordInputType) {
581 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
582 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
583 }
584 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
585 if (numberPasswordInputType) {
586 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
587 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
588 }
589 }
590 }
591
592 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
593 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
594 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
595
596 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
597 com.android.internal.R.dimen.textview_error_popup_default_width);
598 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
599 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
600 float max = 0;
601 for (int i = 0; i < l.getLineCount(); i++) {
602 max = Math.max(max, l.getLineWidth(i));
603 }
604
605 /*
606 * Now set the popup size to be big enough for the text plus the border capped
607 * to DEFAULT_MAX_POPUP_WIDTH
608 */
609 pop.setWidth(wid + (int) Math.ceil(max));
610 pop.setHeight(ht + l.getHeight());
611 }
612
613 void setFrame() {
614 if (mErrorPopup != null) {
615 TextView tv = (TextView) mErrorPopup.getContentView();
616 chooseSize(mErrorPopup, mError, tv);
617 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
618 mErrorPopup.getWidth(), mErrorPopup.getHeight());
619 }
620 }
621
622 /**
623 * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state
624 * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have
625 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
626 */
627 private boolean canSelectText() {
628 return hasSelectionController() && mTextView.getText().length() != 0;
629 }
630
631 /**
632 * It would be better to rely on the input type for everything. A password inputType should have
633 * a password transformation. We should hence use isPasswordInputType instead of this method.
634 *
635 * We should:
636 * - Call setInputType in setKeyListener instead of changing the input type directly (which
637 * would install the correct transformation).
638 * - Refuse the installation of a non-password transformation in setTransformation if the input
639 * type is password.
640 *
641 * However, this is like this for legacy reasons and we cannot break existing apps. This method
642 * is useful since it matches what the user can see (obfuscated text or not).
643 *
644 * @return true if the current transformation method is of the password type.
645 */
646 private boolean hasPasswordTransformationMethod() {
647 return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
648 }
649
650 /**
651 * Adjusts selection to the word under last touch offset.
652 * Return true if the operation was successfully performed.
653 */
654 private boolean selectCurrentWord() {
655 if (!canSelectText()) {
656 return false;
657 }
658
659 if (hasPasswordTransformationMethod()) {
660 // Always select all on a password field.
661 // Cut/copy menu entries are not available for passwords, but being able to select all
662 // is however useful to delete or paste to replace the entire content.
663 return mTextView.selectAllText();
664 }
665
666 int inputType = mTextView.getInputType();
667 int klass = inputType & InputType.TYPE_MASK_CLASS;
668 int variation = inputType & InputType.TYPE_MASK_VARIATION;
669
670 // Specific text field types: select the entire text for these
671 if (klass == InputType.TYPE_CLASS_NUMBER ||
672 klass == InputType.TYPE_CLASS_PHONE ||
673 klass == InputType.TYPE_CLASS_DATETIME ||
674 variation == InputType.TYPE_TEXT_VARIATION_URI ||
675 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
676 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
677 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
678 return mTextView.selectAllText();
679 }
680
681 long lastTouchOffsets = getLastTouchOffsets();
682 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
683 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
684
685 // Safety check in case standard touch event handling has been bypassed
686 if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
687 if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
688
689 int selectionStart, selectionEnd;
690
691 // If a URLSpan (web address, email, phone...) is found at that position, select it.
692 URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
693 getSpans(minOffset, maxOffset, URLSpan.class);
694 if (urlSpans.length >= 1) {
695 URLSpan urlSpan = urlSpans[0];
696 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
697 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
698 } else {
699 final WordIterator wordIterator = getWordIterator();
700 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
701
702 selectionStart = wordIterator.getBeginning(minOffset);
703 selectionEnd = wordIterator.getEnd(maxOffset);
704
705 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
706 selectionStart == selectionEnd) {
707 // Possible when the word iterator does not properly handle the text's language
708 long range = getCharRange(minOffset);
709 selectionStart = TextUtils.unpackRangeStartFromLong(range);
710 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
711 }
712 }
713
714 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
715 return selectionEnd > selectionStart;
716 }
717
718 void onLocaleChanged() {
719 // Will be re-created on demand in getWordIterator with the proper new locale
720 mWordIterator = null;
721 }
722
723 /**
724 * @hide
725 */
726 public WordIterator getWordIterator() {
727 if (mWordIterator == null) {
728 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
729 }
730 return mWordIterator;
731 }
732
733 private long getCharRange(int offset) {
734 final int textLength = mTextView.getText().length();
735 if (offset + 1 < textLength) {
736 final char currentChar = mTextView.getText().charAt(offset);
737 final char nextChar = mTextView.getText().charAt(offset + 1);
738 if (Character.isSurrogatePair(currentChar, nextChar)) {
739 return TextUtils.packRangeInLong(offset, offset + 2);
740 }
741 }
742 if (offset < textLength) {
743 return TextUtils.packRangeInLong(offset, offset + 1);
744 }
745 if (offset - 2 >= 0) {
746 final char previousChar = mTextView.getText().charAt(offset - 1);
747 final char previousPreviousChar = mTextView.getText().charAt(offset - 2);
748 if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
749 return TextUtils.packRangeInLong(offset - 2, offset);
750 }
751 }
752 if (offset - 1 >= 0) {
753 return TextUtils.packRangeInLong(offset - 1, offset);
754 }
755 return TextUtils.packRangeInLong(offset, offset);
756 }
757
758 private boolean touchPositionIsInSelection() {
759 int selectionStart = mTextView.getSelectionStart();
760 int selectionEnd = mTextView.getSelectionEnd();
761
762 if (selectionStart == selectionEnd) {
763 return false;
764 }
765
766 if (selectionStart > selectionEnd) {
767 int tmp = selectionStart;
768 selectionStart = selectionEnd;
769 selectionEnd = tmp;
770 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
771 }
772
773 SelectionModifierCursorController selectionController = getSelectionController();
774 int minOffset = selectionController.getMinTouchOffset();
775 int maxOffset = selectionController.getMaxTouchOffset();
776
777 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
778 }
779
780 private PositionListener getPositionListener() {
781 if (mPositionListener == null) {
782 mPositionListener = new PositionListener();
783 }
784 return mPositionListener;
785 }
786
787 private interface TextViewPositionListener {
788 public void updatePosition(int parentPositionX, int parentPositionY,
789 boolean parentPositionChanged, boolean parentScrolled);
790 }
791
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900792 private boolean isPositionVisible(final float positionX, final float positionY) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700793 synchronized (TEMP_POSITION) {
794 final float[] position = TEMP_POSITION;
795 position[0] = positionX;
796 position[1] = positionY;
797 View view = mTextView;
798
799 while (view != null) {
800 if (view != mTextView) {
801 // Local scroll is already taken into account in positionX/Y
802 position[0] -= view.getScrollX();
803 position[1] -= view.getScrollY();
804 }
805
806 if (position[0] < 0 || position[1] < 0 ||
807 position[0] > view.getWidth() || position[1] > view.getHeight()) {
808 return false;
809 }
810
811 if (!view.getMatrix().isIdentity()) {
812 view.getMatrix().mapPoints(position);
813 }
814
815 position[0] += view.getLeft();
816 position[1] += view.getTop();
817
818 final ViewParent parent = view.getParent();
819 if (parent instanceof View) {
820 view = (View) parent;
821 } else {
822 // We've reached the ViewRoot, stop iterating
823 view = null;
824 }
825 }
826 }
827
828 // We've been able to walk up the view hierarchy and the position was never clipped
829 return true;
830 }
831
832 private boolean isOffsetVisible(int offset) {
833 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -0700834 if (layout == null) return false;
835
Gilles Debunned88876a2012-03-16 17:34:04 -0700836 final int line = layout.getLineForOffset(offset);
837 final int lineBottom = layout.getLineBottom(line);
838 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
839 return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
840 lineBottom + mTextView.viewportToContentVerticalOffset());
841 }
842
843 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
844 * in the view. Returns false when the position is in the empty space of left/right of text.
845 */
846 private boolean isPositionOnText(float x, float y) {
847 Layout layout = mTextView.getLayout();
848 if (layout == null) return false;
849
850 final int line = mTextView.getLineAtCoordinate(y);
851 x = mTextView.convertToLocalHorizontalCoordinate(x);
852
853 if (x < layout.getLineLeft(line)) return false;
854 if (x > layout.getLineRight(line)) return false;
855 return true;
856 }
857
858 public boolean performLongClick(boolean handled) {
859 // Long press in empty space moves cursor and shows the Paste affordance if available.
860 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
861 mInsertionControllerEnabled) {
862 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
863 mLastDownPositionY);
864 stopSelectionActionMode();
865 Selection.setSelection((Spannable) mTextView.getText(), offset);
866 getInsertionController().showWithActionPopup();
867 handled = true;
868 }
869
870 if (!handled && mSelectionActionMode != null) {
871 if (touchPositionIsInSelection()) {
872 // Start a drag
873 final int start = mTextView.getSelectionStart();
874 final int end = mTextView.getSelectionEnd();
875 CharSequence selectedText = mTextView.getTransformedText(start, end);
876 ClipData data = ClipData.newPlainText(null, selectedText);
877 DragLocalState localState = new DragLocalState(mTextView, start, end);
878 mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
879 stopSelectionActionMode();
880 } else {
881 getSelectionController().hide();
882 selectCurrentWord();
883 getSelectionController().show();
884 }
885 handled = true;
886 }
887
888 // Start a new selection
889 if (!handled) {
890 handled = startSelectionActionMode();
891 }
892
893 return handled;
894 }
895
896 private long getLastTouchOffsets() {
897 SelectionModifierCursorController selectionController = getSelectionController();
898 final int minOffset = selectionController.getMinTouchOffset();
899 final int maxOffset = selectionController.getMaxTouchOffset();
900 return TextUtils.packRangeInLong(minOffset, maxOffset);
901 }
902
903 void onFocusChanged(boolean focused, int direction) {
904 mShowCursor = SystemClock.uptimeMillis();
905 ensureEndedBatchEdit();
906
907 if (focused) {
908 int selStart = mTextView.getSelectionStart();
909 int selEnd = mTextView.getSelectionEnd();
910
911 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
912 // mode for these, unless there was a specific selection already started.
913 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
914 selEnd == mTextView.getText().length();
915
916 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
917 !isFocusHighlighted;
918
919 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
920 // If a tap was used to give focus to that view, move cursor at tap position.
921 // Has to be done before onTakeFocus, which can be overloaded.
922 final int lastTapPosition = getLastTapPosition();
923 if (lastTapPosition >= 0) {
924 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
925 }
926
927 // Note this may have to be moved out of the Editor class
928 MovementMethod mMovement = mTextView.getMovementMethod();
929 if (mMovement != null) {
930 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
931 }
932
933 // The DecorView does not have focus when the 'Done' ExtractEditText button is
934 // pressed. Since it is the ViewAncestor's mView, it requests focus before
935 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
936 // This special case ensure that we keep current selection in that case.
937 // It would be better to know why the DecorView does not have focus at that time.
938 if (((mTextView instanceof ExtractEditText) || mSelectionMoved) &&
939 selStart >= 0 && selEnd >= 0) {
940 /*
941 * Someone intentionally set the selection, so let them
942 * do whatever it is that they wanted to do instead of
943 * the default on-focus behavior. We reset the selection
944 * here instead of just skipping the onTakeFocus() call
945 * because some movement methods do something other than
946 * just setting the selection in theirs and we still
947 * need to go through that path.
948 */
949 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
950 }
951
952 if (mSelectAllOnFocus) {
953 mTextView.selectAllText();
954 }
955
956 mTouchFocusSelected = true;
957 }
958
959 mFrozenWithFocus = false;
960 mSelectionMoved = false;
961
962 if (mError != null) {
963 showError();
964 }
965
966 makeBlink();
967 } else {
968 if (mError != null) {
969 hideError();
970 }
971 // Don't leave us in the middle of a batch edit.
972 mTextView.onEndBatchEdit();
973
974 if (mTextView instanceof ExtractEditText) {
975 // terminateTextSelectionMode removes selection, which we want to keep when
976 // ExtractEditText goes out of focus.
977 final int selStart = mTextView.getSelectionStart();
978 final int selEnd = mTextView.getSelectionEnd();
979 hideControllers();
980 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
981 } else {
Adam Powell057a5852012-05-11 10:28:38 -0700982 if (mTemporaryDetach) mPreserveDetachedSelection = true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700983 hideControllers();
Adam Powell057a5852012-05-11 10:28:38 -0700984 if (mTemporaryDetach) mPreserveDetachedSelection = false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700985 downgradeEasyCorrectionSpans();
986 }
987
988 // No need to create the controller
989 if (mSelectionModifierCursorController != null) {
990 mSelectionModifierCursorController.resetTouchOffsets();
991 }
992 }
993 }
994
995 /**
996 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
997 * span.
998 */
999 private void downgradeEasyCorrectionSpans() {
1000 CharSequence text = mTextView.getText();
1001 if (text instanceof Spannable) {
1002 Spannable spannable = (Spannable) text;
1003 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1004 spannable.length(), SuggestionSpan.class);
1005 for (int i = 0; i < suggestionSpans.length; i++) {
1006 int flags = suggestionSpans[i].getFlags();
1007 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1008 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1009 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1010 suggestionSpans[i].setFlags(flags);
1011 }
1012 }
1013 }
1014 }
1015
1016 void sendOnTextChanged(int start, int after) {
1017 updateSpellCheckSpans(start, start + after, false);
1018
1019 // Hide the controllers as soon as text is modified (typing, procedural...)
1020 // We do not hide the span controllers, since they can be added when a new text is
1021 // inserted into the text view (voice IME).
1022 hideCursorControllers();
1023 }
1024
1025 private int getLastTapPosition() {
1026 // No need to create the controller at that point, no last tap position saved
1027 if (mSelectionModifierCursorController != null) {
1028 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1029 if (lastTapPosition >= 0) {
1030 // Safety check, should not be possible.
1031 if (lastTapPosition > mTextView.getText().length()) {
1032 lastTapPosition = mTextView.getText().length();
1033 }
1034 return lastTapPosition;
1035 }
1036 }
1037
1038 return -1;
1039 }
1040
1041 void onWindowFocusChanged(boolean hasWindowFocus) {
1042 if (hasWindowFocus) {
1043 if (mBlink != null) {
1044 mBlink.uncancel();
1045 makeBlink();
1046 }
1047 } else {
1048 if (mBlink != null) {
1049 mBlink.cancel();
1050 }
1051 if (mInputContentType != null) {
1052 mInputContentType.enterDown = false;
1053 }
1054 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
1055 hideControllers();
1056 if (mSuggestionsPopupWindow != null) {
1057 mSuggestionsPopupWindow.onParentLostFocus();
1058 }
1059
Gilles Debunnec72fba82012-06-26 14:47:07 -07001060 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1061 ensureEndedBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001062 }
1063 }
1064
1065 void onTouchEvent(MotionEvent event) {
1066 if (hasSelectionController()) {
1067 getSelectionController().onTouchEvent(event);
1068 }
1069
1070 if (mShowSuggestionRunnable != null) {
1071 mTextView.removeCallbacks(mShowSuggestionRunnable);
1072 mShowSuggestionRunnable = null;
1073 }
1074
1075 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1076 mLastDownPositionX = event.getX();
1077 mLastDownPositionY = event.getY();
1078
1079 // Reset this state; it will be re-set if super.onTouchEvent
1080 // causes focus to move to the view.
1081 mTouchFocusSelected = false;
1082 mIgnoreActionUpEvent = false;
1083 }
1084 }
1085
1086 public void beginBatchEdit() {
1087 mInBatchEditControllers = true;
1088 final InputMethodState ims = mInputMethodState;
1089 if (ims != null) {
1090 int nesting = ++ims.mBatchEditNesting;
1091 if (nesting == 1) {
1092 ims.mCursorChanged = false;
1093 ims.mChangedDelta = 0;
1094 if (ims.mContentChanged) {
1095 // We already have a pending change from somewhere else,
1096 // so turn this into a full update.
1097 ims.mChangedStart = 0;
1098 ims.mChangedEnd = mTextView.getText().length();
1099 } else {
1100 ims.mChangedStart = EXTRACT_UNKNOWN;
1101 ims.mChangedEnd = EXTRACT_UNKNOWN;
1102 ims.mContentChanged = false;
1103 }
1104 mTextView.onBeginBatchEdit();
1105 }
1106 }
1107 }
1108
1109 public void endBatchEdit() {
1110 mInBatchEditControllers = false;
1111 final InputMethodState ims = mInputMethodState;
1112 if (ims != null) {
1113 int nesting = --ims.mBatchEditNesting;
1114 if (nesting == 0) {
1115 finishBatchEdit(ims);
1116 }
1117 }
1118 }
1119
1120 void ensureEndedBatchEdit() {
1121 final InputMethodState ims = mInputMethodState;
1122 if (ims != null && ims.mBatchEditNesting != 0) {
1123 ims.mBatchEditNesting = 0;
1124 finishBatchEdit(ims);
1125 }
1126 }
1127
1128 void finishBatchEdit(final InputMethodState ims) {
1129 mTextView.onEndBatchEdit();
1130
1131 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1132 mTextView.updateAfterEdit();
1133 reportExtractedText();
1134 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001135 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001136 mTextView.invalidateCursor();
1137 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001138 // sendUpdateSelection knows to avoid sending if the selection did
1139 // not actually change.
1140 sendUpdateSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001141 }
1142
1143 static final int EXTRACT_NOTHING = -2;
1144 static final int EXTRACT_UNKNOWN = -1;
1145
1146 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1147 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1148 EXTRACT_UNKNOWN, outText);
1149 }
1150
1151 private boolean extractTextInternal(ExtractedTextRequest request,
1152 int partialStartOffset, int partialEndOffset, int delta,
1153 ExtractedText outText) {
1154 final CharSequence content = mTextView.getText();
1155 if (content != null) {
1156 if (partialStartOffset != EXTRACT_NOTHING) {
1157 final int N = content.length();
1158 if (partialStartOffset < 0) {
1159 outText.partialStartOffset = outText.partialEndOffset = -1;
1160 partialStartOffset = 0;
1161 partialEndOffset = N;
1162 } else {
1163 // Now use the delta to determine the actual amount of text
1164 // we need.
1165 partialEndOffset += delta;
1166 // Adjust offsets to ensure we contain full spans.
1167 if (content instanceof Spanned) {
1168 Spanned spanned = (Spanned)content;
1169 Object[] spans = spanned.getSpans(partialStartOffset,
1170 partialEndOffset, ParcelableSpan.class);
1171 int i = spans.length;
1172 while (i > 0) {
1173 i--;
1174 int j = spanned.getSpanStart(spans[i]);
1175 if (j < partialStartOffset) partialStartOffset = j;
1176 j = spanned.getSpanEnd(spans[i]);
1177 if (j > partialEndOffset) partialEndOffset = j;
1178 }
1179 }
1180 outText.partialStartOffset = partialStartOffset;
1181 outText.partialEndOffset = partialEndOffset - delta;
1182
1183 if (partialStartOffset > N) {
1184 partialStartOffset = N;
1185 } else if (partialStartOffset < 0) {
1186 partialStartOffset = 0;
1187 }
1188 if (partialEndOffset > N) {
1189 partialEndOffset = N;
1190 } else if (partialEndOffset < 0) {
1191 partialEndOffset = 0;
1192 }
1193 }
1194 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
1195 outText.text = content.subSequence(partialStartOffset,
1196 partialEndOffset);
1197 } else {
1198 outText.text = TextUtils.substring(content, partialStartOffset,
1199 partialEndOffset);
1200 }
1201 } else {
1202 outText.partialStartOffset = 0;
1203 outText.partialEndOffset = 0;
1204 outText.text = "";
1205 }
1206 outText.flags = 0;
1207 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1208 outText.flags |= ExtractedText.FLAG_SELECTING;
1209 }
1210 if (mTextView.isSingleLine()) {
1211 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1212 }
1213 outText.startOffset = 0;
1214 outText.selectionStart = mTextView.getSelectionStart();
1215 outText.selectionEnd = mTextView.getSelectionEnd();
1216 return true;
1217 }
1218 return false;
1219 }
1220
1221 boolean reportExtractedText() {
1222 final Editor.InputMethodState ims = mInputMethodState;
1223 if (ims != null) {
1224 final boolean contentChanged = ims.mContentChanged;
1225 if (contentChanged || ims.mSelectionModeChanged) {
1226 ims.mContentChanged = false;
1227 ims.mSelectionModeChanged = false;
Gilles Debunnec62589c2012-04-12 14:50:23 -07001228 final ExtractedTextRequest req = ims.mExtractedTextRequest;
Gilles Debunned88876a2012-03-16 17:34:04 -07001229 if (req != null) {
1230 InputMethodManager imm = InputMethodManager.peekInstance();
1231 if (imm != null) {
1232 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1233 "Retrieving extracted start=" + ims.mChangedStart +
1234 " end=" + ims.mChangedEnd +
1235 " delta=" + ims.mChangedDelta);
1236 if (ims.mChangedStart < 0 && !contentChanged) {
1237 ims.mChangedStart = EXTRACT_NOTHING;
1238 }
1239 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
Gilles Debunnec62589c2012-04-12 14:50:23 -07001240 ims.mChangedDelta, ims.mExtractedText)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001241 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1242 "Reporting extracted start=" +
Gilles Debunnec62589c2012-04-12 14:50:23 -07001243 ims.mExtractedText.partialStartOffset +
1244 " end=" + ims.mExtractedText.partialEndOffset +
1245 ": " + ims.mExtractedText.text);
1246
1247 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
Gilles Debunned88876a2012-03-16 17:34:04 -07001248 ims.mChangedStart = EXTRACT_UNKNOWN;
1249 ims.mChangedEnd = EXTRACT_UNKNOWN;
1250 ims.mChangedDelta = 0;
1251 ims.mContentChanged = false;
1252 return true;
1253 }
1254 }
1255 }
1256 }
1257 }
1258 return false;
1259 }
1260
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001261 private void sendUpdateSelection() {
1262 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1263 final InputMethodManager imm = InputMethodManager.peekInstance();
1264 if (null != imm) {
1265 final int selectionStart = mTextView.getSelectionStart();
1266 final int selectionEnd = mTextView.getSelectionEnd();
1267 int candStart = -1;
1268 int candEnd = -1;
1269 if (mTextView.getText() instanceof Spannable) {
1270 final Spannable sp = (Spannable) mTextView.getText();
1271 candStart = EditableInputConnection.getComposingSpanStart(sp);
1272 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1273 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001274 // InputMethodManager#updateSelection skips sending the message if
1275 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001276 imm.updateSelection(mTextView,
1277 selectionStart, selectionEnd, candStart, candEnd);
1278 }
1279 }
1280 }
1281
Gilles Debunned88876a2012-03-16 17:34:04 -07001282 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1283 int cursorOffsetVertical) {
1284 final int selectionStart = mTextView.getSelectionStart();
1285 final int selectionEnd = mTextView.getSelectionEnd();
1286
1287 final InputMethodState ims = mInputMethodState;
1288 if (ims != null && ims.mBatchEditNesting == 0) {
1289 InputMethodManager imm = InputMethodManager.peekInstance();
1290 if (imm != null) {
1291 if (imm.isActive(mTextView)) {
1292 boolean reported = false;
1293 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1294 // We are in extract mode and the content has changed
1295 // in some way... just report complete new text to the
1296 // input method.
1297 reported = reportExtractedText();
1298 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001299 }
1300
1301 if (imm.isWatchingCursor(mTextView) && highlight != null) {
1302 highlight.computeBounds(ims.mTmpRectF, true);
1303 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
1304
1305 canvas.getMatrix().mapPoints(ims.mTmpOffset);
1306 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
1307
1308 ims.mTmpRectF.offset(0, cursorOffsetVertical);
1309
1310 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
1311 (int)(ims.mTmpRectF.top + 0.5),
1312 (int)(ims.mTmpRectF.right + 0.5),
1313 (int)(ims.mTmpRectF.bottom + 0.5));
1314
1315 imm.updateCursor(mTextView,
1316 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
1317 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
1318 }
1319 }
1320 }
1321
1322 if (mCorrectionHighlighter != null) {
1323 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1324 }
1325
1326 if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
1327 drawCursor(canvas, cursorOffsetVertical);
1328 // Rely on the drawable entirely, do not draw the cursor line.
1329 // Has to be done after the IMM related code above which relies on the highlight.
1330 highlight = null;
1331 }
1332
1333 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1334 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1335 cursorOffsetVertical);
1336 } else {
1337 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1338 }
1339 }
1340
1341 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1342 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001343 final long lineRange = layout.getLineRangeForDraw(canvas);
1344 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1345 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1346 if (lastLine < 0) return;
1347
1348 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1349 firstLine, lastLine);
1350
1351 if (layout instanceof DynamicLayout) {
1352 if (mTextDisplayLists == null) {
Adam Lesinski776abc22014-03-07 11:30:59 -05001353 mTextDisplayLists = ArrayUtils.emptyArray(TextDisplayList.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001354 }
1355
1356 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001357 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001358 int[] blockIndices = dynamicLayout.getBlockIndices();
1359 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001360 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001361
Gilles Debunned88876a2012-03-16 17:34:04 -07001362 int endOfPreviousBlock = -1;
1363 int searchStartIndex = 0;
1364 for (int i = 0; i < numberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001365 int blockEndLine = blockEndLines[i];
Gilles Debunned88876a2012-03-16 17:34:04 -07001366 int blockIndex = blockIndices[i];
1367
1368 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1369 if (blockIsInvalid) {
1370 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1371 searchStartIndex);
Gilles Debunne157aafc2012-04-19 17:21:57 -07001372 // Note how dynamic layout's internal block indices get updated from Editor
Gilles Debunned88876a2012-03-16 17:34:04 -07001373 blockIndices[i] = blockIndex;
1374 searchStartIndex = blockIndex + 1;
1375 }
1376
John Reck7558aa72014-03-05 14:59:59 -08001377 if (mTextDisplayLists[blockIndex] == null) {
1378 mTextDisplayLists[blockIndex] =
1379 new TextDisplayList("Text " + blockIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001380 }
1381
John Reck7558aa72014-03-05 14:59:59 -08001382 final boolean blockDisplayListIsInvalid = mTextDisplayLists[blockIndex].needsRecord();
John Reckf666ad72014-03-14 16:24:57 -07001383 RenderNode blockDisplayList = mTextDisplayLists[blockIndex].displayList;
Sangkyu Lee955beb22012-12-10 15:47:00 +09001384 if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001385 final int blockBeginLine = endOfPreviousBlock + 1;
1386 final int top = layout.getLineTop(blockBeginLine);
1387 final int bottom = layout.getLineBottom(blockEndLine);
Gilles Debunnefd5bc012012-04-23 16:21:35 -07001388 int left = 0;
1389 int right = mTextView.getWidth();
1390 if (mTextView.getHorizontallyScrolling()) {
1391 float min = Float.MAX_VALUE;
1392 float max = Float.MIN_VALUE;
1393 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1394 min = Math.min(min, layout.getLineLeft(line));
1395 max = Math.max(max, layout.getLineRight(line));
1396 }
1397 left = (int) min;
1398 right = (int) (max + 0.5f);
1399 }
Gilles Debunne157aafc2012-04-19 17:21:57 -07001400
Sangkyu Lee955beb22012-12-10 15:47:00 +09001401 // Rebuild display list if it is invalid
1402 if (blockDisplayListIsInvalid) {
Romain Guy52036b12013-02-14 18:03:37 -08001403 final HardwareCanvas hardwareCanvas = blockDisplayList.start(
1404 right - left, bottom - top);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001405 try {
Romain Guy52036b12013-02-14 18:03:37 -08001406 // drawText is always relative to TextView's origin, this translation
1407 // brings this range of text back to the top left corner of the viewport
Sangkyu Lee955beb22012-12-10 15:47:00 +09001408 hardwareCanvas.translate(-left, -top);
1409 layout.drawText(hardwareCanvas, blockBeginLine, blockEndLine);
Romain Guy52036b12013-02-14 18:03:37 -08001410 // No need to untranslate, previous context is popped after
1411 // drawDisplayList
Sangkyu Lee955beb22012-12-10 15:47:00 +09001412 } finally {
John Reck8de65a82014-04-09 15:23:38 -07001413 blockDisplayList.end(hardwareCanvas);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001414 // Same as drawDisplayList below, handled by our TextView's parent
Chet Haasedd671592013-04-19 14:54:34 -07001415 blockDisplayList.setClipToBounds(false);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001416 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001417 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001418
1419 // Valid disply list whose index is >= indexFirstChangedBlock
1420 // only needs to update its drawing location.
1421 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
Gilles Debunned88876a2012-03-16 17:34:04 -07001422 }
1423
Chet Haase1271e2c2012-04-20 09:54:27 -07001424 ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, null,
Gilles Debunne157aafc2012-04-19 17:21:57 -07001425 0 /* no child clipping, our TextView parent enforces it */);
Gilles Debunnefd5bc012012-04-23 16:21:35 -07001426
Gilles Debunne157aafc2012-04-19 17:21:57 -07001427 endOfPreviousBlock = blockEndLine;
Gilles Debunned88876a2012-03-16 17:34:04 -07001428 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001429
1430 dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks);
Gilles Debunned88876a2012-03-16 17:34:04 -07001431 } else {
1432 // Boring layout is used for empty and hint text
1433 layout.drawText(canvas, firstLine, lastLine);
1434 }
1435 }
1436
1437 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1438 int searchStartIndex) {
1439 int length = mTextDisplayLists.length;
1440 for (int i = searchStartIndex; i < length; i++) {
1441 boolean blockIndexFound = false;
1442 for (int j = 0; j < numberOfBlocks; j++) {
1443 if (blockIndices[j] == i) {
1444 blockIndexFound = true;
1445 break;
1446 }
1447 }
1448 if (blockIndexFound) continue;
1449 return i;
1450 }
1451
1452 // No available index found, the pool has to grow
Adam Lesinski776abc22014-03-07 11:30:59 -05001453 mTextDisplayLists = GrowingArrayUtils.append(mTextDisplayLists, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001454 return length;
1455 }
1456
1457 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1458 final boolean translate = cursorOffsetVertical != 0;
1459 if (translate) canvas.translate(0, cursorOffsetVertical);
1460 for (int i = 0; i < mCursorCount; i++) {
1461 mCursorDrawable[i].draw(canvas);
1462 }
1463 if (translate) canvas.translate(0, -cursorOffsetVertical);
1464 }
1465
Gilles Debunneebc86af2012-04-20 15:10:47 -07001466 /**
1467 * Invalidates all the sub-display lists that overlap the specified character range
1468 */
1469 void invalidateTextDisplayList(Layout layout, int start, int end) {
1470 if (mTextDisplayLists != null && layout instanceof DynamicLayout) {
1471 final int firstLine = layout.getLineForOffset(start);
1472 final int lastLine = layout.getLineForOffset(end);
1473
1474 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1475 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1476 int[] blockIndices = dynamicLayout.getBlockIndices();
1477 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1478
1479 int i = 0;
1480 // Skip the blocks before firstLine
1481 while (i < numberOfBlocks) {
1482 if (blockEndLines[i] >= firstLine) break;
1483 i++;
1484 }
1485
1486 // Invalidate all subsequent blocks until lastLine is passed
1487 while (i < numberOfBlocks) {
1488 final int blockIndex = blockIndices[i];
1489 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
John Reck7558aa72014-03-05 14:59:59 -08001490 mTextDisplayLists[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07001491 }
1492 if (blockEndLines[i] >= lastLine) break;
1493 i++;
1494 }
1495 }
1496 }
1497
Gilles Debunned88876a2012-03-16 17:34:04 -07001498 void invalidateTextDisplayList() {
1499 if (mTextDisplayLists != null) {
1500 for (int i = 0; i < mTextDisplayLists.length; i++) {
John Reck7558aa72014-03-05 14:59:59 -08001501 if (mTextDisplayLists[i] != null) mTextDisplayLists[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001502 }
1503 }
1504 }
1505
1506 void updateCursorsPositions() {
1507 if (mTextView.mCursorDrawableRes == 0) {
1508 mCursorCount = 0;
1509 return;
1510 }
1511
1512 Layout layout = mTextView.getLayout();
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001513 Layout hintLayout = mTextView.getHintLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07001514 final int offset = mTextView.getSelectionStart();
1515 final int line = layout.getLineForOffset(offset);
1516 final int top = layout.getLineTop(line);
1517 final int bottom = layout.getLineTop(line + 1);
1518
1519 mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
1520
1521 int middle = bottom;
1522 if (mCursorCount == 2) {
1523 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
1524 middle = (top + bottom) >> 1;
1525 }
1526
Raph Levienafe8e9b2012-12-19 16:09:32 -08001527 boolean clamped = layout.shouldClampCursor(line);
1528 updateCursorPosition(0, top, middle,
1529 getPrimaryHorizontal(layout, hintLayout, offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001530
1531 if (mCursorCount == 2) {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001532 updateCursorPosition(1, middle, bottom,
1533 layout.getSecondaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001534 }
1535 }
1536
Raph Levienafe8e9b2012-12-19 16:09:32 -08001537 private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset,
1538 boolean clamped) {
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001539 if (TextUtils.isEmpty(layout.getText()) &&
1540 hintLayout != null &&
1541 !TextUtils.isEmpty(hintLayout.getText())) {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001542 return hintLayout.getPrimaryHorizontal(offset, clamped);
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001543 } else {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001544 return layout.getPrimaryHorizontal(offset, clamped);
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001545 }
1546 }
1547
Gilles Debunned88876a2012-03-16 17:34:04 -07001548 /**
1549 * @return true if the selection mode was actually started.
1550 */
1551 boolean startSelectionActionMode() {
1552 if (mSelectionActionMode != null) {
1553 // Selection action mode is already started
1554 return false;
1555 }
1556
1557 if (!canSelectText() || !mTextView.requestFocus()) {
1558 Log.w(TextView.LOG_TAG,
1559 "TextView does not support text selection. Action mode cancelled.");
1560 return false;
1561 }
1562
1563 if (!mTextView.hasSelection()) {
1564 // There may already be a selection on device rotation
1565 if (!selectCurrentWord()) {
1566 // No word found under cursor or text selection not permitted.
1567 return false;
1568 }
1569 }
1570
1571 boolean willExtract = extractedTextModeWillBeStarted();
1572
1573 // Do not start the action mode when extracted text will show up full screen, which would
1574 // immediately hide the newly created action bar and would be visually distracting.
1575 if (!willExtract) {
1576 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
1577 mSelectionActionMode = mTextView.startActionMode(actionModeCallback);
1578 }
1579
1580 final boolean selectionStarted = mSelectionActionMode != null || willExtract;
Gilles Debunne3473b2b2012-04-20 16:21:10 -07001581 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001582 // Show the IME to be able to replace text, except when selecting non editable text.
1583 final InputMethodManager imm = InputMethodManager.peekInstance();
1584 if (imm != null) {
1585 imm.showSoftInput(mTextView, 0, null);
1586 }
1587 }
1588
1589 return selectionStarted;
1590 }
1591
1592 private boolean extractedTextModeWillBeStarted() {
1593 if (!(mTextView instanceof ExtractEditText)) {
1594 final InputMethodManager imm = InputMethodManager.peekInstance();
1595 return imm != null && imm.isFullscreenMode();
1596 }
1597 return false;
1598 }
1599
1600 /**
1601 * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
1602 */
1603 private boolean isCursorInsideSuggestionSpan() {
1604 CharSequence text = mTextView.getText();
1605 if (!(text instanceof Spannable)) return false;
1606
1607 SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans(
1608 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class);
1609 return (suggestionSpans.length > 0);
1610 }
1611
1612 /**
1613 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
1614 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
1615 */
1616 private boolean isCursorInsideEasyCorrectionSpan() {
1617 Spannable spannable = (Spannable) mTextView.getText();
1618 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
1619 mTextView.getSelectionEnd(), SuggestionSpan.class);
1620 for (int i = 0; i < suggestionSpans.length; i++) {
1621 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
1622 return true;
1623 }
1624 }
1625 return false;
1626 }
1627
1628 void onTouchUpEvent(MotionEvent event) {
1629 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
1630 hideControllers();
1631 CharSequence text = mTextView.getText();
1632 if (!selectAllGotFocus && text.length() > 0) {
1633 // Move cursor
1634 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1635 Selection.setSelection((Spannable) text, offset);
1636 if (mSpellChecker != null) {
1637 // When the cursor moves, the word that was typed may need spell check
1638 mSpellChecker.onSelectionChanged();
1639 }
1640 if (!extractedTextModeWillBeStarted()) {
1641 if (isCursorInsideEasyCorrectionSpan()) {
1642 mShowSuggestionRunnable = new Runnable() {
1643 public void run() {
1644 showSuggestions();
1645 }
1646 };
1647 // removeCallbacks is performed on every touch
1648 mTextView.postDelayed(mShowSuggestionRunnable,
1649 ViewConfiguration.getDoubleTapTimeout());
1650 } else if (hasInsertionController()) {
1651 getInsertionController().show();
1652 }
1653 }
1654 }
1655 }
1656
1657 protected void stopSelectionActionMode() {
1658 if (mSelectionActionMode != null) {
1659 // This will hide the mSelectionModifierCursorController
1660 mSelectionActionMode.finish();
1661 }
1662 }
1663
1664 /**
1665 * @return True if this view supports insertion handles.
1666 */
1667 boolean hasInsertionController() {
1668 return mInsertionControllerEnabled;
1669 }
1670
1671 /**
1672 * @return True if this view supports selection handles.
1673 */
1674 boolean hasSelectionController() {
1675 return mSelectionControllerEnabled;
1676 }
1677
1678 InsertionPointCursorController getInsertionController() {
1679 if (!mInsertionControllerEnabled) {
1680 return null;
1681 }
1682
1683 if (mInsertionPointCursorController == null) {
1684 mInsertionPointCursorController = new InsertionPointCursorController();
1685
1686 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1687 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
1688 }
1689
1690 return mInsertionPointCursorController;
1691 }
1692
1693 SelectionModifierCursorController getSelectionController() {
1694 if (!mSelectionControllerEnabled) {
1695 return null;
1696 }
1697
1698 if (mSelectionModifierCursorController == null) {
1699 mSelectionModifierCursorController = new SelectionModifierCursorController();
1700
1701 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1702 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
1703 }
1704
1705 return mSelectionModifierCursorController;
1706 }
1707
1708 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
1709 if (mCursorDrawable[cursorIndex] == null)
Alan Viverette8eea3ea2014-02-03 18:40:20 -08001710 mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07001711 mTextView.mCursorDrawableRes);
1712
1713 if (mTempRect == null) mTempRect = new Rect();
1714 mCursorDrawable[cursorIndex].getPadding(mTempRect);
1715 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
1716 horizontal = Math.max(0.5f, horizontal - 0.5f);
1717 final int left = (int) (horizontal) - mTempRect.left;
1718 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
1719 bottom + mTempRect.bottom);
1720 }
1721
1722 /**
1723 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
1724 * a dictionnary) from the current input method, provided by it calling
1725 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
1726 * implementation flashes the background of the corrected word to provide feedback to the user.
1727 *
1728 * @param info The auto correct info about the text that was corrected.
1729 */
1730 public void onCommitCorrection(CorrectionInfo info) {
1731 if (mCorrectionHighlighter == null) {
1732 mCorrectionHighlighter = new CorrectionHighlighter();
1733 } else {
1734 mCorrectionHighlighter.invalidate(false);
1735 }
1736
1737 mCorrectionHighlighter.highlight(info);
1738 }
1739
1740 void showSuggestions() {
1741 if (mSuggestionsPopupWindow == null) {
1742 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
1743 }
1744 hideControllers();
1745 mSuggestionsPopupWindow.show();
1746 }
1747
1748 boolean areSuggestionsShown() {
1749 return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
1750 }
1751
1752 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001753 if (mPositionListener != null) {
1754 mPositionListener.onScrollChanged();
1755 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001756 }
1757
1758 /**
1759 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
1760 */
1761 private boolean shouldBlink() {
1762 if (!isCursorVisible() || !mTextView.isFocused()) return false;
1763
1764 final int start = mTextView.getSelectionStart();
1765 if (start < 0) return false;
1766
1767 final int end = mTextView.getSelectionEnd();
1768 if (end < 0) return false;
1769
1770 return start == end;
1771 }
1772
1773 void makeBlink() {
1774 if (shouldBlink()) {
1775 mShowCursor = SystemClock.uptimeMillis();
1776 if (mBlink == null) mBlink = new Blink();
1777 mBlink.removeCallbacks(mBlink);
1778 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
1779 } else {
1780 if (mBlink != null) mBlink.removeCallbacks(mBlink);
1781 }
1782 }
1783
1784 private class Blink extends Handler implements Runnable {
1785 private boolean mCancelled;
1786
1787 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07001788 if (mCancelled) {
1789 return;
1790 }
1791
1792 removeCallbacks(Blink.this);
1793
1794 if (shouldBlink()) {
1795 if (mTextView.getLayout() != null) {
1796 mTextView.invalidateCursorPath();
1797 }
1798
1799 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
1800 }
1801 }
1802
1803 void cancel() {
1804 if (!mCancelled) {
1805 removeCallbacks(Blink.this);
1806 mCancelled = true;
1807 }
1808 }
1809
1810 void uncancel() {
1811 mCancelled = false;
1812 }
1813 }
1814
1815 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
1816 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
1817 com.android.internal.R.layout.text_drag_thumbnail, null);
1818
1819 if (shadowView == null) {
1820 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
1821 }
1822
1823 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
1824 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
1825 }
1826 shadowView.setText(text);
1827 shadowView.setTextColor(mTextView.getTextColors());
1828
1829 shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge);
1830 shadowView.setGravity(Gravity.CENTER);
1831
1832 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
1833 ViewGroup.LayoutParams.WRAP_CONTENT));
1834
1835 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
1836 shadowView.measure(size, size);
1837
1838 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
1839 shadowView.invalidate();
1840 return new DragShadowBuilder(shadowView);
1841 }
1842
1843 private static class DragLocalState {
1844 public TextView sourceTextView;
1845 public int start, end;
1846
1847 public DragLocalState(TextView sourceTextView, int start, int end) {
1848 this.sourceTextView = sourceTextView;
1849 this.start = start;
1850 this.end = end;
1851 }
1852 }
1853
1854 void onDrop(DragEvent event) {
1855 StringBuilder content = new StringBuilder("");
1856 ClipData clipData = event.getClipData();
1857 final int itemCount = clipData.getItemCount();
1858 for (int i=0; i < itemCount; i++) {
1859 Item item = clipData.getItemAt(i);
Dianne Hackbornacb69bb2012-04-13 15:36:06 -07001860 content.append(item.coerceToStyledText(mTextView.getContext()));
Gilles Debunned88876a2012-03-16 17:34:04 -07001861 }
1862
1863 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1864
1865 Object localState = event.getLocalState();
1866 DragLocalState dragLocalState = null;
1867 if (localState instanceof DragLocalState) {
1868 dragLocalState = (DragLocalState) localState;
1869 }
1870 boolean dragDropIntoItself = dragLocalState != null &&
1871 dragLocalState.sourceTextView == mTextView;
1872
1873 if (dragDropIntoItself) {
1874 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
1875 // A drop inside the original selection discards the drop.
1876 return;
1877 }
1878 }
1879
1880 final int originalLength = mTextView.getText().length();
1881 long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content);
1882 int min = TextUtils.unpackRangeStartFromLong(minMax);
1883 int max = TextUtils.unpackRangeEndFromLong(minMax);
1884
1885 Selection.setSelection((Spannable) mTextView.getText(), max);
1886 mTextView.replaceText_internal(min, max, content);
1887
1888 if (dragDropIntoItself) {
1889 int dragSourceStart = dragLocalState.start;
1890 int dragSourceEnd = dragLocalState.end;
1891 if (max <= dragSourceStart) {
1892 // Inserting text before selection has shifted positions
1893 final int shift = mTextView.getText().length() - originalLength;
1894 dragSourceStart += shift;
1895 dragSourceEnd += shift;
1896 }
1897
1898 // Delete original selection
1899 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
1900
1901 // Make sure we do not leave two adjacent spaces.
Victoria Lease91373202012-09-07 16:41:59 -07001902 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
1903 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
1904 if (nextCharIdx > prevCharIdx + 1) {
1905 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
1906 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
1907 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
1908 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001909 }
1910 }
1911 }
1912
Gilles Debunnec62589c2012-04-12 14:50:23 -07001913 public void addSpanWatchers(Spannable text) {
1914 final int textLength = text.length();
1915
1916 if (mKeyListener != null) {
1917 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
1918 }
1919
Jean Chalardbaf30942013-02-28 16:01:51 -08001920 if (mSpanController == null) {
1921 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07001922 }
Jean Chalardbaf30942013-02-28 16:01:51 -08001923 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07001924 }
1925
Gilles Debunned88876a2012-03-16 17:34:04 -07001926 /**
1927 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
1928 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07001929 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07001930 */
Jean Chalardbaf30942013-02-28 16:01:51 -08001931 class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07001932
1933 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
1934
1935 private EasyEditPopupWindow mPopupWindow;
1936
Gilles Debunned88876a2012-03-16 17:34:04 -07001937 private Runnable mHidePopup;
1938
Jean Chalardbaf30942013-02-28 16:01:51 -08001939 // This function is pure but inner classes can't have static functions
1940 private boolean isNonIntermediateSelectionSpan(final Spannable text,
1941 final Object span) {
1942 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
1943 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
1944 }
1945
Gilles Debunnec62589c2012-04-12 14:50:23 -07001946 @Override
1947 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08001948 if (isNonIntermediateSelectionSpan(text, span)) {
1949 sendUpdateSelection();
1950 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07001951 if (mPopupWindow == null) {
1952 mPopupWindow = new EasyEditPopupWindow();
1953 mHidePopup = new Runnable() {
1954 @Override
1955 public void run() {
1956 hide();
1957 }
1958 };
1959 }
1960
1961 // Make sure there is only at most one EasyEditSpan in the text
1962 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00001963 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07001964 }
1965
1966 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00001967 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
1968 @Override
1969 public void onDeleteClick(EasyEditSpan span) {
1970 Editable editable = (Editable) mTextView.getText();
1971 int start = editable.getSpanStart(span);
1972 int end = editable.getSpanEnd(span);
1973 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08001974 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00001975 mTextView.deleteText_internal(start, end);
1976 }
1977 editable.removeSpan(span);
1978 }
1979 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07001980
1981 if (mTextView.getWindowVisibility() != View.VISIBLE) {
1982 // The window is not visible yet, ignore the text change.
1983 return;
1984 }
1985
1986 if (mTextView.getLayout() == null) {
1987 // The view has not been laid out yet, ignore the text change
1988 return;
1989 }
1990
1991 if (extractedTextModeWillBeStarted()) {
1992 // The input is in extract mode. Do not handle the easy edit in
1993 // the original TextView, as the ExtractEditText will do
1994 return;
1995 }
1996
1997 mPopupWindow.show();
1998 mTextView.removeCallbacks(mHidePopup);
1999 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2000 }
2001 }
2002
2003 @Override
2004 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002005 if (isNonIntermediateSelectionSpan(text, span)) {
2006 sendUpdateSelection();
2007 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002008 hide();
2009 }
2010 }
2011
2012 @Override
2013 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2014 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002015 if (isNonIntermediateSelectionSpan(text, span)) {
2016 sendUpdateSelection();
2017 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002018 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08002019 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002020 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002021 }
2022 }
2023
Gilles Debunned88876a2012-03-16 17:34:04 -07002024 public void hide() {
2025 if (mPopupWindow != null) {
2026 mPopupWindow.hide();
2027 mTextView.removeCallbacks(mHidePopup);
2028 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002029 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002030
Jean Chalardbaf30942013-02-28 16:01:51 -08002031 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002032 try {
2033 PendingIntent pendingIntent = span.getPendingIntent();
2034 if (pendingIntent != null) {
2035 Intent intent = new Intent();
2036 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2037 pendingIntent.send(mTextView.getContext(), 0, intent);
2038 }
2039 } catch (CanceledException e) {
2040 // This should not happen, as we should try to send the intent only once.
2041 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2042 }
2043 }
2044 }
2045
2046 /**
2047 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2048 */
2049 private interface EasyEditDeleteListener {
2050
2051 /**
2052 * Clicks the delete pop-up.
2053 */
2054 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07002055 }
2056
2057 /**
2058 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002059 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002060 */
2061 private class EasyEditPopupWindow extends PinnedPopupWindow
2062 implements OnClickListener {
2063 private static final int POPUP_TEXT_LAYOUT =
2064 com.android.internal.R.layout.text_edit_action_popup_text;
2065 private TextView mDeleteTextView;
2066 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002067 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07002068
2069 @Override
2070 protected void createPopupWindow() {
2071 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2072 com.android.internal.R.attr.textSelectHandleWindowStyle);
2073 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2074 mPopupWindow.setClippingEnabled(true);
2075 }
2076
2077 @Override
2078 protected void initContentView() {
2079 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2080 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2081 mContentView = linearLayout;
2082 mContentView.setBackgroundResource(
2083 com.android.internal.R.drawable.text_edit_side_paste_window);
2084
2085 LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
2086 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2087
2088 LayoutParams wrapContent = new LayoutParams(
2089 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2090
2091 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2092 mDeleteTextView.setLayoutParams(wrapContent);
2093 mDeleteTextView.setText(com.android.internal.R.string.delete);
2094 mDeleteTextView.setOnClickListener(this);
2095 mContentView.addView(mDeleteTextView);
2096 }
2097
Gilles Debunnec62589c2012-04-12 14:50:23 -07002098 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002099 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07002100 }
2101
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002102 private void setOnDeleteListener(EasyEditDeleteListener listener) {
2103 mOnDeleteListener = listener;
2104 }
2105
Gilles Debunned88876a2012-03-16 17:34:04 -07002106 @Override
2107 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002108 if (view == mDeleteTextView
2109 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2110 && mOnDeleteListener != null) {
2111 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07002112 }
2113 }
2114
2115 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002116 public void hide() {
2117 if (mEasyEditSpan != null) {
2118 mEasyEditSpan.setDeleteEnabled(false);
2119 }
2120 mOnDeleteListener = null;
2121 super.hide();
2122 }
2123
2124 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07002125 protected int getTextOffset() {
2126 // Place the pop-up at the end of the span
2127 Editable editable = (Editable) mTextView.getText();
2128 return editable.getSpanEnd(mEasyEditSpan);
2129 }
2130
2131 @Override
2132 protected int getVerticalLocalPosition(int line) {
2133 return mTextView.getLayout().getLineBottom(line);
2134 }
2135
2136 @Override
2137 protected int clipVertically(int positionY) {
2138 // As we display the pop-up below the span, no vertical clipping is required.
2139 return positionY;
2140 }
2141 }
2142
2143 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
2144 // 3 handles
2145 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09002146 // 1 CursorAnchorInfoNotifier
2147 private final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07002148 private TextViewPositionListener[] mPositionListeners =
2149 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
2150 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
2151 private boolean mPositionHasChanged = true;
2152 // Absolute position of the TextView with respect to its parent window
2153 private int mPositionX, mPositionY;
2154 private int mNumberOfListeners;
2155 private boolean mScrollHasChanged;
2156 final int[] mTempCoords = new int[2];
2157
2158 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
2159 if (mNumberOfListeners == 0) {
2160 updatePosition();
2161 ViewTreeObserver vto = mTextView.getViewTreeObserver();
2162 vto.addOnPreDrawListener(this);
2163 }
2164
2165 int emptySlotIndex = -1;
2166 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2167 TextViewPositionListener listener = mPositionListeners[i];
2168 if (listener == positionListener) {
2169 return;
2170 } else if (emptySlotIndex < 0 && listener == null) {
2171 emptySlotIndex = i;
2172 }
2173 }
2174
2175 mPositionListeners[emptySlotIndex] = positionListener;
2176 mCanMove[emptySlotIndex] = canMove;
2177 mNumberOfListeners++;
2178 }
2179
2180 public void removeSubscriber(TextViewPositionListener positionListener) {
2181 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2182 if (mPositionListeners[i] == positionListener) {
2183 mPositionListeners[i] = null;
2184 mNumberOfListeners--;
2185 break;
2186 }
2187 }
2188
2189 if (mNumberOfListeners == 0) {
2190 ViewTreeObserver vto = mTextView.getViewTreeObserver();
2191 vto.removeOnPreDrawListener(this);
2192 }
2193 }
2194
2195 public int getPositionX() {
2196 return mPositionX;
2197 }
2198
2199 public int getPositionY() {
2200 return mPositionY;
2201 }
2202
2203 @Override
2204 public boolean onPreDraw() {
2205 updatePosition();
2206
2207 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2208 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
2209 TextViewPositionListener positionListener = mPositionListeners[i];
2210 if (positionListener != null) {
2211 positionListener.updatePosition(mPositionX, mPositionY,
2212 mPositionHasChanged, mScrollHasChanged);
2213 }
2214 }
2215 }
2216
2217 mScrollHasChanged = false;
2218 return true;
2219 }
2220
2221 private void updatePosition() {
2222 mTextView.getLocationInWindow(mTempCoords);
2223
2224 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
2225
2226 mPositionX = mTempCoords[0];
2227 mPositionY = mTempCoords[1];
2228 }
2229
2230 public void onScrollChanged() {
2231 mScrollHasChanged = true;
2232 }
2233 }
2234
2235 private abstract class PinnedPopupWindow implements TextViewPositionListener {
2236 protected PopupWindow mPopupWindow;
2237 protected ViewGroup mContentView;
2238 int mPositionX, mPositionY;
2239
2240 protected abstract void createPopupWindow();
2241 protected abstract void initContentView();
2242 protected abstract int getTextOffset();
2243 protected abstract int getVerticalLocalPosition(int line);
2244 protected abstract int clipVertically(int positionY);
2245
2246 public PinnedPopupWindow() {
2247 createPopupWindow();
2248
2249 mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
2250 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
2251 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
2252
2253 initContentView();
2254
2255 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2256 ViewGroup.LayoutParams.WRAP_CONTENT);
2257 mContentView.setLayoutParams(wrapContent);
2258
2259 mPopupWindow.setContentView(mContentView);
2260 }
2261
2262 public void show() {
2263 getPositionListener().addSubscriber(this, false /* offset is fixed */);
2264
2265 computeLocalPosition();
2266
2267 final PositionListener positionListener = getPositionListener();
2268 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
2269 }
2270
2271 protected void measureContent() {
2272 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2273 mContentView.measure(
2274 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
2275 View.MeasureSpec.AT_MOST),
2276 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
2277 View.MeasureSpec.AT_MOST));
2278 }
2279
2280 /* The popup window will be horizontally centered on the getTextOffset() and vertically
2281 * positioned according to viewportToContentHorizontalOffset.
2282 *
2283 * This method assumes that mContentView has properly been measured from its content. */
2284 private void computeLocalPosition() {
2285 measureContent();
2286 final int width = mContentView.getMeasuredWidth();
2287 final int offset = getTextOffset();
2288 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
2289 mPositionX += mTextView.viewportToContentHorizontalOffset();
2290
2291 final int line = mTextView.getLayout().getLineForOffset(offset);
2292 mPositionY = getVerticalLocalPosition(line);
2293 mPositionY += mTextView.viewportToContentVerticalOffset();
2294 }
2295
2296 private void updatePosition(int parentPositionX, int parentPositionY) {
2297 int positionX = parentPositionX + mPositionX;
2298 int positionY = parentPositionY + mPositionY;
2299
2300 positionY = clipVertically(positionY);
2301
2302 // Horizontal clipping
2303 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2304 final int width = mContentView.getMeasuredWidth();
2305 positionX = Math.min(displayMetrics.widthPixels - width, positionX);
2306 positionX = Math.max(0, positionX);
2307
2308 if (isShowing()) {
2309 mPopupWindow.update(positionX, positionY, -1, -1);
2310 } else {
2311 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
2312 positionX, positionY);
2313 }
2314 }
2315
2316 public void hide() {
2317 mPopupWindow.dismiss();
2318 getPositionListener().removeSubscriber(this);
2319 }
2320
2321 @Override
2322 public void updatePosition(int parentPositionX, int parentPositionY,
2323 boolean parentPositionChanged, boolean parentScrolled) {
2324 // Either parentPositionChanged or parentScrolled is true, check if still visible
2325 if (isShowing() && isOffsetVisible(getTextOffset())) {
2326 if (parentScrolled) computeLocalPosition();
2327 updatePosition(parentPositionX, parentPositionY);
2328 } else {
2329 hide();
2330 }
2331 }
2332
2333 public boolean isShowing() {
2334 return mPopupWindow.isShowing();
2335 }
2336 }
2337
2338 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
2339 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
2340 private static final int ADD_TO_DICTIONARY = -1;
2341 private static final int DELETE_TEXT = -2;
2342 private SuggestionInfo[] mSuggestionInfos;
2343 private int mNumberOfSuggestions;
2344 private boolean mCursorWasVisibleBeforeSuggestions;
2345 private boolean mIsShowingUp = false;
2346 private SuggestionAdapter mSuggestionsAdapter;
2347 private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
2348 private final HashMap<SuggestionSpan, Integer> mSpansLengths;
2349
2350 private class CustomPopupWindow extends PopupWindow {
Alan Viverette617feb92013-09-09 18:09:13 -07002351 public CustomPopupWindow(Context context, int defStyleAttr) {
2352 super(context, null, defStyleAttr);
Gilles Debunned88876a2012-03-16 17:34:04 -07002353 }
2354
2355 @Override
2356 public void dismiss() {
2357 super.dismiss();
2358
2359 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
2360
2361 // Safe cast since show() checks that mTextView.getText() is an Editable
2362 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
2363
2364 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
2365 if (hasInsertionController()) {
2366 getInsertionController().show();
2367 }
2368 }
2369 }
2370
2371 public SuggestionsPopupWindow() {
2372 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2373 mSuggestionSpanComparator = new SuggestionSpanComparator();
2374 mSpansLengths = new HashMap<SuggestionSpan, Integer>();
2375 }
2376
2377 @Override
2378 protected void createPopupWindow() {
2379 mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
2380 com.android.internal.R.attr.textSuggestionsWindowStyle);
2381 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2382 mPopupWindow.setFocusable(true);
2383 mPopupWindow.setClippingEnabled(false);
2384 }
2385
2386 @Override
2387 protected void initContentView() {
2388 ListView listView = new ListView(mTextView.getContext());
2389 mSuggestionsAdapter = new SuggestionAdapter();
2390 listView.setAdapter(mSuggestionsAdapter);
2391 listView.setOnItemClickListener(this);
2392 mContentView = listView;
2393
2394 // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
2395 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
2396 for (int i = 0; i < mSuggestionInfos.length; i++) {
2397 mSuggestionInfos[i] = new SuggestionInfo();
2398 }
2399 }
2400
2401 public boolean isShowingUp() {
2402 return mIsShowingUp;
2403 }
2404
2405 public void onParentLostFocus() {
2406 mIsShowingUp = false;
2407 }
2408
2409 private class SuggestionInfo {
2410 int suggestionStart, suggestionEnd; // range of actual suggestion within text
2411 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
2412 int suggestionIndex; // the index of this suggestion inside suggestionSpan
2413 SpannableStringBuilder text = new SpannableStringBuilder();
2414 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(),
2415 android.R.style.TextAppearance_SuggestionHighlight);
2416 }
2417
2418 private class SuggestionAdapter extends BaseAdapter {
2419 private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
2420 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2421
2422 @Override
2423 public int getCount() {
2424 return mNumberOfSuggestions;
2425 }
2426
2427 @Override
2428 public Object getItem(int position) {
2429 return mSuggestionInfos[position];
2430 }
2431
2432 @Override
2433 public long getItemId(int position) {
2434 return position;
2435 }
2436
2437 @Override
2438 public View getView(int position, View convertView, ViewGroup parent) {
2439 TextView textView = (TextView) convertView;
2440
2441 if (textView == null) {
2442 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
2443 parent, false);
2444 }
2445
2446 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2447 textView.setText(suggestionInfo.text);
2448
Gilles Debunne1daba182012-06-26 11:41:03 -07002449 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY ||
2450 suggestionInfo.suggestionIndex == DELETE_TEXT) {
2451 textView.setBackgroundColor(Color.TRANSPARENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07002452 } else {
Gilles Debunne1daba182012-06-26 11:41:03 -07002453 textView.setBackgroundColor(Color.WHITE);
Gilles Debunned88876a2012-03-16 17:34:04 -07002454 }
2455
2456 return textView;
2457 }
2458 }
2459
2460 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
2461 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
2462 final int flag1 = span1.getFlags();
2463 final int flag2 = span2.getFlags();
2464 if (flag1 != flag2) {
2465 // The order here should match what is used in updateDrawState
2466 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2467 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2468 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2469 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2470 if (easy1 && !misspelled1) return -1;
2471 if (easy2 && !misspelled2) return 1;
2472 if (misspelled1) return -1;
2473 if (misspelled2) return 1;
2474 }
2475
2476 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
2477 }
2478 }
2479
2480 /**
2481 * Returns the suggestion spans that cover the current cursor position. The suggestion
2482 * spans are sorted according to the length of text that they are attached to.
2483 */
2484 private SuggestionSpan[] getSuggestionSpans() {
2485 int pos = mTextView.getSelectionStart();
2486 Spannable spannable = (Spannable) mTextView.getText();
2487 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
2488
2489 mSpansLengths.clear();
2490 for (SuggestionSpan suggestionSpan : suggestionSpans) {
2491 int start = spannable.getSpanStart(suggestionSpan);
2492 int end = spannable.getSpanEnd(suggestionSpan);
2493 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
2494 }
2495
2496 // The suggestions are sorted according to their types (easy correction first, then
2497 // misspelled) and to the length of the text that they cover (shorter first).
2498 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
2499 return suggestionSpans;
2500 }
2501
2502 @Override
2503 public void show() {
2504 if (!(mTextView.getText() instanceof Editable)) return;
2505
2506 if (updateSuggestions()) {
2507 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2508 mTextView.setCursorVisible(false);
2509 mIsShowingUp = true;
2510 super.show();
2511 }
2512 }
2513
2514 @Override
2515 protected void measureContent() {
2516 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2517 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
2518 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
2519 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
2520 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
2521
2522 int width = 0;
2523 View view = null;
2524 for (int i = 0; i < mNumberOfSuggestions; i++) {
2525 view = mSuggestionsAdapter.getView(i, view, mContentView);
2526 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
2527 view.measure(horizontalMeasure, verticalMeasure);
2528 width = Math.max(width, view.getMeasuredWidth());
2529 }
2530
2531 // Enforce the width based on actual text widths
2532 mContentView.measure(
2533 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
2534 verticalMeasure);
2535
2536 Drawable popupBackground = mPopupWindow.getBackground();
2537 if (popupBackground != null) {
2538 if (mTempRect == null) mTempRect = new Rect();
2539 popupBackground.getPadding(mTempRect);
2540 width += mTempRect.left + mTempRect.right;
2541 }
2542 mPopupWindow.setWidth(width);
2543 }
2544
2545 @Override
2546 protected int getTextOffset() {
2547 return mTextView.getSelectionStart();
2548 }
2549
2550 @Override
2551 protected int getVerticalLocalPosition(int line) {
2552 return mTextView.getLayout().getLineBottom(line);
2553 }
2554
2555 @Override
2556 protected int clipVertically(int positionY) {
2557 final int height = mContentView.getMeasuredHeight();
2558 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2559 return Math.min(positionY, displayMetrics.heightPixels - height);
2560 }
2561
2562 @Override
2563 public void hide() {
2564 super.hide();
2565 }
2566
2567 private boolean updateSuggestions() {
2568 Spannable spannable = (Spannable) mTextView.getText();
2569 SuggestionSpan[] suggestionSpans = getSuggestionSpans();
2570
2571 final int nbSpans = suggestionSpans.length;
2572 // Suggestions are shown after a delay: the underlying spans may have been removed
2573 if (nbSpans == 0) return false;
2574
2575 mNumberOfSuggestions = 0;
2576 int spanUnionStart = mTextView.getText().length();
2577 int spanUnionEnd = 0;
2578
2579 SuggestionSpan misspelledSpan = null;
2580 int underlineColor = 0;
2581
2582 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
2583 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
2584 final int spanStart = spannable.getSpanStart(suggestionSpan);
2585 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
2586 spanUnionStart = Math.min(spanStart, spanUnionStart);
2587 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
2588
2589 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2590 misspelledSpan = suggestionSpan;
2591 }
2592
2593 // The first span dictates the background color of the highlighted text
2594 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
2595
2596 String[] suggestions = suggestionSpan.getSuggestions();
2597 int nbSuggestions = suggestions.length;
2598 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
2599 String suggestion = suggestions[suggestionIndex];
2600
2601 boolean suggestionIsDuplicate = false;
2602 for (int i = 0; i < mNumberOfSuggestions; i++) {
2603 if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
2604 SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
2605 final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
2606 final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
2607 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
2608 suggestionIsDuplicate = true;
2609 break;
2610 }
2611 }
2612 }
2613
2614 if (!suggestionIsDuplicate) {
2615 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2616 suggestionInfo.suggestionSpan = suggestionSpan;
2617 suggestionInfo.suggestionIndex = suggestionIndex;
2618 suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
2619
2620 mNumberOfSuggestions++;
2621
2622 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
2623 // Also end outer for loop
2624 spanIndex = nbSpans;
2625 break;
2626 }
2627 }
2628 }
2629 }
2630
2631 for (int i = 0; i < mNumberOfSuggestions; i++) {
2632 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
2633 }
2634
2635 // Add "Add to dictionary" item if there is a span with the misspelled flag
2636 if (misspelledSpan != null) {
2637 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
2638 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
2639 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
2640 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2641 suggestionInfo.suggestionSpan = misspelledSpan;
2642 suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
2643 suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView.
2644 getContext().getString(com.android.internal.R.string.addToDictionary));
2645 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2646 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2647
2648 mNumberOfSuggestions++;
2649 }
2650 }
2651
2652 // Delete item
2653 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2654 suggestionInfo.suggestionSpan = null;
2655 suggestionInfo.suggestionIndex = DELETE_TEXT;
2656 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
2657 mTextView.getContext().getString(com.android.internal.R.string.deleteText));
2658 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2659 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2660 mNumberOfSuggestions++;
2661
2662 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
2663 if (underlineColor == 0) {
2664 // Fallback on the default highlight color when the first span does not provide one
2665 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
2666 } else {
2667 final float BACKGROUND_TRANSPARENCY = 0.4f;
2668 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
2669 mSuggestionRangeSpan.setBackgroundColor(
2670 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
2671 }
2672 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
2673 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2674
2675 mSuggestionsAdapter.notifyDataSetChanged();
2676 return true;
2677 }
2678
2679 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
2680 int unionEnd) {
2681 final Spannable text = (Spannable) mTextView.getText();
2682 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
2683 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
2684
2685 // Adjust the start/end of the suggestion span
2686 suggestionInfo.suggestionStart = spanStart - unionStart;
2687 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
2688 + suggestionInfo.text.length();
2689
2690 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
2691 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2692
2693 // Add the text before and after the span.
2694 final String textAsString = text.toString();
2695 suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
2696 suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
2697 }
2698
2699 @Override
2700 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
2701 Editable editable = (Editable) mTextView.getText();
2702 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2703
2704 if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
2705 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
2706 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
2707 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
2708 // Do not leave two adjacent spaces after deletion, or one at beginning of text
2709 if (spanUnionEnd < editable.length() &&
2710 Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
2711 (spanUnionStart == 0 ||
2712 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
2713 spanUnionEnd = spanUnionEnd + 1;
2714 }
2715 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
2716 }
2717 hide();
2718 return;
2719 }
2720
2721 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
2722 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
2723 if (spanStart < 0 || spanEnd <= spanStart) {
2724 // Span has been removed
2725 hide();
2726 return;
2727 }
2728
2729 final String originalText = editable.toString().substring(spanStart, spanEnd);
2730
2731 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
2732 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
2733 intent.putExtra("word", originalText);
2734 intent.putExtra("locale", mTextView.getTextServicesLocale().toString());
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09002735 // Put a listener to replace the original text with a word which the user
2736 // modified in a user dictionary dialog.
Gilles Debunned88876a2012-03-16 17:34:04 -07002737 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
2738 mTextView.getContext().startActivity(intent);
2739 // There is no way to know if the word was indeed added. Re-check.
2740 // TODO The ExtractEditText should remove the span in the original text instead
2741 editable.removeSpan(suggestionInfo.suggestionSpan);
Gilles Debunne2eb70fb2012-04-18 17:57:45 -07002742 Selection.setSelection(editable, spanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002743 updateSpellCheckSpans(spanStart, spanEnd, false);
2744 } else {
2745 // SuggestionSpans are removed by replace: save them before
2746 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2747 SuggestionSpan.class);
2748 final int length = suggestionSpans.length;
2749 int[] suggestionSpansStarts = new int[length];
2750 int[] suggestionSpansEnds = new int[length];
2751 int[] suggestionSpansFlags = new int[length];
2752 for (int i = 0; i < length; i++) {
2753 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2754 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2755 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2756 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2757
2758 // Remove potential misspelled flags
2759 int suggestionSpanFlags = suggestionSpan.getFlags();
2760 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
2761 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2762 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2763 suggestionSpan.setFlags(suggestionSpanFlags);
2764 }
2765 }
2766
2767 final int suggestionStart = suggestionInfo.suggestionStart;
2768 final int suggestionEnd = suggestionInfo.suggestionEnd;
2769 final String suggestion = suggestionInfo.text.subSequence(
2770 suggestionStart, suggestionEnd).toString();
2771 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2772
Luca Zanolin0c96b81f2012-08-29 11:33:12 +01002773 // Notify source IME of the suggestion pick. Do this before
2774 // swaping texts.
2775 suggestionInfo.suggestionSpan.notifySelection(
2776 mTextView.getContext(), originalText, suggestionInfo.suggestionIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07002777
2778 // Swap text content between actual text and Suggestion span
2779 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
2780 suggestions[suggestionInfo.suggestionIndex] = originalText;
2781
2782 // Restore previous SuggestionSpans
2783 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
2784 for (int i = 0; i < length; i++) {
2785 // Only spans that include the modified region make sense after replacement
2786 // Spans partially included in the replaced region are removed, there is no
2787 // way to assign them a valid range after replacement
2788 if (suggestionSpansStarts[i] <= spanStart &&
2789 suggestionSpansEnds[i] >= spanEnd) {
2790 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2791 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
2792 }
2793 }
2794
2795 // Move cursor at the end of the replaced word
2796 final int newCursorPosition = spanEnd + lengthDifference;
2797 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2798 }
2799
2800 hide();
2801 }
2802 }
2803
2804 /**
2805 * An ActionMode Callback class that is used to provide actions while in text selection mode.
2806 *
2807 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
2808 * on which of these this TextView supports.
2809 */
2810 private class SelectionActionModeCallback implements ActionMode.Callback {
2811
2812 @Override
2813 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
2814 TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes(
2815 com.android.internal.R.styleable.SelectionModeDrawables);
2816
Gilles Debunned88876a2012-03-16 17:34:04 -07002817 mode.setTitle(mTextView.getContext().getString(
2818 com.android.internal.R.string.textSelectionCABTitle));
2819 mode.setSubtitle(null);
2820 mode.setTitleOptionalHint(true);
2821
Gilles Debunned88876a2012-03-16 17:34:04 -07002822 menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
Sungmin Choif0369202013-01-25 21:39:01 +09002823 setIcon(styledAttributes.getResourceId(
2824 R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0)).
Gilles Debunned88876a2012-03-16 17:34:04 -07002825 setAlphabeticShortcut('a').
2826 setShowAsAction(
2827 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2828
2829 if (mTextView.canCut()) {
2830 menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut).
2831 setIcon(styledAttributes.getResourceId(
2832 R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
2833 setAlphabeticShortcut('x').
2834 setShowAsAction(
2835 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2836 }
2837
2838 if (mTextView.canCopy()) {
2839 menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy).
2840 setIcon(styledAttributes.getResourceId(
2841 R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
2842 setAlphabeticShortcut('c').
2843 setShowAsAction(
2844 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2845 }
2846
2847 if (mTextView.canPaste()) {
2848 menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste).
2849 setIcon(styledAttributes.getResourceId(
2850 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
2851 setAlphabeticShortcut('v').
2852 setShowAsAction(
2853 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2854 }
2855
2856 styledAttributes.recycle();
2857
2858 if (mCustomSelectionActionModeCallback != null) {
2859 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
2860 // The custom mode can choose to cancel the action mode
2861 return false;
2862 }
2863 }
2864
2865 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
2866 getSelectionController().show();
Adam Powell057a5852012-05-11 10:28:38 -07002867 mTextView.setHasTransientState(true);
Gilles Debunned88876a2012-03-16 17:34:04 -07002868 return true;
2869 } else {
2870 return false;
2871 }
2872 }
2873
2874 @Override
2875 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
2876 if (mCustomSelectionActionModeCallback != null) {
2877 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
2878 }
2879 return true;
2880 }
2881
2882 @Override
2883 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
2884 if (mCustomSelectionActionModeCallback != null &&
2885 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
2886 return true;
2887 }
2888 return mTextView.onTextContextMenuItem(item.getItemId());
2889 }
2890
2891 @Override
2892 public void onDestroyActionMode(ActionMode mode) {
2893 if (mCustomSelectionActionModeCallback != null) {
2894 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
2895 }
Adam Powell057a5852012-05-11 10:28:38 -07002896
2897 /*
2898 * If we're ending this mode because we're detaching from a window,
2899 * we still have selection state to preserve. Don't clear it, we'll
2900 * bring back the selection mode when (if) we get reattached.
2901 */
2902 if (!mPreserveDetachedSelection) {
2903 Selection.setSelection((Spannable) mTextView.getText(),
2904 mTextView.getSelectionEnd());
2905 mTextView.setHasTransientState(false);
2906 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002907
2908 if (mSelectionModifierCursorController != null) {
2909 mSelectionModifierCursorController.hide();
2910 }
2911
2912 mSelectionActionMode = null;
2913 }
2914 }
2915
2916 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
2917 private static final int POPUP_TEXT_LAYOUT =
2918 com.android.internal.R.layout.text_edit_action_popup_text;
2919 private TextView mPasteTextView;
2920 private TextView mReplaceTextView;
2921
2922 @Override
2923 protected void createPopupWindow() {
2924 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2925 com.android.internal.R.attr.textSelectHandleWindowStyle);
2926 mPopupWindow.setClippingEnabled(true);
2927 }
2928
2929 @Override
2930 protected void initContentView() {
2931 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2932 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2933 mContentView = linearLayout;
2934 mContentView.setBackgroundResource(
2935 com.android.internal.R.drawable.text_edit_paste_window);
2936
2937 LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
2938 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2939
2940 LayoutParams wrapContent = new LayoutParams(
2941 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2942
2943 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2944 mPasteTextView.setLayoutParams(wrapContent);
2945 mContentView.addView(mPasteTextView);
2946 mPasteTextView.setText(com.android.internal.R.string.paste);
2947 mPasteTextView.setOnClickListener(this);
2948
2949 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2950 mReplaceTextView.setLayoutParams(wrapContent);
2951 mContentView.addView(mReplaceTextView);
2952 mReplaceTextView.setText(com.android.internal.R.string.replace);
2953 mReplaceTextView.setOnClickListener(this);
2954 }
2955
2956 @Override
2957 public void show() {
2958 boolean canPaste = mTextView.canPaste();
2959 boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
2960 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
2961 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
2962
2963 if (!canPaste && !canSuggest) return;
2964
2965 super.show();
2966 }
2967
2968 @Override
2969 public void onClick(View view) {
2970 if (view == mPasteTextView && mTextView.canPaste()) {
2971 mTextView.onTextContextMenuItem(TextView.ID_PASTE);
2972 hide();
2973 } else if (view == mReplaceTextView) {
2974 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2975 stopSelectionActionMode();
2976 Selection.setSelection((Spannable) mTextView.getText(), middle);
2977 showSuggestions();
2978 }
2979 }
2980
2981 @Override
2982 protected int getTextOffset() {
2983 return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2984 }
2985
2986 @Override
2987 protected int getVerticalLocalPosition(int line) {
2988 return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight();
2989 }
2990
2991 @Override
2992 protected int clipVertically(int positionY) {
2993 if (positionY < 0) {
2994 final int offset = getTextOffset();
2995 final Layout layout = mTextView.getLayout();
2996 final int line = layout.getLineForOffset(offset);
2997 positionY += layout.getLineBottom(line) - layout.getLineTop(line);
2998 positionY += mContentView.getMeasuredHeight();
2999
3000 // Assumes insertion and selection handles share the same height
Alan Viverette8eea3ea2014-02-03 18:40:20 -08003001 final Drawable handle = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07003002 mTextView.mTextSelectHandleRes);
3003 positionY += handle.getIntrinsicHeight();
3004 }
3005
3006 return positionY;
3007 }
3008 }
3009
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003010 /**
3011 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
3012 * while the input method is requesting the cursor/anchor position. Does nothing as long as
3013 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
3014 */
3015 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
3016 final CursorAnchorInfoBuilder mSelectionInfoBuilder = new CursorAnchorInfoBuilder();
3017 final int[] mTmpIntOffset = new int[2];
3018 final Matrix mViewToScreenMatrix = new Matrix();
3019
3020 @Override
3021 public void updatePosition(int parentPositionX, int parentPositionY,
3022 boolean parentPositionChanged, boolean parentScrolled) {
3023 final InputMethodState ims = mInputMethodState;
3024 if (ims == null || ims.mBatchEditNesting > 0) {
3025 return;
3026 }
3027 final InputMethodManager imm = InputMethodManager.peekInstance();
3028 if (null == imm) {
3029 return;
3030 }
3031 // Skip if the IME has not requested the cursor/anchor position.
3032 if (!imm.isWatchingCursor(mTextView)) {
3033 return;
3034 }
3035 Layout layout = mTextView.getLayout();
3036 if (layout == null) {
3037 return;
3038 }
3039
3040 final CursorAnchorInfoBuilder builder = mSelectionInfoBuilder;
3041 builder.reset();
3042
3043 final int selectionStart = mTextView.getSelectionStart();
3044 final int selectionEnd = mTextView.getSelectionEnd();
3045 builder.setSelectionRange(mTextView.getSelectionStart(), mTextView.getSelectionEnd());
3046
3047 // Construct transformation matrix from view local coordinates to screen coordinates.
3048 mViewToScreenMatrix.set(mTextView.getMatrix());
3049 mTextView.getLocationOnScreen(mTmpIntOffset);
3050 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
3051 builder.setMatrix(mViewToScreenMatrix);
3052
3053 final float viewportToContentHorizontalOffset =
3054 mTextView.viewportToContentHorizontalOffset();
3055 final float viewportToContentVerticalOffset =
3056 mTextView.viewportToContentVerticalOffset();
3057
3058 if (mTextView.getText() instanceof Spannable) {
3059 final Spannable sp = (Spannable) mTextView.getText();
3060 int compositionStart = EditableInputConnection.getComposingSpanStart(sp);
3061 int compositionEnd = EditableInputConnection.getComposingSpanEnd(sp);
3062 if (compositionEnd < compositionStart) {
3063 final int temp = compositionEnd;
3064 compositionEnd = compositionStart;
3065 compositionStart = temp;
3066 }
3067 builder.setCandidateRange(compositionStart, compositionEnd);
3068 for (int offset = compositionStart; offset < compositionEnd; offset++) {
3069 if (offset < 0) {
3070 continue;
3071 }
3072 final int line = layout.getLineForOffset(offset);
3073 final float left = layout.getPrimaryHorizontal(offset)
3074 + viewportToContentHorizontalOffset;
3075 final float top = layout.getLineTop(line) + viewportToContentVerticalOffset;
3076 // Here we are tentatively passing offset + 1 to calculate the other side of
3077 // the primary horizontal to preserve as many positions as possible so that
3078 // the IME can reconstruct the layout entirely. However, we should revisit this
3079 // to have a clear specification about the relationship between the index of
3080 // the character and its bounding box. See also the TODO comment below.
3081 final float right = layout.getPrimaryHorizontal(offset + 1)
3082 + viewportToContentHorizontalOffset;
3083 final float bottom = layout.getLineBottom(line)
3084 + viewportToContentVerticalOffset;
3085 // Take TextView's padding and scroll into account.
3086 if (isPositionVisible(left, top) && isPositionVisible(right, bottom)) {
3087 // Here offset is the index in Java chars.
3088 // TODO: We must have a well-defined specification. For example, how
3089 // RTL, surrogate pairs, and composition letters are handled must be
3090 // documented.
3091 builder.addCharacterRect(offset, left, top, right, bottom);
3092 }
3093 }
3094 }
3095
3096 // Treat selectionStart as the insertion point.
3097 if (0 <= selectionStart) {
3098 final int offset = selectionStart;
3099 final int line = layout.getLineForOffset(offset);
3100 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
3101 + viewportToContentHorizontalOffset;
3102 final float insertionMarkerTop = layout.getLineTop(line)
3103 + viewportToContentVerticalOffset;
3104 final float insertionMarkerBaseline = layout.getLineBaseline(line)
3105 + viewportToContentVerticalOffset;
3106 final float insertionMarkerBottom = layout.getLineBottom(line)
3107 + viewportToContentVerticalOffset;
3108 // Take TextView's padding and scroll into account.
3109 if (isPositionVisible(insertionMarkerX, insertionMarkerTop) &&
3110 isPositionVisible(insertionMarkerX, insertionMarkerBottom)) {
3111 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
3112 insertionMarkerBaseline, insertionMarkerBottom);
3113 }
3114 }
3115
3116 imm.updateCursorAnchorInfo(mTextView, builder.build());
3117 }
3118 }
3119
Gilles Debunned88876a2012-03-16 17:34:04 -07003120 private abstract class HandleView extends View implements TextViewPositionListener {
3121 protected Drawable mDrawable;
3122 protected Drawable mDrawableLtr;
3123 protected Drawable mDrawableRtl;
3124 private final PopupWindow mContainer;
3125 // Position with respect to the parent TextView
3126 private int mPositionX, mPositionY;
3127 private boolean mIsDragging;
3128 // Offset from touch position to mPosition
3129 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
3130 protected int mHotspotX;
3131 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
3132 private float mTouchOffsetY;
3133 // Where the touch position should be on the handle to ensure a maximum cursor visibility
3134 private float mIdealVerticalOffset;
3135 // Parent's (TextView) previous position in window
3136 private int mLastParentX, mLastParentY;
3137 // Transient action popup window for Paste and Replace actions
3138 protected ActionPopupWindow mActionPopupWindow;
3139 // Previous text character offset
3140 private int mPreviousOffset = -1;
3141 // Previous text character offset
3142 private boolean mPositionHasChanged = true;
3143 // Used to delay the appearance of the action popup window
3144 private Runnable mActionPopupShower;
3145
3146 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
3147 super(mTextView.getContext());
3148 mContainer = new PopupWindow(mTextView.getContext(), null,
3149 com.android.internal.R.attr.textSelectHandleWindowStyle);
3150 mContainer.setSplitTouchEnabled(true);
3151 mContainer.setClippingEnabled(false);
3152 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
3153 mContainer.setContentView(this);
3154
3155 mDrawableLtr = drawableLtr;
3156 mDrawableRtl = drawableRtl;
3157
3158 updateDrawable();
3159
3160 final int handleHeight = mDrawable.getIntrinsicHeight();
3161 mTouchOffsetY = -0.3f * handleHeight;
3162 mIdealVerticalOffset = 0.7f * handleHeight;
3163 }
3164
3165 protected void updateDrawable() {
3166 final int offset = getCurrentCursorOffset();
3167 final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
3168 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
3169 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
3170 }
3171
3172 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
3173
3174 // Touch-up filter: number of previous positions remembered
3175 private static final int HISTORY_SIZE = 5;
3176 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
3177 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
3178 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
3179 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
3180 private int mPreviousOffsetIndex = 0;
3181 private int mNumberPreviousOffsets = 0;
3182
3183 private void startTouchUpFilter(int offset) {
3184 mNumberPreviousOffsets = 0;
3185 addPositionToTouchUpFilter(offset);
3186 }
3187
3188 private void addPositionToTouchUpFilter(int offset) {
3189 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
3190 mPreviousOffsets[mPreviousOffsetIndex] = offset;
3191 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
3192 mNumberPreviousOffsets++;
3193 }
3194
3195 private void filterOnTouchUp() {
3196 final long now = SystemClock.uptimeMillis();
3197 int i = 0;
3198 int index = mPreviousOffsetIndex;
3199 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
3200 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
3201 i++;
3202 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
3203 }
3204
3205 if (i > 0 && i < iMax &&
3206 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
3207 positionAtCursorOffset(mPreviousOffsets[index], false);
3208 }
3209 }
3210
3211 public boolean offsetHasBeenChanged() {
3212 return mNumberPreviousOffsets > 1;
3213 }
3214
3215 @Override
3216 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3217 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
3218 }
3219
3220 public void show() {
3221 if (isShowing()) return;
3222
3223 getPositionListener().addSubscriber(this, true /* local position may change */);
3224
3225 // Make sure the offset is always considered new, even when focusing at same position
3226 mPreviousOffset = -1;
3227 positionAtCursorOffset(getCurrentCursorOffset(), false);
3228
3229 hideActionPopupWindow();
3230 }
3231
3232 protected void dismiss() {
3233 mIsDragging = false;
3234 mContainer.dismiss();
3235 onDetached();
3236 }
3237
3238 public void hide() {
3239 dismiss();
3240
3241 getPositionListener().removeSubscriber(this);
3242 }
3243
3244 void showActionPopupWindow(int delay) {
3245 if (mActionPopupWindow == null) {
3246 mActionPopupWindow = new ActionPopupWindow();
3247 }
3248 if (mActionPopupShower == null) {
3249 mActionPopupShower = new Runnable() {
3250 public void run() {
3251 mActionPopupWindow.show();
3252 }
3253 };
3254 } else {
3255 mTextView.removeCallbacks(mActionPopupShower);
3256 }
3257 mTextView.postDelayed(mActionPopupShower, delay);
3258 }
3259
3260 protected void hideActionPopupWindow() {
3261 if (mActionPopupShower != null) {
3262 mTextView.removeCallbacks(mActionPopupShower);
3263 }
3264 if (mActionPopupWindow != null) {
3265 mActionPopupWindow.hide();
3266 }
3267 }
3268
3269 public boolean isShowing() {
3270 return mContainer.isShowing();
3271 }
3272
3273 private boolean isVisible() {
3274 // Always show a dragging handle.
3275 if (mIsDragging) {
3276 return true;
3277 }
3278
3279 if (mTextView.isInBatchEditMode()) {
3280 return false;
3281 }
3282
3283 return isPositionVisible(mPositionX + mHotspotX, mPositionY);
3284 }
3285
3286 public abstract int getCurrentCursorOffset();
3287
3288 protected abstract void updateSelection(int offset);
3289
3290 public abstract void updatePosition(float x, float y);
3291
3292 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
3293 // A HandleView relies on the layout, which may be nulled by external methods
3294 Layout layout = mTextView.getLayout();
3295 if (layout == null) {
3296 // Will update controllers' state, hiding them and stopping selection mode if needed
3297 prepareCursorControllers();
3298 return;
3299 }
3300
3301 boolean offsetChanged = offset != mPreviousOffset;
3302 if (offsetChanged || parentScrolled) {
3303 if (offsetChanged) {
3304 updateSelection(offset);
3305 addPositionToTouchUpFilter(offset);
3306 }
3307 final int line = layout.getLineForOffset(offset);
3308
3309 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
3310 mPositionY = layout.getLineBottom(line);
3311
3312 // Take TextView's padding and scroll into account.
3313 mPositionX += mTextView.viewportToContentHorizontalOffset();
3314 mPositionY += mTextView.viewportToContentVerticalOffset();
3315
3316 mPreviousOffset = offset;
3317 mPositionHasChanged = true;
3318 }
3319 }
3320
3321 public void updatePosition(int parentPositionX, int parentPositionY,
3322 boolean parentPositionChanged, boolean parentScrolled) {
3323 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
3324 if (parentPositionChanged || mPositionHasChanged) {
3325 if (mIsDragging) {
3326 // Update touchToWindow offset in case of parent scrolling while dragging
3327 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
3328 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
3329 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
3330 mLastParentX = parentPositionX;
3331 mLastParentY = parentPositionY;
3332 }
3333
3334 onHandleMoved();
3335 }
3336
3337 if (isVisible()) {
3338 final int positionX = parentPositionX + mPositionX;
3339 final int positionY = parentPositionY + mPositionY;
3340 if (isShowing()) {
3341 mContainer.update(positionX, positionY, -1, -1);
3342 } else {
3343 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3344 positionX, positionY);
3345 }
3346 } else {
3347 if (isShowing()) {
3348 dismiss();
3349 }
3350 }
3351
3352 mPositionHasChanged = false;
3353 }
3354 }
3355
3356 @Override
3357 protected void onDraw(Canvas c) {
3358 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
3359 mDrawable.draw(c);
3360 }
3361
3362 @Override
3363 public boolean onTouchEvent(MotionEvent ev) {
3364 switch (ev.getActionMasked()) {
3365 case MotionEvent.ACTION_DOWN: {
3366 startTouchUpFilter(getCurrentCursorOffset());
3367 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
3368 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
3369
3370 final PositionListener positionListener = getPositionListener();
3371 mLastParentX = positionListener.getPositionX();
3372 mLastParentY = positionListener.getPositionY();
3373 mIsDragging = true;
3374 break;
3375 }
3376
3377 case MotionEvent.ACTION_MOVE: {
3378 final float rawX = ev.getRawX();
3379 final float rawY = ev.getRawY();
3380
3381 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
3382 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
3383 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
3384 float newVerticalOffset;
3385 if (previousVerticalOffset < mIdealVerticalOffset) {
3386 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
3387 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
3388 } else {
3389 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
3390 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
3391 }
3392 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
3393
3394 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
3395 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
3396
3397 updatePosition(newPosX, newPosY);
3398 break;
3399 }
3400
3401 case MotionEvent.ACTION_UP:
3402 filterOnTouchUp();
3403 mIsDragging = false;
3404 break;
3405
3406 case MotionEvent.ACTION_CANCEL:
3407 mIsDragging = false;
3408 break;
3409 }
3410 return true;
3411 }
3412
3413 public boolean isDragging() {
3414 return mIsDragging;
3415 }
3416
3417 void onHandleMoved() {
3418 hideActionPopupWindow();
3419 }
3420
3421 public void onDetached() {
3422 hideActionPopupWindow();
3423 }
3424 }
3425
3426 private class InsertionHandleView extends HandleView {
3427 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
3428 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
3429
3430 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
3431 private float mDownPositionX, mDownPositionY;
3432 private Runnable mHider;
3433
3434 public InsertionHandleView(Drawable drawable) {
3435 super(drawable, drawable);
3436 }
3437
3438 @Override
3439 public void show() {
3440 super.show();
3441
3442 final long durationSinceCutOrCopy =
3443 SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME;
3444 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
3445 showActionPopupWindow(0);
3446 }
3447
3448 hideAfterDelay();
3449 }
3450
3451 public void showWithActionPopup() {
3452 show();
3453 showActionPopupWindow(0);
3454 }
3455
3456 private void hideAfterDelay() {
3457 if (mHider == null) {
3458 mHider = new Runnable() {
3459 public void run() {
3460 hide();
3461 }
3462 };
3463 } else {
3464 removeHiderCallback();
3465 }
3466 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
3467 }
3468
3469 private void removeHiderCallback() {
3470 if (mHider != null) {
3471 mTextView.removeCallbacks(mHider);
3472 }
3473 }
3474
3475 @Override
3476 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3477 return drawable.getIntrinsicWidth() / 2;
3478 }
3479
3480 @Override
3481 public boolean onTouchEvent(MotionEvent ev) {
3482 final boolean result = super.onTouchEvent(ev);
3483
3484 switch (ev.getActionMasked()) {
3485 case MotionEvent.ACTION_DOWN:
3486 mDownPositionX = ev.getRawX();
3487 mDownPositionY = ev.getRawY();
3488 break;
3489
3490 case MotionEvent.ACTION_UP:
3491 if (!offsetHasBeenChanged()) {
3492 final float deltaX = mDownPositionX - ev.getRawX();
3493 final float deltaY = mDownPositionY - ev.getRawY();
3494 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3495
3496 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3497 mTextView.getContext());
3498 final int touchSlop = viewConfiguration.getScaledTouchSlop();
3499
3500 if (distanceSquared < touchSlop * touchSlop) {
3501 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
3502 // Tapping on the handle dismisses the displayed action popup
3503 mActionPopupWindow.hide();
3504 } else {
3505 showWithActionPopup();
3506 }
3507 }
3508 }
3509 hideAfterDelay();
3510 break;
3511
3512 case MotionEvent.ACTION_CANCEL:
3513 hideAfterDelay();
3514 break;
3515
3516 default:
3517 break;
3518 }
3519
3520 return result;
3521 }
3522
3523 @Override
3524 public int getCurrentCursorOffset() {
3525 return mTextView.getSelectionStart();
3526 }
3527
3528 @Override
3529 public void updateSelection(int offset) {
3530 Selection.setSelection((Spannable) mTextView.getText(), offset);
3531 }
3532
3533 @Override
3534 public void updatePosition(float x, float y) {
3535 positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
3536 }
3537
3538 @Override
3539 void onHandleMoved() {
3540 super.onHandleMoved();
3541 removeHiderCallback();
3542 }
3543
3544 @Override
3545 public void onDetached() {
3546 super.onDetached();
3547 removeHiderCallback();
3548 }
3549 }
3550
3551 private class SelectionStartHandleView extends HandleView {
3552
3553 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3554 super(drawableLtr, drawableRtl);
3555 }
3556
3557 @Override
3558 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3559 if (isRtlRun) {
3560 return drawable.getIntrinsicWidth() / 4;
3561 } else {
3562 return (drawable.getIntrinsicWidth() * 3) / 4;
3563 }
3564 }
3565
3566 @Override
3567 public int getCurrentCursorOffset() {
3568 return mTextView.getSelectionStart();
3569 }
3570
3571 @Override
3572 public void updateSelection(int offset) {
3573 Selection.setSelection((Spannable) mTextView.getText(), offset,
3574 mTextView.getSelectionEnd());
3575 updateDrawable();
3576 }
3577
3578 @Override
3579 public void updatePosition(float x, float y) {
3580 int offset = mTextView.getOffsetForPosition(x, y);
3581
3582 // Handles can not cross and selection is at least one character
3583 final int selectionEnd = mTextView.getSelectionEnd();
3584 if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
3585
3586 positionAtCursorOffset(offset, false);
3587 }
3588
3589 public ActionPopupWindow getActionPopupWindow() {
3590 return mActionPopupWindow;
3591 }
3592 }
3593
3594 private class SelectionEndHandleView extends HandleView {
3595
3596 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3597 super(drawableLtr, drawableRtl);
3598 }
3599
3600 @Override
3601 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3602 if (isRtlRun) {
3603 return (drawable.getIntrinsicWidth() * 3) / 4;
3604 } else {
3605 return drawable.getIntrinsicWidth() / 4;
3606 }
3607 }
3608
3609 @Override
3610 public int getCurrentCursorOffset() {
3611 return mTextView.getSelectionEnd();
3612 }
3613
3614 @Override
3615 public void updateSelection(int offset) {
3616 Selection.setSelection((Spannable) mTextView.getText(),
3617 mTextView.getSelectionStart(), offset);
3618 updateDrawable();
3619 }
3620
3621 @Override
3622 public void updatePosition(float x, float y) {
3623 int offset = mTextView.getOffsetForPosition(x, y);
3624
3625 // Handles can not cross and selection is at least one character
3626 final int selectionStart = mTextView.getSelectionStart();
3627 if (offset <= selectionStart) {
3628 offset = Math.min(selectionStart + 1, mTextView.getText().length());
3629 }
3630
3631 positionAtCursorOffset(offset, false);
3632 }
3633
3634 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
3635 mActionPopupWindow = actionPopupWindow;
3636 }
3637 }
3638
3639 /**
3640 * A CursorController instance can be used to control a cursor in the text.
3641 */
3642 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
3643 /**
3644 * Makes the cursor controller visible on screen.
3645 * See also {@link #hide()}.
3646 */
3647 public void show();
3648
3649 /**
3650 * Hide the cursor controller from screen.
3651 * See also {@link #show()}.
3652 */
3653 public void hide();
3654
3655 /**
3656 * Called when the view is detached from window. Perform house keeping task, such as
3657 * stopping Runnable thread that would otherwise keep a reference on the context, thus
3658 * preventing the activity from being recycled.
3659 */
3660 public void onDetached();
3661 }
3662
3663 private class InsertionPointCursorController implements CursorController {
3664 private InsertionHandleView mHandle;
3665
3666 public void show() {
3667 getHandle().show();
3668 }
3669
3670 public void showWithActionPopup() {
3671 getHandle().showWithActionPopup();
3672 }
3673
3674 public void hide() {
3675 if (mHandle != null) {
3676 mHandle.hide();
3677 }
3678 }
3679
3680 public void onTouchModeChanged(boolean isInTouchMode) {
3681 if (!isInTouchMode) {
3682 hide();
3683 }
3684 }
3685
3686 private InsertionHandleView getHandle() {
3687 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08003688 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07003689 mTextView.mTextSelectHandleRes);
3690 }
3691 if (mHandle == null) {
3692 mHandle = new InsertionHandleView(mSelectHandleCenter);
3693 }
3694 return mHandle;
3695 }
3696
3697 @Override
3698 public void onDetached() {
3699 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3700 observer.removeOnTouchModeChangeListener(this);
3701
3702 if (mHandle != null) mHandle.onDetached();
3703 }
3704 }
3705
3706 class SelectionModifierCursorController implements CursorController {
3707 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
3708 // The cursor controller handles, lazily created when shown.
3709 private SelectionStartHandleView mStartHandle;
3710 private SelectionEndHandleView mEndHandle;
3711 // The offsets of that last touch down event. Remembered to start selection there.
3712 private int mMinTouchOffset, mMaxTouchOffset;
3713
3714 // Double tap detection
3715 private long mPreviousTapUpTime = 0;
3716 private float mDownPositionX, mDownPositionY;
3717 private boolean mGestureStayedInTapRegion;
3718
3719 SelectionModifierCursorController() {
3720 resetTouchOffsets();
3721 }
3722
3723 public void show() {
3724 if (mTextView.isInBatchEditMode()) {
3725 return;
3726 }
3727 initDrawables();
3728 initHandles();
3729 hideInsertionPointCursorController();
3730 }
3731
3732 private void initDrawables() {
3733 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08003734 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07003735 mTextView.mTextSelectHandleLeftRes);
3736 }
3737 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08003738 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07003739 mTextView.mTextSelectHandleRightRes);
3740 }
3741 }
3742
3743 private void initHandles() {
3744 // Lazy object creation has to be done before updatePosition() is called.
3745 if (mStartHandle == null) {
3746 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
3747 }
3748 if (mEndHandle == null) {
3749 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
3750 }
3751
3752 mStartHandle.show();
3753 mEndHandle.show();
3754
3755 // Make sure both left and right handles share the same ActionPopupWindow (so that
3756 // moving any of the handles hides the action popup).
3757 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
3758 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
3759
3760 hideInsertionPointCursorController();
3761 }
3762
3763 public void hide() {
3764 if (mStartHandle != null) mStartHandle.hide();
3765 if (mEndHandle != null) mEndHandle.hide();
3766 }
3767
3768 public void onTouchEvent(MotionEvent event) {
3769 // This is done even when the View does not have focus, so that long presses can start
3770 // selection and tap can move cursor from this tap position.
3771 switch (event.getActionMasked()) {
3772 case MotionEvent.ACTION_DOWN:
3773 final float x = event.getX();
3774 final float y = event.getY();
3775
3776 // Remember finger down position, to be able to start selection from there
3777 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
3778
3779 // Double tap detection
3780 if (mGestureStayedInTapRegion) {
3781 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
3782 if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
3783 final float deltaX = x - mDownPositionX;
3784 final float deltaY = y - mDownPositionY;
3785 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3786
3787 ViewConfiguration viewConfiguration = ViewConfiguration.get(
3788 mTextView.getContext());
3789 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
3790 boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
3791
3792 if (stayedInArea && isPositionOnText(x, y)) {
3793 startSelectionActionMode();
3794 mDiscardNextActionUp = true;
3795 }
3796 }
3797 }
3798
3799 mDownPositionX = x;
3800 mDownPositionY = y;
3801 mGestureStayedInTapRegion = true;
3802 break;
3803
3804 case MotionEvent.ACTION_POINTER_DOWN:
3805 case MotionEvent.ACTION_POINTER_UP:
3806 // Handle multi-point gestures. Keep min and max offset positions.
3807 // Only activated for devices that correctly handle multi-touch.
3808 if (mTextView.getContext().getPackageManager().hasSystemFeature(
3809 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
3810 updateMinAndMaxOffsets(event);
3811 }
3812 break;
3813
3814 case MotionEvent.ACTION_MOVE:
3815 if (mGestureStayedInTapRegion) {
3816 final float deltaX = event.getX() - mDownPositionX;
3817 final float deltaY = event.getY() - mDownPositionY;
3818 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3819
3820 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3821 mTextView.getContext());
3822 int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
3823
3824 if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
3825 mGestureStayedInTapRegion = false;
3826 }
3827 }
3828 break;
3829
3830 case MotionEvent.ACTION_UP:
3831 mPreviousTapUpTime = SystemClock.uptimeMillis();
3832 break;
3833 }
3834 }
3835
3836 /**
3837 * @param event
3838 */
3839 private void updateMinAndMaxOffsets(MotionEvent event) {
3840 int pointerCount = event.getPointerCount();
3841 for (int index = 0; index < pointerCount; index++) {
3842 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
3843 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
3844 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
3845 }
3846 }
3847
3848 public int getMinTouchOffset() {
3849 return mMinTouchOffset;
3850 }
3851
3852 public int getMaxTouchOffset() {
3853 return mMaxTouchOffset;
3854 }
3855
3856 public void resetTouchOffsets() {
3857 mMinTouchOffset = mMaxTouchOffset = -1;
3858 }
3859
3860 /**
3861 * @return true iff this controller is currently used to move the selection start.
3862 */
3863 public boolean isSelectionStartDragged() {
3864 return mStartHandle != null && mStartHandle.isDragging();
3865 }
3866
3867 public void onTouchModeChanged(boolean isInTouchMode) {
3868 if (!isInTouchMode) {
3869 hide();
3870 }
3871 }
3872
3873 @Override
3874 public void onDetached() {
3875 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3876 observer.removeOnTouchModeChangeListener(this);
3877
3878 if (mStartHandle != null) mStartHandle.onDetached();
3879 if (mEndHandle != null) mEndHandle.onDetached();
3880 }
3881 }
3882
3883 private class CorrectionHighlighter {
3884 private final Path mPath = new Path();
3885 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
3886 private int mStart, mEnd;
3887 private long mFadingStartTime;
3888 private RectF mTempRectF;
3889 private final static int FADE_OUT_DURATION = 400;
3890
3891 public CorrectionHighlighter() {
3892 mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
3893 applicationScale);
3894 mPaint.setStyle(Paint.Style.FILL);
3895 }
3896
3897 public void highlight(CorrectionInfo info) {
3898 mStart = info.getOffset();
3899 mEnd = mStart + info.getNewText().length();
3900 mFadingStartTime = SystemClock.uptimeMillis();
3901
3902 if (mStart < 0 || mEnd < 0) {
3903 stopAnimation();
3904 }
3905 }
3906
3907 public void draw(Canvas canvas, int cursorOffsetVertical) {
3908 if (updatePath() && updatePaint()) {
3909 if (cursorOffsetVertical != 0) {
3910 canvas.translate(0, cursorOffsetVertical);
3911 }
3912
3913 canvas.drawPath(mPath, mPaint);
3914
3915 if (cursorOffsetVertical != 0) {
3916 canvas.translate(0, -cursorOffsetVertical);
3917 }
3918 invalidate(true); // TODO invalidate cursor region only
3919 } else {
3920 stopAnimation();
3921 invalidate(false); // TODO invalidate cursor region only
3922 }
3923 }
3924
3925 private boolean updatePaint() {
3926 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
3927 if (duration > FADE_OUT_DURATION) return false;
3928
3929 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
3930 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
3931 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
3932 ((int) (highlightColorAlpha * coef) << 24);
3933 mPaint.setColor(color);
3934 return true;
3935 }
3936
3937 private boolean updatePath() {
3938 final Layout layout = mTextView.getLayout();
3939 if (layout == null) return false;
3940
3941 // Update in case text is edited while the animation is run
3942 final int length = mTextView.getText().length();
3943 int start = Math.min(length, mStart);
3944 int end = Math.min(length, mEnd);
3945
3946 mPath.reset();
3947 layout.getSelectionPath(start, end, mPath);
3948 return true;
3949 }
3950
3951 private void invalidate(boolean delayed) {
3952 if (mTextView.getLayout() == null) return;
3953
3954 if (mTempRectF == null) mTempRectF = new RectF();
3955 mPath.computeBounds(mTempRectF, false);
3956
3957 int left = mTextView.getCompoundPaddingLeft();
3958 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
3959
3960 if (delayed) {
3961 mTextView.postInvalidateOnAnimation(
3962 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
3963 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
3964 } else {
3965 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
3966 (int) mTempRectF.right, (int) mTempRectF.bottom);
3967 }
3968 }
3969
3970 private void stopAnimation() {
3971 Editor.this.mCorrectionHighlighter = null;
3972 }
3973 }
3974
3975 private static class ErrorPopup extends PopupWindow {
3976 private boolean mAbove = false;
3977 private final TextView mView;
3978 private int mPopupInlineErrorBackgroundId = 0;
3979 private int mPopupInlineErrorAboveBackgroundId = 0;
3980
3981 ErrorPopup(TextView v, int width, int height) {
3982 super(v, width, height);
3983 mView = v;
3984 // Make sure the TextView has a background set as it will be used the first time it is
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -08003985 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07003986 // dimensions identical to the above version for this to work (and is more likely).
3987 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3988 com.android.internal.R.styleable.Theme_errorMessageBackground);
3989 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
3990 }
3991
3992 void fixDirection(boolean above) {
3993 mAbove = above;
3994
3995 if (above) {
3996 mPopupInlineErrorAboveBackgroundId =
3997 getResourceId(mPopupInlineErrorAboveBackgroundId,
3998 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
3999 } else {
4000 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4001 com.android.internal.R.styleable.Theme_errorMessageBackground);
4002 }
4003
4004 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
4005 mPopupInlineErrorBackgroundId);
4006 }
4007
4008 private int getResourceId(int currentId, int index) {
4009 if (currentId == 0) {
4010 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
4011 R.styleable.Theme);
4012 currentId = styledAttributes.getResourceId(index, 0);
4013 styledAttributes.recycle();
4014 }
4015 return currentId;
4016 }
4017
4018 @Override
4019 public void update(int x, int y, int w, int h, boolean force) {
4020 super.update(x, y, w, h, force);
4021
4022 boolean above = isAboveAnchor();
4023 if (above != mAbove) {
4024 fixDirection(above);
4025 }
4026 }
4027 }
4028
4029 static class InputContentType {
4030 int imeOptions = EditorInfo.IME_NULL;
4031 String privateImeOptions;
4032 CharSequence imeActionLabel;
4033 int imeActionId;
4034 Bundle extras;
4035 OnEditorActionListener onEditorActionListener;
4036 boolean enterDown;
4037 }
4038
4039 static class InputMethodState {
4040 Rect mCursorRectInWindow = new Rect();
4041 RectF mTmpRectF = new RectF();
4042 float[] mTmpOffset = new float[2];
Gilles Debunnec62589c2012-04-12 14:50:23 -07004043 ExtractedTextRequest mExtractedTextRequest;
4044 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07004045 int mBatchEditNesting;
4046 boolean mCursorChanged;
4047 boolean mSelectionModeChanged;
4048 boolean mContentChanged;
4049 int mChangedStart, mChangedEnd, mChangedDelta;
4050 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09004051
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004052 public static class UndoInputFilter implements InputFilter {
4053 final Editor mEditor;
4054
4055 public UndoInputFilter(Editor editor) {
4056 mEditor = editor;
4057 }
4058
4059 @Override
4060 public CharSequence filter(CharSequence source, int start, int end,
4061 Spanned dest, int dstart, int dend) {
4062 if (DEBUG_UNDO) {
4063 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ")");
4064 Log.d(TAG, "filter: dest=" + dest + " (" + dstart + "-" + dend + ")");
4065 }
4066 final UndoManager um = mEditor.mUndoManager;
4067 if (um.isInUndo()) {
4068 if (DEBUG_UNDO) Log.d(TAG, "*** skipping, currently performing undo/redo");
4069 return null;
4070 }
4071
4072 um.beginUpdate("Edit text");
4073 TextModifyOperation op = um.getLastOperation(
4074 TextModifyOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
4075 if (op != null) {
4076 if (DEBUG_UNDO) Log.d(TAG, "Last op: range=(" + op.mRangeStart + "-" + op.mRangeEnd
4077 + "), oldText=" + op.mOldText);
4078 // See if we can continue modifying this operation.
4079 if (op.mOldText == null) {
4080 // The current operation is an add... are we adding more? We are adding
4081 // more if we are either appending new text to the end of the last edit or
4082 // completely replacing some or all of the last edit.
4083 if (start < end && ((dstart >= op.mRangeStart && dend <= op.mRangeEnd)
4084 || (dstart == op.mRangeEnd && dend == op.mRangeEnd))) {
4085 op.mRangeEnd = dstart + (end-start);
4086 um.endUpdate();
4087 if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, mRangeEnd="
4088 + op.mRangeEnd);
4089 return null;
4090 }
4091 } else {
4092 // The current operation is a delete... can we delete more?
4093 if (start == end && dend == op.mRangeStart-1) {
4094 SpannableStringBuilder str;
4095 if (op.mOldText instanceof SpannableString) {
4096 str = (SpannableStringBuilder)op.mOldText;
4097 } else {
4098 str = new SpannableStringBuilder(op.mOldText);
4099 }
4100 str.insert(0, dest, dstart, dend);
4101 op.mRangeStart = dstart;
4102 op.mOldText = str;
4103 um.endUpdate();
4104 if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, range=("
4105 + op.mRangeStart + "-" + op.mRangeEnd
4106 + "), oldText=" + op.mOldText);
4107 return null;
4108 }
4109 }
4110
4111 // Couldn't add to the current undo operation, need to start a new
4112 // undo state for a new undo operation.
4113 um.commitState(null);
4114 um.setUndoLabel("Edit text");
4115 }
4116
4117 // Create a new undo state reflecting the operation being performed.
4118 op = new TextModifyOperation(mEditor.mUndoOwner);
4119 op.mRangeStart = dstart;
4120 if (start < end) {
4121 op.mRangeEnd = dstart + (end-start);
4122 } else {
4123 op.mRangeEnd = dstart;
4124 }
4125 if (dstart < dend) {
4126 op.mOldText = dest.subSequence(dstart, dend);
4127 }
4128 if (DEBUG_UNDO) Log.d(TAG, "*** adding new op, range=(" + op.mRangeStart
4129 + "-" + op.mRangeEnd + "), oldText=" + op.mOldText);
4130 um.addOperation(op, UndoManager.MERGE_MODE_NONE);
4131 um.endUpdate();
4132 return null;
4133 }
4134 }
4135
4136 public static class TextModifyOperation extends UndoOperation<TextView> {
4137 int mRangeStart, mRangeEnd;
4138 CharSequence mOldText;
4139
4140 public TextModifyOperation(UndoOwner owner) {
4141 super(owner);
4142 }
4143
4144 public TextModifyOperation(Parcel src, ClassLoader loader) {
4145 super(src, loader);
4146 mRangeStart = src.readInt();
4147 mRangeEnd = src.readInt();
4148 mOldText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src);
4149 }
4150
4151 @Override
4152 public void commit() {
4153 }
4154
4155 @Override
4156 public void undo() {
4157 swapText();
4158 }
4159
4160 @Override
4161 public void redo() {
4162 swapText();
4163 }
4164
4165 private void swapText() {
4166 // Both undo and redo involves swapping the contents of the range
4167 // in the text view with our local text.
4168 TextView tv = getOwnerData();
4169 Editable editable = (Editable)tv.getText();
4170 CharSequence curText;
4171 if (mRangeStart >= mRangeEnd) {
4172 curText = null;
4173 } else {
4174 curText = editable.subSequence(mRangeStart, mRangeEnd);
4175 }
4176 if (DEBUG_UNDO) {
4177 Log.d(TAG, "Swap: range=(" + mRangeStart + "-" + mRangeEnd
4178 + "), oldText=" + mOldText);
4179 Log.d(TAG, "Swap: curText=" + curText);
4180 }
4181 if (mOldText == null) {
4182 editable.delete(mRangeStart, mRangeEnd);
4183 mRangeEnd = mRangeStart;
4184 } else {
4185 editable.replace(mRangeStart, mRangeEnd, mOldText);
4186 mRangeEnd = mRangeStart + mOldText.length();
4187 }
4188 mOldText = curText;
4189 }
4190
4191 @Override
4192 public void writeToParcel(Parcel dest, int flags) {
4193 dest.writeInt(mRangeStart);
4194 dest.writeInt(mRangeEnd);
4195 TextUtils.writeToParcel(mOldText, dest, flags);
4196 }
4197
4198 public static final Parcelable.ClassLoaderCreator<TextModifyOperation> CREATOR
4199 = new Parcelable.ClassLoaderCreator<TextModifyOperation>() {
4200 public TextModifyOperation createFromParcel(Parcel in) {
4201 return new TextModifyOperation(in, null);
4202 }
4203
4204 public TextModifyOperation createFromParcel(Parcel in, ClassLoader loader) {
4205 return new TextModifyOperation(in, loader);
4206 }
4207
4208 public TextModifyOperation[] newArray(int size) {
4209 return new TextModifyOperation[size];
4210 }
4211 };
4212 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004213}