blob: 936da3207fc7ce4e96ead7e41885feb9ce55457a [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;
Adam Powell098e7fb2014-08-06 12:42:44 -070022import android.os.Build;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070023import android.os.Parcel;
24import android.os.Parcelable;
25import android.text.InputFilter;
26import android.text.SpannableString;
John Reck44fd8d22014-02-26 11:00:11 -080027
Adam Powell057a5852012-05-11 10:28:38 -070028import com.android.internal.util.ArrayUtils;
Adam Lesinski776abc22014-03-07 11:30:59 -050029import com.android.internal.util.GrowingArrayUtils;
Adam Powell098e7fb2014-08-06 12:42:44 -070030import com.android.internal.view.menu.MenuBuilder;
Adam Powell057a5852012-05-11 10:28:38 -070031import com.android.internal.widget.EditableInputConnection;
32
Gilles Debunned88876a2012-03-16 17:34:04 -070033import android.R;
Luca Zanolin1b15ba52013-02-20 14:31:37 +000034import android.app.PendingIntent;
35import android.app.PendingIntent.CanceledException;
Gilles Debunned88876a2012-03-16 17:34:04 -070036import android.content.ClipData;
37import android.content.ClipData.Item;
38import android.content.Context;
39import android.content.Intent;
40import android.content.pm.PackageManager;
41import android.content.res.TypedArray;
42import android.graphics.Canvas;
43import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090044import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070045import android.graphics.Paint;
46import android.graphics.Path;
47import android.graphics.Rect;
48import android.graphics.RectF;
49import android.graphics.drawable.Drawable;
50import android.inputmethodservice.ExtractEditText;
51import android.os.Bundle;
52import android.os.Handler;
53import android.os.SystemClock;
54import android.provider.Settings;
55import android.text.DynamicLayout;
56import android.text.Editable;
57import android.text.InputType;
58import android.text.Layout;
59import android.text.ParcelableSpan;
60import android.text.Selection;
61import android.text.SpanWatcher;
62import android.text.Spannable;
63import android.text.SpannableStringBuilder;
64import android.text.Spanned;
65import android.text.StaticLayout;
66import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070067import android.text.method.KeyListener;
68import android.text.method.MetaKeyKeyListener;
69import android.text.method.MovementMethod;
70import android.text.method.PasswordTransformationMethod;
71import android.text.method.WordIterator;
72import android.text.style.EasyEditSpan;
73import android.text.style.SuggestionRangeSpan;
74import android.text.style.SuggestionSpan;
75import android.text.style.TextAppearanceSpan;
76import android.text.style.URLSpan;
77import android.util.DisplayMetrics;
78import android.util.Log;
79import android.view.ActionMode;
80import android.view.ActionMode.Callback;
John Reckf666ad72014-03-14 16:24:57 -070081import android.view.RenderNode;
Gilles Debunned88876a2012-03-16 17:34:04 -070082import android.view.DragEvent;
83import android.view.Gravity;
84import android.view.HardwareCanvas;
85import android.view.LayoutInflater;
86import android.view.Menu;
87import android.view.MenuItem;
88import android.view.MotionEvent;
89import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -070090import android.view.View.DragShadowBuilder;
91import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -070092import android.view.ViewConfiguration;
93import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -070094import android.view.ViewGroup.LayoutParams;
95import android.view.ViewParent;
96import android.view.ViewTreeObserver;
97import android.view.WindowManager;
98import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090099import android.view.inputmethod.CursorAnchorInfo;
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 Reck119907c2014-08-14 09:02:01 -0700149 displayList = RenderNode.create(name, null);
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 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001300 }
1301 }
1302
1303 if (mCorrectionHighlighter != null) {
1304 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1305 }
1306
1307 if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
1308 drawCursor(canvas, cursorOffsetVertical);
1309 // Rely on the drawable entirely, do not draw the cursor line.
1310 // Has to be done after the IMM related code above which relies on the highlight.
1311 highlight = null;
1312 }
1313
1314 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1315 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1316 cursorOffsetVertical);
1317 } else {
1318 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1319 }
1320 }
1321
1322 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1323 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001324 final long lineRange = layout.getLineRangeForDraw(canvas);
1325 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1326 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1327 if (lastLine < 0) return;
1328
1329 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1330 firstLine, lastLine);
1331
1332 if (layout instanceof DynamicLayout) {
1333 if (mTextDisplayLists == null) {
Adam Lesinski776abc22014-03-07 11:30:59 -05001334 mTextDisplayLists = ArrayUtils.emptyArray(TextDisplayList.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001335 }
1336
1337 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001338 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001339 int[] blockIndices = dynamicLayout.getBlockIndices();
1340 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001341 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001342
Gilles Debunned88876a2012-03-16 17:34:04 -07001343 int endOfPreviousBlock = -1;
1344 int searchStartIndex = 0;
1345 for (int i = 0; i < numberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001346 int blockEndLine = blockEndLines[i];
Gilles Debunned88876a2012-03-16 17:34:04 -07001347 int blockIndex = blockIndices[i];
1348
1349 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1350 if (blockIsInvalid) {
1351 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1352 searchStartIndex);
Gilles Debunne157aafc2012-04-19 17:21:57 -07001353 // Note how dynamic layout's internal block indices get updated from Editor
Gilles Debunned88876a2012-03-16 17:34:04 -07001354 blockIndices[i] = blockIndex;
1355 searchStartIndex = blockIndex + 1;
1356 }
1357
John Reck7558aa72014-03-05 14:59:59 -08001358 if (mTextDisplayLists[blockIndex] == null) {
1359 mTextDisplayLists[blockIndex] =
1360 new TextDisplayList("Text " + blockIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001361 }
1362
John Reck7558aa72014-03-05 14:59:59 -08001363 final boolean blockDisplayListIsInvalid = mTextDisplayLists[blockIndex].needsRecord();
John Reckf666ad72014-03-14 16:24:57 -07001364 RenderNode blockDisplayList = mTextDisplayLists[blockIndex].displayList;
Sangkyu Lee955beb22012-12-10 15:47:00 +09001365 if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001366 final int blockBeginLine = endOfPreviousBlock + 1;
1367 final int top = layout.getLineTop(blockBeginLine);
1368 final int bottom = layout.getLineBottom(blockEndLine);
Gilles Debunnefd5bc012012-04-23 16:21:35 -07001369 int left = 0;
1370 int right = mTextView.getWidth();
1371 if (mTextView.getHorizontallyScrolling()) {
1372 float min = Float.MAX_VALUE;
1373 float max = Float.MIN_VALUE;
1374 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1375 min = Math.min(min, layout.getLineLeft(line));
1376 max = Math.max(max, layout.getLineRight(line));
1377 }
1378 left = (int) min;
1379 right = (int) (max + 0.5f);
1380 }
Gilles Debunne157aafc2012-04-19 17:21:57 -07001381
Sangkyu Lee955beb22012-12-10 15:47:00 +09001382 // Rebuild display list if it is invalid
1383 if (blockDisplayListIsInvalid) {
Romain Guy52036b12013-02-14 18:03:37 -08001384 final HardwareCanvas hardwareCanvas = blockDisplayList.start(
1385 right - left, bottom - top);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001386 try {
Romain Guy52036b12013-02-14 18:03:37 -08001387 // drawText is always relative to TextView's origin, this translation
1388 // brings this range of text back to the top left corner of the viewport
Sangkyu Lee955beb22012-12-10 15:47:00 +09001389 hardwareCanvas.translate(-left, -top);
1390 layout.drawText(hardwareCanvas, blockBeginLine, blockEndLine);
Romain Guy52036b12013-02-14 18:03:37 -08001391 // No need to untranslate, previous context is popped after
1392 // drawDisplayList
Sangkyu Lee955beb22012-12-10 15:47:00 +09001393 } finally {
John Reck8de65a82014-04-09 15:23:38 -07001394 blockDisplayList.end(hardwareCanvas);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001395 // Same as drawDisplayList below, handled by our TextView's parent
Chet Haasedd671592013-04-19 14:54:34 -07001396 blockDisplayList.setClipToBounds(false);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001397 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001398 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001399
1400 // Valid disply list whose index is >= indexFirstChangedBlock
1401 // only needs to update its drawing location.
1402 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
Gilles Debunned88876a2012-03-16 17:34:04 -07001403 }
1404
Chris Craika7090e02014-06-20 16:01:00 -07001405 ((HardwareCanvas) canvas).drawRenderNode(blockDisplayList, null,
Gilles Debunne157aafc2012-04-19 17:21:57 -07001406 0 /* no child clipping, our TextView parent enforces it */);
Gilles Debunnefd5bc012012-04-23 16:21:35 -07001407
Gilles Debunne157aafc2012-04-19 17:21:57 -07001408 endOfPreviousBlock = blockEndLine;
Gilles Debunned88876a2012-03-16 17:34:04 -07001409 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001410
1411 dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks);
Gilles Debunned88876a2012-03-16 17:34:04 -07001412 } else {
1413 // Boring layout is used for empty and hint text
1414 layout.drawText(canvas, firstLine, lastLine);
1415 }
1416 }
1417
1418 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1419 int searchStartIndex) {
1420 int length = mTextDisplayLists.length;
1421 for (int i = searchStartIndex; i < length; i++) {
1422 boolean blockIndexFound = false;
1423 for (int j = 0; j < numberOfBlocks; j++) {
1424 if (blockIndices[j] == i) {
1425 blockIndexFound = true;
1426 break;
1427 }
1428 }
1429 if (blockIndexFound) continue;
1430 return i;
1431 }
1432
1433 // No available index found, the pool has to grow
Adam Lesinski776abc22014-03-07 11:30:59 -05001434 mTextDisplayLists = GrowingArrayUtils.append(mTextDisplayLists, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001435 return length;
1436 }
1437
1438 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1439 final boolean translate = cursorOffsetVertical != 0;
1440 if (translate) canvas.translate(0, cursorOffsetVertical);
1441 for (int i = 0; i < mCursorCount; i++) {
1442 mCursorDrawable[i].draw(canvas);
1443 }
1444 if (translate) canvas.translate(0, -cursorOffsetVertical);
1445 }
1446
Gilles Debunneebc86af2012-04-20 15:10:47 -07001447 /**
1448 * Invalidates all the sub-display lists that overlap the specified character range
1449 */
1450 void invalidateTextDisplayList(Layout layout, int start, int end) {
1451 if (mTextDisplayLists != null && layout instanceof DynamicLayout) {
1452 final int firstLine = layout.getLineForOffset(start);
1453 final int lastLine = layout.getLineForOffset(end);
1454
1455 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1456 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1457 int[] blockIndices = dynamicLayout.getBlockIndices();
1458 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1459
1460 int i = 0;
1461 // Skip the blocks before firstLine
1462 while (i < numberOfBlocks) {
1463 if (blockEndLines[i] >= firstLine) break;
1464 i++;
1465 }
1466
1467 // Invalidate all subsequent blocks until lastLine is passed
1468 while (i < numberOfBlocks) {
1469 final int blockIndex = blockIndices[i];
1470 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
John Reck7558aa72014-03-05 14:59:59 -08001471 mTextDisplayLists[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07001472 }
1473 if (blockEndLines[i] >= lastLine) break;
1474 i++;
1475 }
1476 }
1477 }
1478
Gilles Debunned88876a2012-03-16 17:34:04 -07001479 void invalidateTextDisplayList() {
1480 if (mTextDisplayLists != null) {
1481 for (int i = 0; i < mTextDisplayLists.length; i++) {
John Reck7558aa72014-03-05 14:59:59 -08001482 if (mTextDisplayLists[i] != null) mTextDisplayLists[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001483 }
1484 }
1485 }
1486
1487 void updateCursorsPositions() {
1488 if (mTextView.mCursorDrawableRes == 0) {
1489 mCursorCount = 0;
1490 return;
1491 }
1492
1493 Layout layout = mTextView.getLayout();
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001494 Layout hintLayout = mTextView.getHintLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07001495 final int offset = mTextView.getSelectionStart();
1496 final int line = layout.getLineForOffset(offset);
1497 final int top = layout.getLineTop(line);
1498 final int bottom = layout.getLineTop(line + 1);
1499
1500 mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
1501
1502 int middle = bottom;
1503 if (mCursorCount == 2) {
1504 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
1505 middle = (top + bottom) >> 1;
1506 }
1507
Raph Levienafe8e9b2012-12-19 16:09:32 -08001508 boolean clamped = layout.shouldClampCursor(line);
1509 updateCursorPosition(0, top, middle,
1510 getPrimaryHorizontal(layout, hintLayout, offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001511
1512 if (mCursorCount == 2) {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001513 updateCursorPosition(1, middle, bottom,
1514 layout.getSecondaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001515 }
1516 }
1517
Raph Levienafe8e9b2012-12-19 16:09:32 -08001518 private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset,
1519 boolean clamped) {
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001520 if (TextUtils.isEmpty(layout.getText()) &&
1521 hintLayout != null &&
1522 !TextUtils.isEmpty(hintLayout.getText())) {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001523 return hintLayout.getPrimaryHorizontal(offset, clamped);
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001524 } else {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001525 return layout.getPrimaryHorizontal(offset, clamped);
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001526 }
1527 }
1528
Gilles Debunned88876a2012-03-16 17:34:04 -07001529 /**
1530 * @return true if the selection mode was actually started.
1531 */
1532 boolean startSelectionActionMode() {
1533 if (mSelectionActionMode != null) {
1534 // Selection action mode is already started
1535 return false;
1536 }
1537
1538 if (!canSelectText() || !mTextView.requestFocus()) {
1539 Log.w(TextView.LOG_TAG,
1540 "TextView does not support text selection. Action mode cancelled.");
1541 return false;
1542 }
1543
1544 if (!mTextView.hasSelection()) {
1545 // There may already be a selection on device rotation
1546 if (!selectCurrentWord()) {
1547 // No word found under cursor or text selection not permitted.
1548 return false;
1549 }
1550 }
1551
1552 boolean willExtract = extractedTextModeWillBeStarted();
1553
1554 // Do not start the action mode when extracted text will show up full screen, which would
1555 // immediately hide the newly created action bar and would be visually distracting.
1556 if (!willExtract) {
1557 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
1558 mSelectionActionMode = mTextView.startActionMode(actionModeCallback);
1559 }
1560
1561 final boolean selectionStarted = mSelectionActionMode != null || willExtract;
Gilles Debunne3473b2b2012-04-20 16:21:10 -07001562 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001563 // Show the IME to be able to replace text, except when selecting non editable text.
1564 final InputMethodManager imm = InputMethodManager.peekInstance();
1565 if (imm != null) {
1566 imm.showSoftInput(mTextView, 0, null);
1567 }
1568 }
1569
1570 return selectionStarted;
1571 }
1572
1573 private boolean extractedTextModeWillBeStarted() {
1574 if (!(mTextView instanceof ExtractEditText)) {
1575 final InputMethodManager imm = InputMethodManager.peekInstance();
1576 return imm != null && imm.isFullscreenMode();
1577 }
1578 return false;
1579 }
1580
1581 /**
1582 * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
1583 */
1584 private boolean isCursorInsideSuggestionSpan() {
1585 CharSequence text = mTextView.getText();
1586 if (!(text instanceof Spannable)) return false;
1587
1588 SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans(
1589 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class);
1590 return (suggestionSpans.length > 0);
1591 }
1592
1593 /**
1594 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
1595 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
1596 */
1597 private boolean isCursorInsideEasyCorrectionSpan() {
1598 Spannable spannable = (Spannable) mTextView.getText();
1599 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
1600 mTextView.getSelectionEnd(), SuggestionSpan.class);
1601 for (int i = 0; i < suggestionSpans.length; i++) {
1602 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
1603 return true;
1604 }
1605 }
1606 return false;
1607 }
1608
1609 void onTouchUpEvent(MotionEvent event) {
1610 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
1611 hideControllers();
1612 CharSequence text = mTextView.getText();
1613 if (!selectAllGotFocus && text.length() > 0) {
1614 // Move cursor
1615 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1616 Selection.setSelection((Spannable) text, offset);
1617 if (mSpellChecker != null) {
1618 // When the cursor moves, the word that was typed may need spell check
1619 mSpellChecker.onSelectionChanged();
1620 }
1621 if (!extractedTextModeWillBeStarted()) {
1622 if (isCursorInsideEasyCorrectionSpan()) {
1623 mShowSuggestionRunnable = new Runnable() {
1624 public void run() {
1625 showSuggestions();
1626 }
1627 };
1628 // removeCallbacks is performed on every touch
1629 mTextView.postDelayed(mShowSuggestionRunnable,
1630 ViewConfiguration.getDoubleTapTimeout());
1631 } else if (hasInsertionController()) {
1632 getInsertionController().show();
1633 }
1634 }
1635 }
1636 }
1637
1638 protected void stopSelectionActionMode() {
1639 if (mSelectionActionMode != null) {
1640 // This will hide the mSelectionModifierCursorController
1641 mSelectionActionMode.finish();
1642 }
1643 }
1644
1645 /**
1646 * @return True if this view supports insertion handles.
1647 */
1648 boolean hasInsertionController() {
1649 return mInsertionControllerEnabled;
1650 }
1651
1652 /**
1653 * @return True if this view supports selection handles.
1654 */
1655 boolean hasSelectionController() {
1656 return mSelectionControllerEnabled;
1657 }
1658
1659 InsertionPointCursorController getInsertionController() {
1660 if (!mInsertionControllerEnabled) {
1661 return null;
1662 }
1663
1664 if (mInsertionPointCursorController == null) {
1665 mInsertionPointCursorController = new InsertionPointCursorController();
1666
1667 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1668 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
1669 }
1670
1671 return mInsertionPointCursorController;
1672 }
1673
1674 SelectionModifierCursorController getSelectionController() {
1675 if (!mSelectionControllerEnabled) {
1676 return null;
1677 }
1678
1679 if (mSelectionModifierCursorController == null) {
1680 mSelectionModifierCursorController = new SelectionModifierCursorController();
1681
1682 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1683 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
1684 }
1685
1686 return mSelectionModifierCursorController;
1687 }
1688
1689 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
1690 if (mCursorDrawable[cursorIndex] == null)
Alan Viverette8eea3ea2014-02-03 18:40:20 -08001691 mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07001692 mTextView.mCursorDrawableRes);
1693
1694 if (mTempRect == null) mTempRect = new Rect();
1695 mCursorDrawable[cursorIndex].getPadding(mTempRect);
1696 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
1697 horizontal = Math.max(0.5f, horizontal - 0.5f);
1698 final int left = (int) (horizontal) - mTempRect.left;
1699 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
1700 bottom + mTempRect.bottom);
1701 }
1702
1703 /**
1704 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
1705 * a dictionnary) from the current input method, provided by it calling
1706 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
1707 * implementation flashes the background of the corrected word to provide feedback to the user.
1708 *
1709 * @param info The auto correct info about the text that was corrected.
1710 */
1711 public void onCommitCorrection(CorrectionInfo info) {
1712 if (mCorrectionHighlighter == null) {
1713 mCorrectionHighlighter = new CorrectionHighlighter();
1714 } else {
1715 mCorrectionHighlighter.invalidate(false);
1716 }
1717
1718 mCorrectionHighlighter.highlight(info);
1719 }
1720
1721 void showSuggestions() {
1722 if (mSuggestionsPopupWindow == null) {
1723 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
1724 }
1725 hideControllers();
1726 mSuggestionsPopupWindow.show();
1727 }
1728
1729 boolean areSuggestionsShown() {
1730 return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
1731 }
1732
1733 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001734 if (mPositionListener != null) {
1735 mPositionListener.onScrollChanged();
1736 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001737 }
1738
1739 /**
1740 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
1741 */
1742 private boolean shouldBlink() {
1743 if (!isCursorVisible() || !mTextView.isFocused()) return false;
1744
1745 final int start = mTextView.getSelectionStart();
1746 if (start < 0) return false;
1747
1748 final int end = mTextView.getSelectionEnd();
1749 if (end < 0) return false;
1750
1751 return start == end;
1752 }
1753
1754 void makeBlink() {
1755 if (shouldBlink()) {
1756 mShowCursor = SystemClock.uptimeMillis();
1757 if (mBlink == null) mBlink = new Blink();
1758 mBlink.removeCallbacks(mBlink);
1759 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
1760 } else {
1761 if (mBlink != null) mBlink.removeCallbacks(mBlink);
1762 }
1763 }
1764
1765 private class Blink extends Handler implements Runnable {
1766 private boolean mCancelled;
1767
1768 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07001769 if (mCancelled) {
1770 return;
1771 }
1772
1773 removeCallbacks(Blink.this);
1774
1775 if (shouldBlink()) {
1776 if (mTextView.getLayout() != null) {
1777 mTextView.invalidateCursorPath();
1778 }
1779
1780 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
1781 }
1782 }
1783
1784 void cancel() {
1785 if (!mCancelled) {
1786 removeCallbacks(Blink.this);
1787 mCancelled = true;
1788 }
1789 }
1790
1791 void uncancel() {
1792 mCancelled = false;
1793 }
1794 }
1795
1796 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
1797 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
1798 com.android.internal.R.layout.text_drag_thumbnail, null);
1799
1800 if (shadowView == null) {
1801 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
1802 }
1803
1804 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
1805 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
1806 }
1807 shadowView.setText(text);
1808 shadowView.setTextColor(mTextView.getTextColors());
1809
1810 shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge);
1811 shadowView.setGravity(Gravity.CENTER);
1812
1813 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
1814 ViewGroup.LayoutParams.WRAP_CONTENT));
1815
1816 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
1817 shadowView.measure(size, size);
1818
1819 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
1820 shadowView.invalidate();
1821 return new DragShadowBuilder(shadowView);
1822 }
1823
1824 private static class DragLocalState {
1825 public TextView sourceTextView;
1826 public int start, end;
1827
1828 public DragLocalState(TextView sourceTextView, int start, int end) {
1829 this.sourceTextView = sourceTextView;
1830 this.start = start;
1831 this.end = end;
1832 }
1833 }
1834
1835 void onDrop(DragEvent event) {
1836 StringBuilder content = new StringBuilder("");
1837 ClipData clipData = event.getClipData();
1838 final int itemCount = clipData.getItemCount();
1839 for (int i=0; i < itemCount; i++) {
1840 Item item = clipData.getItemAt(i);
Dianne Hackbornacb69bb2012-04-13 15:36:06 -07001841 content.append(item.coerceToStyledText(mTextView.getContext()));
Gilles Debunned88876a2012-03-16 17:34:04 -07001842 }
1843
1844 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1845
1846 Object localState = event.getLocalState();
1847 DragLocalState dragLocalState = null;
1848 if (localState instanceof DragLocalState) {
1849 dragLocalState = (DragLocalState) localState;
1850 }
1851 boolean dragDropIntoItself = dragLocalState != null &&
1852 dragLocalState.sourceTextView == mTextView;
1853
1854 if (dragDropIntoItself) {
1855 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
1856 // A drop inside the original selection discards the drop.
1857 return;
1858 }
1859 }
1860
1861 final int originalLength = mTextView.getText().length();
Raph Levien5a689ce2014-09-10 11:03:18 -07001862 int min = offset;
1863 int max = offset;
Gilles Debunned88876a2012-03-16 17:34:04 -07001864
1865 Selection.setSelection((Spannable) mTextView.getText(), max);
1866 mTextView.replaceText_internal(min, max, content);
1867
1868 if (dragDropIntoItself) {
1869 int dragSourceStart = dragLocalState.start;
1870 int dragSourceEnd = dragLocalState.end;
1871 if (max <= dragSourceStart) {
1872 // Inserting text before selection has shifted positions
1873 final int shift = mTextView.getText().length() - originalLength;
1874 dragSourceStart += shift;
1875 dragSourceEnd += shift;
1876 }
1877
1878 // Delete original selection
1879 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
1880
1881 // Make sure we do not leave two adjacent spaces.
Victoria Lease91373202012-09-07 16:41:59 -07001882 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
1883 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
1884 if (nextCharIdx > prevCharIdx + 1) {
1885 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
1886 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
1887 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
1888 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001889 }
1890 }
1891 }
1892
Gilles Debunnec62589c2012-04-12 14:50:23 -07001893 public void addSpanWatchers(Spannable text) {
1894 final int textLength = text.length();
1895
1896 if (mKeyListener != null) {
1897 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
1898 }
1899
Jean Chalardbaf30942013-02-28 16:01:51 -08001900 if (mSpanController == null) {
1901 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07001902 }
Jean Chalardbaf30942013-02-28 16:01:51 -08001903 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07001904 }
1905
Gilles Debunned88876a2012-03-16 17:34:04 -07001906 /**
1907 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
1908 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07001909 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07001910 */
Jean Chalardbaf30942013-02-28 16:01:51 -08001911 class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07001912
1913 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
1914
1915 private EasyEditPopupWindow mPopupWindow;
1916
Gilles Debunned88876a2012-03-16 17:34:04 -07001917 private Runnable mHidePopup;
1918
Jean Chalardbaf30942013-02-28 16:01:51 -08001919 // This function is pure but inner classes can't have static functions
1920 private boolean isNonIntermediateSelectionSpan(final Spannable text,
1921 final Object span) {
1922 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
1923 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
1924 }
1925
Gilles Debunnec62589c2012-04-12 14:50:23 -07001926 @Override
1927 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08001928 if (isNonIntermediateSelectionSpan(text, span)) {
1929 sendUpdateSelection();
1930 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07001931 if (mPopupWindow == null) {
1932 mPopupWindow = new EasyEditPopupWindow();
1933 mHidePopup = new Runnable() {
1934 @Override
1935 public void run() {
1936 hide();
1937 }
1938 };
1939 }
1940
1941 // Make sure there is only at most one EasyEditSpan in the text
1942 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00001943 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07001944 }
1945
1946 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00001947 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
1948 @Override
1949 public void onDeleteClick(EasyEditSpan span) {
1950 Editable editable = (Editable) mTextView.getText();
1951 int start = editable.getSpanStart(span);
1952 int end = editable.getSpanEnd(span);
1953 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08001954 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00001955 mTextView.deleteText_internal(start, end);
1956 }
1957 editable.removeSpan(span);
1958 }
1959 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07001960
1961 if (mTextView.getWindowVisibility() != View.VISIBLE) {
1962 // The window is not visible yet, ignore the text change.
1963 return;
1964 }
1965
1966 if (mTextView.getLayout() == null) {
1967 // The view has not been laid out yet, ignore the text change
1968 return;
1969 }
1970
1971 if (extractedTextModeWillBeStarted()) {
1972 // The input is in extract mode. Do not handle the easy edit in
1973 // the original TextView, as the ExtractEditText will do
1974 return;
1975 }
1976
1977 mPopupWindow.show();
1978 mTextView.removeCallbacks(mHidePopup);
1979 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
1980 }
1981 }
1982
1983 @Override
1984 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08001985 if (isNonIntermediateSelectionSpan(text, span)) {
1986 sendUpdateSelection();
1987 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07001988 hide();
1989 }
1990 }
1991
1992 @Override
1993 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
1994 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08001995 if (isNonIntermediateSelectionSpan(text, span)) {
1996 sendUpdateSelection();
1997 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00001998 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08001999 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002000 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002001 }
2002 }
2003
Gilles Debunned88876a2012-03-16 17:34:04 -07002004 public void hide() {
2005 if (mPopupWindow != null) {
2006 mPopupWindow.hide();
2007 mTextView.removeCallbacks(mHidePopup);
2008 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002009 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002010
Jean Chalardbaf30942013-02-28 16:01:51 -08002011 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002012 try {
2013 PendingIntent pendingIntent = span.getPendingIntent();
2014 if (pendingIntent != null) {
2015 Intent intent = new Intent();
2016 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2017 pendingIntent.send(mTextView.getContext(), 0, intent);
2018 }
2019 } catch (CanceledException e) {
2020 // This should not happen, as we should try to send the intent only once.
2021 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2022 }
2023 }
2024 }
2025
2026 /**
2027 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2028 */
2029 private interface EasyEditDeleteListener {
2030
2031 /**
2032 * Clicks the delete pop-up.
2033 */
2034 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07002035 }
2036
2037 /**
2038 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002039 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002040 */
2041 private class EasyEditPopupWindow extends PinnedPopupWindow
2042 implements OnClickListener {
2043 private static final int POPUP_TEXT_LAYOUT =
2044 com.android.internal.R.layout.text_edit_action_popup_text;
2045 private TextView mDeleteTextView;
2046 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002047 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07002048
2049 @Override
2050 protected void createPopupWindow() {
2051 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2052 com.android.internal.R.attr.textSelectHandleWindowStyle);
2053 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2054 mPopupWindow.setClippingEnabled(true);
2055 }
2056
2057 @Override
2058 protected void initContentView() {
2059 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2060 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2061 mContentView = linearLayout;
2062 mContentView.setBackgroundResource(
2063 com.android.internal.R.drawable.text_edit_side_paste_window);
2064
2065 LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
2066 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2067
2068 LayoutParams wrapContent = new LayoutParams(
2069 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2070
2071 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2072 mDeleteTextView.setLayoutParams(wrapContent);
2073 mDeleteTextView.setText(com.android.internal.R.string.delete);
2074 mDeleteTextView.setOnClickListener(this);
2075 mContentView.addView(mDeleteTextView);
2076 }
2077
Gilles Debunnec62589c2012-04-12 14:50:23 -07002078 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002079 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07002080 }
2081
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002082 private void setOnDeleteListener(EasyEditDeleteListener listener) {
2083 mOnDeleteListener = listener;
2084 }
2085
Gilles Debunned88876a2012-03-16 17:34:04 -07002086 @Override
2087 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002088 if (view == mDeleteTextView
2089 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2090 && mOnDeleteListener != null) {
2091 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07002092 }
2093 }
2094
2095 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002096 public void hide() {
2097 if (mEasyEditSpan != null) {
2098 mEasyEditSpan.setDeleteEnabled(false);
2099 }
2100 mOnDeleteListener = null;
2101 super.hide();
2102 }
2103
2104 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07002105 protected int getTextOffset() {
2106 // Place the pop-up at the end of the span
2107 Editable editable = (Editable) mTextView.getText();
2108 return editable.getSpanEnd(mEasyEditSpan);
2109 }
2110
2111 @Override
2112 protected int getVerticalLocalPosition(int line) {
2113 return mTextView.getLayout().getLineBottom(line);
2114 }
2115
2116 @Override
2117 protected int clipVertically(int positionY) {
2118 // As we display the pop-up below the span, no vertical clipping is required.
2119 return positionY;
2120 }
2121 }
2122
2123 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
2124 // 3 handles
2125 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09002126 // 1 CursorAnchorInfoNotifier
2127 private final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07002128 private TextViewPositionListener[] mPositionListeners =
2129 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
2130 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
2131 private boolean mPositionHasChanged = true;
2132 // Absolute position of the TextView with respect to its parent window
2133 private int mPositionX, mPositionY;
2134 private int mNumberOfListeners;
2135 private boolean mScrollHasChanged;
2136 final int[] mTempCoords = new int[2];
2137
2138 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
2139 if (mNumberOfListeners == 0) {
2140 updatePosition();
2141 ViewTreeObserver vto = mTextView.getViewTreeObserver();
2142 vto.addOnPreDrawListener(this);
2143 }
2144
2145 int emptySlotIndex = -1;
2146 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2147 TextViewPositionListener listener = mPositionListeners[i];
2148 if (listener == positionListener) {
2149 return;
2150 } else if (emptySlotIndex < 0 && listener == null) {
2151 emptySlotIndex = i;
2152 }
2153 }
2154
2155 mPositionListeners[emptySlotIndex] = positionListener;
2156 mCanMove[emptySlotIndex] = canMove;
2157 mNumberOfListeners++;
2158 }
2159
2160 public void removeSubscriber(TextViewPositionListener positionListener) {
2161 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2162 if (mPositionListeners[i] == positionListener) {
2163 mPositionListeners[i] = null;
2164 mNumberOfListeners--;
2165 break;
2166 }
2167 }
2168
2169 if (mNumberOfListeners == 0) {
2170 ViewTreeObserver vto = mTextView.getViewTreeObserver();
2171 vto.removeOnPreDrawListener(this);
2172 }
2173 }
2174
2175 public int getPositionX() {
2176 return mPositionX;
2177 }
2178
2179 public int getPositionY() {
2180 return mPositionY;
2181 }
2182
2183 @Override
2184 public boolean onPreDraw() {
2185 updatePosition();
2186
2187 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2188 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
2189 TextViewPositionListener positionListener = mPositionListeners[i];
2190 if (positionListener != null) {
2191 positionListener.updatePosition(mPositionX, mPositionY,
2192 mPositionHasChanged, mScrollHasChanged);
2193 }
2194 }
2195 }
2196
2197 mScrollHasChanged = false;
2198 return true;
2199 }
2200
2201 private void updatePosition() {
2202 mTextView.getLocationInWindow(mTempCoords);
2203
2204 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
2205
2206 mPositionX = mTempCoords[0];
2207 mPositionY = mTempCoords[1];
2208 }
2209
2210 public void onScrollChanged() {
2211 mScrollHasChanged = true;
2212 }
2213 }
2214
2215 private abstract class PinnedPopupWindow implements TextViewPositionListener {
2216 protected PopupWindow mPopupWindow;
2217 protected ViewGroup mContentView;
2218 int mPositionX, mPositionY;
2219
2220 protected abstract void createPopupWindow();
2221 protected abstract void initContentView();
2222 protected abstract int getTextOffset();
2223 protected abstract int getVerticalLocalPosition(int line);
2224 protected abstract int clipVertically(int positionY);
2225
2226 public PinnedPopupWindow() {
2227 createPopupWindow();
2228
2229 mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
2230 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
2231 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
2232
2233 initContentView();
2234
2235 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2236 ViewGroup.LayoutParams.WRAP_CONTENT);
2237 mContentView.setLayoutParams(wrapContent);
2238
2239 mPopupWindow.setContentView(mContentView);
2240 }
2241
2242 public void show() {
2243 getPositionListener().addSubscriber(this, false /* offset is fixed */);
2244
2245 computeLocalPosition();
2246
2247 final PositionListener positionListener = getPositionListener();
2248 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
2249 }
2250
2251 protected void measureContent() {
2252 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2253 mContentView.measure(
2254 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
2255 View.MeasureSpec.AT_MOST),
2256 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
2257 View.MeasureSpec.AT_MOST));
2258 }
2259
2260 /* The popup window will be horizontally centered on the getTextOffset() and vertically
2261 * positioned according to viewportToContentHorizontalOffset.
2262 *
2263 * This method assumes that mContentView has properly been measured from its content. */
2264 private void computeLocalPosition() {
2265 measureContent();
2266 final int width = mContentView.getMeasuredWidth();
2267 final int offset = getTextOffset();
2268 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
2269 mPositionX += mTextView.viewportToContentHorizontalOffset();
2270
2271 final int line = mTextView.getLayout().getLineForOffset(offset);
2272 mPositionY = getVerticalLocalPosition(line);
2273 mPositionY += mTextView.viewportToContentVerticalOffset();
2274 }
2275
2276 private void updatePosition(int parentPositionX, int parentPositionY) {
2277 int positionX = parentPositionX + mPositionX;
2278 int positionY = parentPositionY + mPositionY;
2279
2280 positionY = clipVertically(positionY);
2281
2282 // Horizontal clipping
2283 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2284 final int width = mContentView.getMeasuredWidth();
2285 positionX = Math.min(displayMetrics.widthPixels - width, positionX);
2286 positionX = Math.max(0, positionX);
2287
2288 if (isShowing()) {
2289 mPopupWindow.update(positionX, positionY, -1, -1);
2290 } else {
2291 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
2292 positionX, positionY);
2293 }
2294 }
2295
2296 public void hide() {
2297 mPopupWindow.dismiss();
2298 getPositionListener().removeSubscriber(this);
2299 }
2300
2301 @Override
2302 public void updatePosition(int parentPositionX, int parentPositionY,
2303 boolean parentPositionChanged, boolean parentScrolled) {
2304 // Either parentPositionChanged or parentScrolled is true, check if still visible
2305 if (isShowing() && isOffsetVisible(getTextOffset())) {
2306 if (parentScrolled) computeLocalPosition();
2307 updatePosition(parentPositionX, parentPositionY);
2308 } else {
2309 hide();
2310 }
2311 }
2312
2313 public boolean isShowing() {
2314 return mPopupWindow.isShowing();
2315 }
2316 }
2317
2318 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
2319 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
2320 private static final int ADD_TO_DICTIONARY = -1;
2321 private static final int DELETE_TEXT = -2;
2322 private SuggestionInfo[] mSuggestionInfos;
2323 private int mNumberOfSuggestions;
2324 private boolean mCursorWasVisibleBeforeSuggestions;
2325 private boolean mIsShowingUp = false;
2326 private SuggestionAdapter mSuggestionsAdapter;
2327 private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
2328 private final HashMap<SuggestionSpan, Integer> mSpansLengths;
2329
2330 private class CustomPopupWindow extends PopupWindow {
Alan Viverette617feb92013-09-09 18:09:13 -07002331 public CustomPopupWindow(Context context, int defStyleAttr) {
2332 super(context, null, defStyleAttr);
Gilles Debunned88876a2012-03-16 17:34:04 -07002333 }
2334
2335 @Override
2336 public void dismiss() {
2337 super.dismiss();
2338
2339 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
2340
2341 // Safe cast since show() checks that mTextView.getText() is an Editable
2342 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
2343
2344 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
2345 if (hasInsertionController()) {
2346 getInsertionController().show();
2347 }
2348 }
2349 }
2350
2351 public SuggestionsPopupWindow() {
2352 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2353 mSuggestionSpanComparator = new SuggestionSpanComparator();
2354 mSpansLengths = new HashMap<SuggestionSpan, Integer>();
2355 }
2356
2357 @Override
2358 protected void createPopupWindow() {
2359 mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
2360 com.android.internal.R.attr.textSuggestionsWindowStyle);
2361 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2362 mPopupWindow.setFocusable(true);
2363 mPopupWindow.setClippingEnabled(false);
2364 }
2365
2366 @Override
2367 protected void initContentView() {
2368 ListView listView = new ListView(mTextView.getContext());
2369 mSuggestionsAdapter = new SuggestionAdapter();
2370 listView.setAdapter(mSuggestionsAdapter);
2371 listView.setOnItemClickListener(this);
2372 mContentView = listView;
2373
2374 // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
2375 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
2376 for (int i = 0; i < mSuggestionInfos.length; i++) {
2377 mSuggestionInfos[i] = new SuggestionInfo();
2378 }
2379 }
2380
2381 public boolean isShowingUp() {
2382 return mIsShowingUp;
2383 }
2384
2385 public void onParentLostFocus() {
2386 mIsShowingUp = false;
2387 }
2388
2389 private class SuggestionInfo {
2390 int suggestionStart, suggestionEnd; // range of actual suggestion within text
2391 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
2392 int suggestionIndex; // the index of this suggestion inside suggestionSpan
2393 SpannableStringBuilder text = new SpannableStringBuilder();
2394 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(),
2395 android.R.style.TextAppearance_SuggestionHighlight);
2396 }
2397
2398 private class SuggestionAdapter extends BaseAdapter {
2399 private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
2400 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2401
2402 @Override
2403 public int getCount() {
2404 return mNumberOfSuggestions;
2405 }
2406
2407 @Override
2408 public Object getItem(int position) {
2409 return mSuggestionInfos[position];
2410 }
2411
2412 @Override
2413 public long getItemId(int position) {
2414 return position;
2415 }
2416
2417 @Override
2418 public View getView(int position, View convertView, ViewGroup parent) {
2419 TextView textView = (TextView) convertView;
2420
2421 if (textView == null) {
2422 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
2423 parent, false);
2424 }
2425
2426 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2427 textView.setText(suggestionInfo.text);
2428
Gilles Debunne1daba182012-06-26 11:41:03 -07002429 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY ||
2430 suggestionInfo.suggestionIndex == DELETE_TEXT) {
2431 textView.setBackgroundColor(Color.TRANSPARENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07002432 } else {
Gilles Debunne1daba182012-06-26 11:41:03 -07002433 textView.setBackgroundColor(Color.WHITE);
Gilles Debunned88876a2012-03-16 17:34:04 -07002434 }
2435
2436 return textView;
2437 }
2438 }
2439
2440 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
2441 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
2442 final int flag1 = span1.getFlags();
2443 final int flag2 = span2.getFlags();
2444 if (flag1 != flag2) {
2445 // The order here should match what is used in updateDrawState
2446 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2447 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2448 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2449 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2450 if (easy1 && !misspelled1) return -1;
2451 if (easy2 && !misspelled2) return 1;
2452 if (misspelled1) return -1;
2453 if (misspelled2) return 1;
2454 }
2455
2456 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
2457 }
2458 }
2459
2460 /**
2461 * Returns the suggestion spans that cover the current cursor position. The suggestion
2462 * spans are sorted according to the length of text that they are attached to.
2463 */
2464 private SuggestionSpan[] getSuggestionSpans() {
2465 int pos = mTextView.getSelectionStart();
2466 Spannable spannable = (Spannable) mTextView.getText();
2467 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
2468
2469 mSpansLengths.clear();
2470 for (SuggestionSpan suggestionSpan : suggestionSpans) {
2471 int start = spannable.getSpanStart(suggestionSpan);
2472 int end = spannable.getSpanEnd(suggestionSpan);
2473 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
2474 }
2475
2476 // The suggestions are sorted according to their types (easy correction first, then
2477 // misspelled) and to the length of the text that they cover (shorter first).
2478 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
2479 return suggestionSpans;
2480 }
2481
2482 @Override
2483 public void show() {
2484 if (!(mTextView.getText() instanceof Editable)) return;
2485
2486 if (updateSuggestions()) {
2487 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2488 mTextView.setCursorVisible(false);
2489 mIsShowingUp = true;
2490 super.show();
2491 }
2492 }
2493
2494 @Override
2495 protected void measureContent() {
2496 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2497 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
2498 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
2499 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
2500 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
2501
2502 int width = 0;
2503 View view = null;
2504 for (int i = 0; i < mNumberOfSuggestions; i++) {
2505 view = mSuggestionsAdapter.getView(i, view, mContentView);
2506 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
2507 view.measure(horizontalMeasure, verticalMeasure);
2508 width = Math.max(width, view.getMeasuredWidth());
2509 }
2510
2511 // Enforce the width based on actual text widths
2512 mContentView.measure(
2513 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
2514 verticalMeasure);
2515
2516 Drawable popupBackground = mPopupWindow.getBackground();
2517 if (popupBackground != null) {
2518 if (mTempRect == null) mTempRect = new Rect();
2519 popupBackground.getPadding(mTempRect);
2520 width += mTempRect.left + mTempRect.right;
2521 }
2522 mPopupWindow.setWidth(width);
2523 }
2524
2525 @Override
2526 protected int getTextOffset() {
2527 return mTextView.getSelectionStart();
2528 }
2529
2530 @Override
2531 protected int getVerticalLocalPosition(int line) {
2532 return mTextView.getLayout().getLineBottom(line);
2533 }
2534
2535 @Override
2536 protected int clipVertically(int positionY) {
2537 final int height = mContentView.getMeasuredHeight();
2538 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2539 return Math.min(positionY, displayMetrics.heightPixels - height);
2540 }
2541
2542 @Override
2543 public void hide() {
2544 super.hide();
2545 }
2546
2547 private boolean updateSuggestions() {
2548 Spannable spannable = (Spannable) mTextView.getText();
2549 SuggestionSpan[] suggestionSpans = getSuggestionSpans();
2550
2551 final int nbSpans = suggestionSpans.length;
2552 // Suggestions are shown after a delay: the underlying spans may have been removed
2553 if (nbSpans == 0) return false;
2554
2555 mNumberOfSuggestions = 0;
2556 int spanUnionStart = mTextView.getText().length();
2557 int spanUnionEnd = 0;
2558
2559 SuggestionSpan misspelledSpan = null;
2560 int underlineColor = 0;
2561
2562 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
2563 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
2564 final int spanStart = spannable.getSpanStart(suggestionSpan);
2565 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
2566 spanUnionStart = Math.min(spanStart, spanUnionStart);
2567 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
2568
2569 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2570 misspelledSpan = suggestionSpan;
2571 }
2572
2573 // The first span dictates the background color of the highlighted text
2574 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
2575
2576 String[] suggestions = suggestionSpan.getSuggestions();
2577 int nbSuggestions = suggestions.length;
2578 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
2579 String suggestion = suggestions[suggestionIndex];
2580
2581 boolean suggestionIsDuplicate = false;
2582 for (int i = 0; i < mNumberOfSuggestions; i++) {
2583 if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
2584 SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
2585 final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
2586 final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
2587 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
2588 suggestionIsDuplicate = true;
2589 break;
2590 }
2591 }
2592 }
2593
2594 if (!suggestionIsDuplicate) {
2595 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2596 suggestionInfo.suggestionSpan = suggestionSpan;
2597 suggestionInfo.suggestionIndex = suggestionIndex;
2598 suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
2599
2600 mNumberOfSuggestions++;
2601
2602 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
2603 // Also end outer for loop
2604 spanIndex = nbSpans;
2605 break;
2606 }
2607 }
2608 }
2609 }
2610
2611 for (int i = 0; i < mNumberOfSuggestions; i++) {
2612 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
2613 }
2614
2615 // Add "Add to dictionary" item if there is a span with the misspelled flag
2616 if (misspelledSpan != null) {
2617 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
2618 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
2619 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
2620 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2621 suggestionInfo.suggestionSpan = misspelledSpan;
2622 suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
2623 suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView.
2624 getContext().getString(com.android.internal.R.string.addToDictionary));
2625 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2626 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2627
2628 mNumberOfSuggestions++;
2629 }
2630 }
2631
2632 // Delete item
2633 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2634 suggestionInfo.suggestionSpan = null;
2635 suggestionInfo.suggestionIndex = DELETE_TEXT;
2636 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
2637 mTextView.getContext().getString(com.android.internal.R.string.deleteText));
2638 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2639 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2640 mNumberOfSuggestions++;
2641
2642 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
2643 if (underlineColor == 0) {
2644 // Fallback on the default highlight color when the first span does not provide one
2645 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
2646 } else {
2647 final float BACKGROUND_TRANSPARENCY = 0.4f;
2648 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
2649 mSuggestionRangeSpan.setBackgroundColor(
2650 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
2651 }
2652 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
2653 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2654
2655 mSuggestionsAdapter.notifyDataSetChanged();
2656 return true;
2657 }
2658
2659 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
2660 int unionEnd) {
2661 final Spannable text = (Spannable) mTextView.getText();
2662 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
2663 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
2664
2665 // Adjust the start/end of the suggestion span
2666 suggestionInfo.suggestionStart = spanStart - unionStart;
2667 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
2668 + suggestionInfo.text.length();
2669
2670 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
2671 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2672
2673 // Add the text before and after the span.
2674 final String textAsString = text.toString();
2675 suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
2676 suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
2677 }
2678
2679 @Override
2680 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
2681 Editable editable = (Editable) mTextView.getText();
2682 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2683
2684 if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
2685 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
2686 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
2687 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
2688 // Do not leave two adjacent spaces after deletion, or one at beginning of text
2689 if (spanUnionEnd < editable.length() &&
2690 Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
2691 (spanUnionStart == 0 ||
2692 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
2693 spanUnionEnd = spanUnionEnd + 1;
2694 }
2695 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
2696 }
2697 hide();
2698 return;
2699 }
2700
2701 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
2702 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
2703 if (spanStart < 0 || spanEnd <= spanStart) {
2704 // Span has been removed
2705 hide();
2706 return;
2707 }
2708
2709 final String originalText = editable.toString().substring(spanStart, spanEnd);
2710
2711 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
2712 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
2713 intent.putExtra("word", originalText);
2714 intent.putExtra("locale", mTextView.getTextServicesLocale().toString());
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09002715 // Put a listener to replace the original text with a word which the user
2716 // modified in a user dictionary dialog.
Gilles Debunned88876a2012-03-16 17:34:04 -07002717 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
2718 mTextView.getContext().startActivity(intent);
2719 // There is no way to know if the word was indeed added. Re-check.
2720 // TODO The ExtractEditText should remove the span in the original text instead
2721 editable.removeSpan(suggestionInfo.suggestionSpan);
Gilles Debunne2eb70fb2012-04-18 17:57:45 -07002722 Selection.setSelection(editable, spanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002723 updateSpellCheckSpans(spanStart, spanEnd, false);
2724 } else {
2725 // SuggestionSpans are removed by replace: save them before
2726 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2727 SuggestionSpan.class);
2728 final int length = suggestionSpans.length;
2729 int[] suggestionSpansStarts = new int[length];
2730 int[] suggestionSpansEnds = new int[length];
2731 int[] suggestionSpansFlags = new int[length];
2732 for (int i = 0; i < length; i++) {
2733 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2734 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2735 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2736 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2737
2738 // Remove potential misspelled flags
2739 int suggestionSpanFlags = suggestionSpan.getFlags();
2740 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
2741 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2742 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2743 suggestionSpan.setFlags(suggestionSpanFlags);
2744 }
2745 }
2746
2747 final int suggestionStart = suggestionInfo.suggestionStart;
2748 final int suggestionEnd = suggestionInfo.suggestionEnd;
2749 final String suggestion = suggestionInfo.text.subSequence(
2750 suggestionStart, suggestionEnd).toString();
2751 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2752
Luca Zanolin0c96b812012-08-29 11:33:12 +01002753 // Notify source IME of the suggestion pick. Do this before
2754 // swaping texts.
2755 suggestionInfo.suggestionSpan.notifySelection(
2756 mTextView.getContext(), originalText, suggestionInfo.suggestionIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07002757
2758 // Swap text content between actual text and Suggestion span
2759 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
2760 suggestions[suggestionInfo.suggestionIndex] = originalText;
2761
2762 // Restore previous SuggestionSpans
2763 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
2764 for (int i = 0; i < length; i++) {
2765 // Only spans that include the modified region make sense after replacement
2766 // Spans partially included in the replaced region are removed, there is no
2767 // way to assign them a valid range after replacement
2768 if (suggestionSpansStarts[i] <= spanStart &&
2769 suggestionSpansEnds[i] >= spanEnd) {
2770 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2771 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
2772 }
2773 }
2774
2775 // Move cursor at the end of the replaced word
2776 final int newCursorPosition = spanEnd + lengthDifference;
2777 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2778 }
2779
2780 hide();
2781 }
2782 }
2783
2784 /**
2785 * An ActionMode Callback class that is used to provide actions while in text selection mode.
2786 *
2787 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
2788 * on which of these this TextView supports.
2789 */
2790 private class SelectionActionModeCallback implements ActionMode.Callback {
2791
2792 @Override
2793 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Adam Powell098e7fb2014-08-06 12:42:44 -07002794 final boolean legacy = mTextView.getContext().getApplicationInfo().targetSdkVersion <
Dianne Hackborn955d8d62014-10-07 20:17:19 -07002795 Build.VERSION_CODES.LOLLIPOP;
Adam Powell098e7fb2014-08-06 12:42:44 -07002796 final Context context = !legacy && menu instanceof MenuBuilder ?
2797 ((MenuBuilder) menu).getContext() :
2798 mTextView.getContext();
2799 final TypedArray styledAttributes = context.obtainStyledAttributes(
Gilles Debunned88876a2012-03-16 17:34:04 -07002800 com.android.internal.R.styleable.SelectionModeDrawables);
2801
Gilles Debunned88876a2012-03-16 17:34:04 -07002802 mode.setTitle(mTextView.getContext().getString(
2803 com.android.internal.R.string.textSelectionCABTitle));
2804 mode.setSubtitle(null);
2805 mode.setTitleOptionalHint(true);
2806
Gilles Debunned88876a2012-03-16 17:34:04 -07002807 menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
Sungmin Choif0369202013-01-25 21:39:01 +09002808 setIcon(styledAttributes.getResourceId(
2809 R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0)).
Gilles Debunned88876a2012-03-16 17:34:04 -07002810 setAlphabeticShortcut('a').
2811 setShowAsAction(
2812 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2813
2814 if (mTextView.canCut()) {
2815 menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut).
2816 setIcon(styledAttributes.getResourceId(
2817 R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
2818 setAlphabeticShortcut('x').
2819 setShowAsAction(
2820 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2821 }
2822
2823 if (mTextView.canCopy()) {
2824 menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy).
2825 setIcon(styledAttributes.getResourceId(
2826 R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
2827 setAlphabeticShortcut('c').
2828 setShowAsAction(
2829 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2830 }
2831
2832 if (mTextView.canPaste()) {
2833 menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste).
2834 setIcon(styledAttributes.getResourceId(
2835 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
2836 setAlphabeticShortcut('v').
2837 setShowAsAction(
2838 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2839 }
2840
2841 styledAttributes.recycle();
2842
2843 if (mCustomSelectionActionModeCallback != null) {
2844 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
2845 // The custom mode can choose to cancel the action mode
2846 return false;
2847 }
2848 }
2849
2850 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
2851 getSelectionController().show();
Adam Powell057a5852012-05-11 10:28:38 -07002852 mTextView.setHasTransientState(true);
Gilles Debunned88876a2012-03-16 17:34:04 -07002853 return true;
2854 } else {
2855 return false;
2856 }
2857 }
2858
2859 @Override
2860 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
2861 if (mCustomSelectionActionModeCallback != null) {
2862 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
2863 }
2864 return true;
2865 }
2866
2867 @Override
2868 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
2869 if (mCustomSelectionActionModeCallback != null &&
2870 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
2871 return true;
2872 }
2873 return mTextView.onTextContextMenuItem(item.getItemId());
2874 }
2875
2876 @Override
2877 public void onDestroyActionMode(ActionMode mode) {
2878 if (mCustomSelectionActionModeCallback != null) {
2879 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
2880 }
Adam Powell057a5852012-05-11 10:28:38 -07002881
2882 /*
2883 * If we're ending this mode because we're detaching from a window,
2884 * we still have selection state to preserve. Don't clear it, we'll
2885 * bring back the selection mode when (if) we get reattached.
2886 */
2887 if (!mPreserveDetachedSelection) {
2888 Selection.setSelection((Spannable) mTextView.getText(),
2889 mTextView.getSelectionEnd());
2890 mTextView.setHasTransientState(false);
2891 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002892
2893 if (mSelectionModifierCursorController != null) {
2894 mSelectionModifierCursorController.hide();
2895 }
2896
2897 mSelectionActionMode = null;
2898 }
2899 }
2900
2901 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
2902 private static final int POPUP_TEXT_LAYOUT =
2903 com.android.internal.R.layout.text_edit_action_popup_text;
2904 private TextView mPasteTextView;
2905 private TextView mReplaceTextView;
2906
2907 @Override
2908 protected void createPopupWindow() {
2909 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2910 com.android.internal.R.attr.textSelectHandleWindowStyle);
2911 mPopupWindow.setClippingEnabled(true);
2912 }
2913
2914 @Override
2915 protected void initContentView() {
2916 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2917 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2918 mContentView = linearLayout;
2919 mContentView.setBackgroundResource(
2920 com.android.internal.R.drawable.text_edit_paste_window);
2921
2922 LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
2923 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2924
2925 LayoutParams wrapContent = new LayoutParams(
2926 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2927
2928 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2929 mPasteTextView.setLayoutParams(wrapContent);
2930 mContentView.addView(mPasteTextView);
2931 mPasteTextView.setText(com.android.internal.R.string.paste);
2932 mPasteTextView.setOnClickListener(this);
2933
2934 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2935 mReplaceTextView.setLayoutParams(wrapContent);
2936 mContentView.addView(mReplaceTextView);
2937 mReplaceTextView.setText(com.android.internal.R.string.replace);
2938 mReplaceTextView.setOnClickListener(this);
2939 }
2940
2941 @Override
2942 public void show() {
2943 boolean canPaste = mTextView.canPaste();
2944 boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
2945 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
2946 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
2947
2948 if (!canPaste && !canSuggest) return;
2949
2950 super.show();
2951 }
2952
2953 @Override
2954 public void onClick(View view) {
2955 if (view == mPasteTextView && mTextView.canPaste()) {
2956 mTextView.onTextContextMenuItem(TextView.ID_PASTE);
2957 hide();
2958 } else if (view == mReplaceTextView) {
2959 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2960 stopSelectionActionMode();
2961 Selection.setSelection((Spannable) mTextView.getText(), middle);
2962 showSuggestions();
2963 }
2964 }
2965
2966 @Override
2967 protected int getTextOffset() {
2968 return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2969 }
2970
2971 @Override
2972 protected int getVerticalLocalPosition(int line) {
2973 return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight();
2974 }
2975
2976 @Override
2977 protected int clipVertically(int positionY) {
2978 if (positionY < 0) {
2979 final int offset = getTextOffset();
2980 final Layout layout = mTextView.getLayout();
2981 final int line = layout.getLineForOffset(offset);
2982 positionY += layout.getLineBottom(line) - layout.getLineTop(line);
2983 positionY += mContentView.getMeasuredHeight();
2984
2985 // Assumes insertion and selection handles share the same height
Alan Viverette8eea3ea2014-02-03 18:40:20 -08002986 final Drawable handle = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07002987 mTextView.mTextSelectHandleRes);
2988 positionY += handle.getIntrinsicHeight();
2989 }
2990
2991 return positionY;
2992 }
2993 }
2994
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09002995 /**
2996 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
2997 * while the input method is requesting the cursor/anchor position. Does nothing as long as
2998 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
2999 */
3000 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09003001 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003002 final int[] mTmpIntOffset = new int[2];
3003 final Matrix mViewToScreenMatrix = new Matrix();
3004
3005 @Override
3006 public void updatePosition(int parentPositionX, int parentPositionY,
3007 boolean parentPositionChanged, boolean parentScrolled) {
3008 final InputMethodState ims = mInputMethodState;
3009 if (ims == null || ims.mBatchEditNesting > 0) {
3010 return;
3011 }
3012 final InputMethodManager imm = InputMethodManager.peekInstance();
3013 if (null == imm) {
3014 return;
3015 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09003016 if (!imm.isActive(mTextView)) {
3017 return;
3018 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003019 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09003020 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003021 return;
3022 }
3023 Layout layout = mTextView.getLayout();
3024 if (layout == null) {
3025 return;
3026 }
3027
Yohei Yukawac46b5f02014-06-10 12:26:34 +09003028 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003029 builder.reset();
3030
3031 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09003032 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003033
3034 // Construct transformation matrix from view local coordinates to screen coordinates.
3035 mViewToScreenMatrix.set(mTextView.getMatrix());
3036 mTextView.getLocationOnScreen(mTmpIntOffset);
3037 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
3038 builder.setMatrix(mViewToScreenMatrix);
3039
3040 final float viewportToContentHorizontalOffset =
3041 mTextView.viewportToContentHorizontalOffset();
3042 final float viewportToContentVerticalOffset =
3043 mTextView.viewportToContentVerticalOffset();
3044
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09003045 final CharSequence text = mTextView.getText();
3046 if (text instanceof Spannable) {
3047 final Spannable sp = (Spannable) text;
3048 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
3049 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
3050 if (composingTextEnd < composingTextStart) {
3051 final int temp = composingTextEnd;
3052 composingTextEnd = composingTextStart;
3053 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003054 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09003055 final boolean hasComposingText =
3056 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
3057 if (hasComposingText) {
3058 final CharSequence composingText = text.subSequence(composingTextStart,
3059 composingTextEnd);
3060 builder.setComposingText(composingTextStart, composingText);
Yohei Yukawa5f183f02014-09-02 14:18:40 -07003061
3062 final int minLine = layout.getLineForOffset(composingTextStart);
3063 final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
3064 for (int line = minLine; line <= maxLine; ++line) {
3065 final int lineStart = layout.getLineStart(line);
3066 final int lineEnd = layout.getLineEnd(line);
3067 final int offsetStart = Math.max(lineStart, composingTextStart);
3068 final int offsetEnd = Math.min(lineEnd, composingTextEnd);
3069 final boolean ltrLine =
3070 layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
3071 final float[] widths = new float[offsetEnd - offsetStart];
3072 layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
3073 final float top = layout.getLineTop(line);
3074 final float bottom = layout.getLineBottom(line);
3075 for (int offset = offsetStart; offset < offsetEnd; ++offset) {
3076 final float charWidth = widths[offset - offsetStart];
3077 final boolean isRtl = layout.isRtlCharAt(offset);
3078 final float primary = layout.getPrimaryHorizontal(offset);
3079 final float secondary = layout.getSecondaryHorizontal(offset);
3080 // TODO: This doesn't work perfectly for text with custom styles and
3081 // TAB chars.
3082 final float left;
3083 final float right;
3084 if (ltrLine) {
3085 if (isRtl) {
3086 left = secondary - charWidth;
3087 right = secondary;
3088 } else {
3089 left = primary;
3090 right = primary + charWidth;
3091 }
3092 } else {
3093 if (!isRtl) {
3094 left = secondary;
3095 right = secondary + charWidth;
3096 } else {
3097 left = primary - charWidth;
3098 right = primary;
3099 }
3100 }
3101 // TODO: Check top-right and bottom-left as well.
3102 final float localLeft = left + viewportToContentHorizontalOffset;
3103 final float localRight = right + viewportToContentHorizontalOffset;
3104 final float localTop = top + viewportToContentVerticalOffset;
3105 final float localBottom = bottom + viewportToContentVerticalOffset;
3106 final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
3107 final boolean isBottomRightVisible =
3108 isPositionVisible(localRight, localBottom);
3109 int characterBoundsFlags = 0;
3110 if (isTopLeftVisible || isBottomRightVisible) {
3111 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
3112 }
3113 if (!isTopLeftVisible || !isTopLeftVisible) {
3114 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
3115 }
3116 if (isRtl) {
3117 characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
3118 }
3119 // Here offset is the index in Java chars.
3120 builder.addCharacterBounds(offset, localLeft, localTop, localRight,
3121 localBottom, characterBoundsFlags);
3122 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003123 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003124 }
3125 }
3126
3127 // Treat selectionStart as the insertion point.
3128 if (0 <= selectionStart) {
3129 final int offset = selectionStart;
3130 final int line = layout.getLineForOffset(offset);
3131 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
3132 + viewportToContentHorizontalOffset;
3133 final float insertionMarkerTop = layout.getLineTop(line)
3134 + viewportToContentVerticalOffset;
3135 final float insertionMarkerBaseline = layout.getLineBaseline(line)
3136 + viewportToContentVerticalOffset;
3137 final float insertionMarkerBottom = layout.getLineBottom(line)
3138 + viewportToContentVerticalOffset;
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07003139 final boolean isTopVisible =
3140 isPositionVisible(insertionMarkerX, insertionMarkerTop);
3141 final boolean isBottomVisible =
3142 isPositionVisible(insertionMarkerX, insertionMarkerBottom);
3143 int insertionMarkerFlags = 0;
3144 if (isTopVisible || isBottomVisible) {
3145 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
3146 }
3147 if (!isTopVisible || !isBottomVisible) {
3148 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
3149 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07003150 if (layout.isRtlCharAt(offset)) {
3151 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
3152 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09003153 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07003154 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003155 }
3156
3157 imm.updateCursorAnchorInfo(mTextView, builder.build());
3158 }
3159 }
3160
Gilles Debunned88876a2012-03-16 17:34:04 -07003161 private abstract class HandleView extends View implements TextViewPositionListener {
3162 protected Drawable mDrawable;
3163 protected Drawable mDrawableLtr;
3164 protected Drawable mDrawableRtl;
3165 private final PopupWindow mContainer;
3166 // Position with respect to the parent TextView
3167 private int mPositionX, mPositionY;
3168 private boolean mIsDragging;
3169 // Offset from touch position to mPosition
3170 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
3171 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07003172 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07003173 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
3174 private float mTouchOffsetY;
3175 // Where the touch position should be on the handle to ensure a maximum cursor visibility
3176 private float mIdealVerticalOffset;
3177 // Parent's (TextView) previous position in window
3178 private int mLastParentX, mLastParentY;
3179 // Transient action popup window for Paste and Replace actions
3180 protected ActionPopupWindow mActionPopupWindow;
3181 // Previous text character offset
3182 private int mPreviousOffset = -1;
3183 // Previous text character offset
3184 private boolean mPositionHasChanged = true;
3185 // Used to delay the appearance of the action popup window
3186 private Runnable mActionPopupShower;
Adam Powell3fceabd2014-08-19 18:28:04 -07003187 // Minimum touch target size for handles
3188 private int mMinSize;
Gilles Debunned88876a2012-03-16 17:34:04 -07003189
3190 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
3191 super(mTextView.getContext());
3192 mContainer = new PopupWindow(mTextView.getContext(), null,
3193 com.android.internal.R.attr.textSelectHandleWindowStyle);
3194 mContainer.setSplitTouchEnabled(true);
3195 mContainer.setClippingEnabled(false);
3196 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
3197 mContainer.setContentView(this);
3198
3199 mDrawableLtr = drawableLtr;
3200 mDrawableRtl = drawableRtl;
Adam Powell3fceabd2014-08-19 18:28:04 -07003201 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
3202 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07003203
3204 updateDrawable();
3205
Adam Powell3fceabd2014-08-19 18:28:04 -07003206 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07003207 mTouchOffsetY = -0.3f * handleHeight;
3208 mIdealVerticalOffset = 0.7f * handleHeight;
3209 }
3210
3211 protected void updateDrawable() {
3212 final int offset = getCurrentCursorOffset();
3213 final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
3214 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
3215 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07003216 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07003217 }
3218
3219 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07003220 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07003221
3222 // Touch-up filter: number of previous positions remembered
3223 private static final int HISTORY_SIZE = 5;
3224 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
3225 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
3226 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
3227 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
3228 private int mPreviousOffsetIndex = 0;
3229 private int mNumberPreviousOffsets = 0;
3230
3231 private void startTouchUpFilter(int offset) {
3232 mNumberPreviousOffsets = 0;
3233 addPositionToTouchUpFilter(offset);
3234 }
3235
3236 private void addPositionToTouchUpFilter(int offset) {
3237 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
3238 mPreviousOffsets[mPreviousOffsetIndex] = offset;
3239 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
3240 mNumberPreviousOffsets++;
3241 }
3242
3243 private void filterOnTouchUp() {
3244 final long now = SystemClock.uptimeMillis();
3245 int i = 0;
3246 int index = mPreviousOffsetIndex;
3247 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
3248 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
3249 i++;
3250 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
3251 }
3252
3253 if (i > 0 && i < iMax &&
3254 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
3255 positionAtCursorOffset(mPreviousOffsets[index], false);
3256 }
3257 }
3258
3259 public boolean offsetHasBeenChanged() {
3260 return mNumberPreviousOffsets > 1;
3261 }
3262
3263 @Override
3264 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07003265 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
3266 }
3267
3268 private int getPreferredWidth() {
3269 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
3270 }
3271
3272 private int getPreferredHeight() {
3273 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07003274 }
3275
3276 public void show() {
3277 if (isShowing()) return;
3278
3279 getPositionListener().addSubscriber(this, true /* local position may change */);
3280
3281 // Make sure the offset is always considered new, even when focusing at same position
3282 mPreviousOffset = -1;
3283 positionAtCursorOffset(getCurrentCursorOffset(), false);
3284
3285 hideActionPopupWindow();
3286 }
3287
3288 protected void dismiss() {
3289 mIsDragging = false;
3290 mContainer.dismiss();
3291 onDetached();
3292 }
3293
3294 public void hide() {
3295 dismiss();
3296
3297 getPositionListener().removeSubscriber(this);
3298 }
3299
3300 void showActionPopupWindow(int delay) {
3301 if (mActionPopupWindow == null) {
3302 mActionPopupWindow = new ActionPopupWindow();
3303 }
3304 if (mActionPopupShower == null) {
3305 mActionPopupShower = new Runnable() {
3306 public void run() {
3307 mActionPopupWindow.show();
3308 }
3309 };
3310 } else {
3311 mTextView.removeCallbacks(mActionPopupShower);
3312 }
3313 mTextView.postDelayed(mActionPopupShower, delay);
3314 }
3315
3316 protected void hideActionPopupWindow() {
3317 if (mActionPopupShower != null) {
3318 mTextView.removeCallbacks(mActionPopupShower);
3319 }
3320 if (mActionPopupWindow != null) {
3321 mActionPopupWindow.hide();
3322 }
3323 }
3324
3325 public boolean isShowing() {
3326 return mContainer.isShowing();
3327 }
3328
3329 private boolean isVisible() {
3330 // Always show a dragging handle.
3331 if (mIsDragging) {
3332 return true;
3333 }
3334
3335 if (mTextView.isInBatchEditMode()) {
3336 return false;
3337 }
3338
3339 return isPositionVisible(mPositionX + mHotspotX, mPositionY);
3340 }
3341
3342 public abstract int getCurrentCursorOffset();
3343
3344 protected abstract void updateSelection(int offset);
3345
3346 public abstract void updatePosition(float x, float y);
3347
3348 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
3349 // A HandleView relies on the layout, which may be nulled by external methods
3350 Layout layout = mTextView.getLayout();
3351 if (layout == null) {
3352 // Will update controllers' state, hiding them and stopping selection mode if needed
3353 prepareCursorControllers();
3354 return;
3355 }
3356
3357 boolean offsetChanged = offset != mPreviousOffset;
3358 if (offsetChanged || parentScrolled) {
3359 if (offsetChanged) {
3360 updateSelection(offset);
3361 addPositionToTouchUpFilter(offset);
3362 }
3363 final int line = layout.getLineForOffset(offset);
3364
Adam Powell3fceabd2014-08-19 18:28:04 -07003365 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
3366 getHorizontalOffset() + getCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07003367 mPositionY = layout.getLineBottom(line);
3368
3369 // Take TextView's padding and scroll into account.
3370 mPositionX += mTextView.viewportToContentHorizontalOffset();
3371 mPositionY += mTextView.viewportToContentVerticalOffset();
3372
3373 mPreviousOffset = offset;
3374 mPositionHasChanged = true;
3375 }
3376 }
3377
3378 public void updatePosition(int parentPositionX, int parentPositionY,
3379 boolean parentPositionChanged, boolean parentScrolled) {
3380 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
3381 if (parentPositionChanged || mPositionHasChanged) {
3382 if (mIsDragging) {
3383 // Update touchToWindow offset in case of parent scrolling while dragging
3384 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
3385 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
3386 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
3387 mLastParentX = parentPositionX;
3388 mLastParentY = parentPositionY;
3389 }
3390
3391 onHandleMoved();
3392 }
3393
3394 if (isVisible()) {
3395 final int positionX = parentPositionX + mPositionX;
3396 final int positionY = parentPositionY + mPositionY;
3397 if (isShowing()) {
3398 mContainer.update(positionX, positionY, -1, -1);
3399 } else {
3400 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3401 positionX, positionY);
3402 }
3403 } else {
3404 if (isShowing()) {
3405 dismiss();
3406 }
3407 }
3408
3409 mPositionHasChanged = false;
3410 }
3411 }
3412
3413 @Override
3414 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07003415 final int drawWidth = mDrawable.getIntrinsicWidth();
3416 final int left = getHorizontalOffset();
3417
3418 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07003419 mDrawable.draw(c);
3420 }
3421
Adam Powell3fceabd2014-08-19 18:28:04 -07003422 private int getHorizontalOffset() {
3423 final int width = getPreferredWidth();
3424 final int drawWidth = mDrawable.getIntrinsicWidth();
3425 final int left;
3426 switch (mHorizontalGravity) {
3427 case Gravity.LEFT:
3428 left = 0;
3429 break;
3430 default:
3431 case Gravity.CENTER:
3432 left = (width - drawWidth) / 2;
3433 break;
3434 case Gravity.RIGHT:
3435 left = width - drawWidth;
3436 break;
3437 }
3438 return left;
3439 }
3440
3441 protected int getCursorOffset() {
3442 return 0;
3443 }
3444
Gilles Debunned88876a2012-03-16 17:34:04 -07003445 @Override
3446 public boolean onTouchEvent(MotionEvent ev) {
3447 switch (ev.getActionMasked()) {
3448 case MotionEvent.ACTION_DOWN: {
3449 startTouchUpFilter(getCurrentCursorOffset());
3450 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
3451 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
3452
3453 final PositionListener positionListener = getPositionListener();
3454 mLastParentX = positionListener.getPositionX();
3455 mLastParentY = positionListener.getPositionY();
3456 mIsDragging = true;
3457 break;
3458 }
3459
3460 case MotionEvent.ACTION_MOVE: {
3461 final float rawX = ev.getRawX();
3462 final float rawY = ev.getRawY();
3463
3464 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
3465 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
3466 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
3467 float newVerticalOffset;
3468 if (previousVerticalOffset < mIdealVerticalOffset) {
3469 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
3470 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
3471 } else {
3472 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
3473 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
3474 }
3475 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
3476
3477 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
3478 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
3479
3480 updatePosition(newPosX, newPosY);
3481 break;
3482 }
3483
3484 case MotionEvent.ACTION_UP:
3485 filterOnTouchUp();
3486 mIsDragging = false;
3487 break;
3488
3489 case MotionEvent.ACTION_CANCEL:
3490 mIsDragging = false;
3491 break;
3492 }
3493 return true;
3494 }
3495
3496 public boolean isDragging() {
3497 return mIsDragging;
3498 }
3499
3500 void onHandleMoved() {
3501 hideActionPopupWindow();
3502 }
3503
3504 public void onDetached() {
3505 hideActionPopupWindow();
3506 }
3507 }
3508
3509 private class InsertionHandleView extends HandleView {
3510 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
3511 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
3512
3513 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
3514 private float mDownPositionX, mDownPositionY;
3515 private Runnable mHider;
3516
3517 public InsertionHandleView(Drawable drawable) {
3518 super(drawable, drawable);
3519 }
3520
3521 @Override
3522 public void show() {
3523 super.show();
3524
3525 final long durationSinceCutOrCopy =
3526 SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME;
3527 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
3528 showActionPopupWindow(0);
3529 }
3530
3531 hideAfterDelay();
3532 }
3533
3534 public void showWithActionPopup() {
3535 show();
3536 showActionPopupWindow(0);
3537 }
3538
3539 private void hideAfterDelay() {
3540 if (mHider == null) {
3541 mHider = new Runnable() {
3542 public void run() {
3543 hide();
3544 }
3545 };
3546 } else {
3547 removeHiderCallback();
3548 }
3549 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
3550 }
3551
3552 private void removeHiderCallback() {
3553 if (mHider != null) {
3554 mTextView.removeCallbacks(mHider);
3555 }
3556 }
3557
3558 @Override
3559 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3560 return drawable.getIntrinsicWidth() / 2;
3561 }
3562
3563 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07003564 protected int getHorizontalGravity(boolean isRtlRun) {
3565 return Gravity.CENTER_HORIZONTAL;
3566 }
3567
3568 @Override
3569 protected int getCursorOffset() {
3570 int offset = super.getCursorOffset();
3571 final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null;
3572 if (cursor != null) {
3573 cursor.getPadding(mTempRect);
3574 offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2;
3575 }
3576 return offset;
3577 }
3578
3579 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003580 public boolean onTouchEvent(MotionEvent ev) {
3581 final boolean result = super.onTouchEvent(ev);
3582
3583 switch (ev.getActionMasked()) {
3584 case MotionEvent.ACTION_DOWN:
3585 mDownPositionX = ev.getRawX();
3586 mDownPositionY = ev.getRawY();
3587 break;
3588
3589 case MotionEvent.ACTION_UP:
3590 if (!offsetHasBeenChanged()) {
3591 final float deltaX = mDownPositionX - ev.getRawX();
3592 final float deltaY = mDownPositionY - ev.getRawY();
3593 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3594
3595 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3596 mTextView.getContext());
3597 final int touchSlop = viewConfiguration.getScaledTouchSlop();
3598
3599 if (distanceSquared < touchSlop * touchSlop) {
3600 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
3601 // Tapping on the handle dismisses the displayed action popup
3602 mActionPopupWindow.hide();
3603 } else {
3604 showWithActionPopup();
3605 }
3606 }
3607 }
3608 hideAfterDelay();
3609 break;
3610
3611 case MotionEvent.ACTION_CANCEL:
3612 hideAfterDelay();
3613 break;
3614
3615 default:
3616 break;
3617 }
3618
3619 return result;
3620 }
3621
3622 @Override
3623 public int getCurrentCursorOffset() {
3624 return mTextView.getSelectionStart();
3625 }
3626
3627 @Override
3628 public void updateSelection(int offset) {
3629 Selection.setSelection((Spannable) mTextView.getText(), offset);
3630 }
3631
3632 @Override
3633 public void updatePosition(float x, float y) {
3634 positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
3635 }
3636
3637 @Override
3638 void onHandleMoved() {
3639 super.onHandleMoved();
3640 removeHiderCallback();
3641 }
3642
3643 @Override
3644 public void onDetached() {
3645 super.onDetached();
3646 removeHiderCallback();
3647 }
3648 }
3649
3650 private class SelectionStartHandleView extends HandleView {
3651
3652 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3653 super(drawableLtr, drawableRtl);
3654 }
3655
3656 @Override
3657 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3658 if (isRtlRun) {
3659 return drawable.getIntrinsicWidth() / 4;
3660 } else {
3661 return (drawable.getIntrinsicWidth() * 3) / 4;
3662 }
3663 }
3664
3665 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07003666 protected int getHorizontalGravity(boolean isRtlRun) {
3667 return isRtlRun ? Gravity.RIGHT : Gravity.LEFT;
3668 }
3669
3670 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003671 public int getCurrentCursorOffset() {
3672 return mTextView.getSelectionStart();
3673 }
3674
3675 @Override
3676 public void updateSelection(int offset) {
3677 Selection.setSelection((Spannable) mTextView.getText(), offset,
3678 mTextView.getSelectionEnd());
3679 updateDrawable();
3680 }
3681
3682 @Override
3683 public void updatePosition(float x, float y) {
3684 int offset = mTextView.getOffsetForPosition(x, y);
3685
3686 // Handles can not cross and selection is at least one character
3687 final int selectionEnd = mTextView.getSelectionEnd();
3688 if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
3689
3690 positionAtCursorOffset(offset, false);
3691 }
3692
3693 public ActionPopupWindow getActionPopupWindow() {
3694 return mActionPopupWindow;
3695 }
3696 }
3697
3698 private class SelectionEndHandleView extends HandleView {
3699
3700 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3701 super(drawableLtr, drawableRtl);
3702 }
3703
3704 @Override
3705 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3706 if (isRtlRun) {
3707 return (drawable.getIntrinsicWidth() * 3) / 4;
3708 } else {
3709 return drawable.getIntrinsicWidth() / 4;
3710 }
3711 }
3712
3713 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07003714 protected int getHorizontalGravity(boolean isRtlRun) {
3715 return isRtlRun ? Gravity.LEFT : Gravity.RIGHT;
3716 }
3717
3718 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003719 public int getCurrentCursorOffset() {
3720 return mTextView.getSelectionEnd();
3721 }
3722
3723 @Override
3724 public void updateSelection(int offset) {
3725 Selection.setSelection((Spannable) mTextView.getText(),
3726 mTextView.getSelectionStart(), offset);
3727 updateDrawable();
3728 }
3729
3730 @Override
3731 public void updatePosition(float x, float y) {
3732 int offset = mTextView.getOffsetForPosition(x, y);
3733
3734 // Handles can not cross and selection is at least one character
3735 final int selectionStart = mTextView.getSelectionStart();
3736 if (offset <= selectionStart) {
3737 offset = Math.min(selectionStart + 1, mTextView.getText().length());
3738 }
3739
3740 positionAtCursorOffset(offset, false);
3741 }
3742
3743 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
3744 mActionPopupWindow = actionPopupWindow;
3745 }
3746 }
3747
3748 /**
3749 * A CursorController instance can be used to control a cursor in the text.
3750 */
3751 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
3752 /**
3753 * Makes the cursor controller visible on screen.
3754 * See also {@link #hide()}.
3755 */
3756 public void show();
3757
3758 /**
3759 * Hide the cursor controller from screen.
3760 * See also {@link #show()}.
3761 */
3762 public void hide();
3763
3764 /**
3765 * Called when the view is detached from window. Perform house keeping task, such as
3766 * stopping Runnable thread that would otherwise keep a reference on the context, thus
3767 * preventing the activity from being recycled.
3768 */
3769 public void onDetached();
3770 }
3771
3772 private class InsertionPointCursorController implements CursorController {
3773 private InsertionHandleView mHandle;
3774
3775 public void show() {
3776 getHandle().show();
3777 }
3778
3779 public void showWithActionPopup() {
3780 getHandle().showWithActionPopup();
3781 }
3782
3783 public void hide() {
3784 if (mHandle != null) {
3785 mHandle.hide();
3786 }
3787 }
3788
3789 public void onTouchModeChanged(boolean isInTouchMode) {
3790 if (!isInTouchMode) {
3791 hide();
3792 }
3793 }
3794
3795 private InsertionHandleView getHandle() {
3796 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08003797 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07003798 mTextView.mTextSelectHandleRes);
3799 }
3800 if (mHandle == null) {
3801 mHandle = new InsertionHandleView(mSelectHandleCenter);
3802 }
3803 return mHandle;
3804 }
3805
3806 @Override
3807 public void onDetached() {
3808 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3809 observer.removeOnTouchModeChangeListener(this);
3810
3811 if (mHandle != null) mHandle.onDetached();
3812 }
3813 }
3814
3815 class SelectionModifierCursorController implements CursorController {
3816 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
3817 // The cursor controller handles, lazily created when shown.
3818 private SelectionStartHandleView mStartHandle;
3819 private SelectionEndHandleView mEndHandle;
3820 // The offsets of that last touch down event. Remembered to start selection there.
3821 private int mMinTouchOffset, mMaxTouchOffset;
3822
3823 // Double tap detection
3824 private long mPreviousTapUpTime = 0;
3825 private float mDownPositionX, mDownPositionY;
3826 private boolean mGestureStayedInTapRegion;
3827
3828 SelectionModifierCursorController() {
3829 resetTouchOffsets();
3830 }
3831
3832 public void show() {
3833 if (mTextView.isInBatchEditMode()) {
3834 return;
3835 }
3836 initDrawables();
3837 initHandles();
3838 hideInsertionPointCursorController();
3839 }
3840
3841 private void initDrawables() {
3842 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08003843 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07003844 mTextView.mTextSelectHandleLeftRes);
3845 }
3846 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08003847 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07003848 mTextView.mTextSelectHandleRightRes);
3849 }
3850 }
3851
3852 private void initHandles() {
3853 // Lazy object creation has to be done before updatePosition() is called.
3854 if (mStartHandle == null) {
3855 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
3856 }
3857 if (mEndHandle == null) {
3858 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
3859 }
3860
3861 mStartHandle.show();
3862 mEndHandle.show();
3863
3864 // Make sure both left and right handles share the same ActionPopupWindow (so that
3865 // moving any of the handles hides the action popup).
3866 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
3867 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
3868
3869 hideInsertionPointCursorController();
3870 }
3871
3872 public void hide() {
3873 if (mStartHandle != null) mStartHandle.hide();
3874 if (mEndHandle != null) mEndHandle.hide();
3875 }
3876
3877 public void onTouchEvent(MotionEvent event) {
3878 // This is done even when the View does not have focus, so that long presses can start
3879 // selection and tap can move cursor from this tap position.
3880 switch (event.getActionMasked()) {
3881 case MotionEvent.ACTION_DOWN:
3882 final float x = event.getX();
3883 final float y = event.getY();
3884
3885 // Remember finger down position, to be able to start selection from there
3886 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
3887
3888 // Double tap detection
3889 if (mGestureStayedInTapRegion) {
3890 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
3891 if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
3892 final float deltaX = x - mDownPositionX;
3893 final float deltaY = y - mDownPositionY;
3894 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3895
3896 ViewConfiguration viewConfiguration = ViewConfiguration.get(
3897 mTextView.getContext());
3898 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
3899 boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
3900
3901 if (stayedInArea && isPositionOnText(x, y)) {
3902 startSelectionActionMode();
3903 mDiscardNextActionUp = true;
3904 }
3905 }
3906 }
3907
3908 mDownPositionX = x;
3909 mDownPositionY = y;
3910 mGestureStayedInTapRegion = true;
3911 break;
3912
3913 case MotionEvent.ACTION_POINTER_DOWN:
3914 case MotionEvent.ACTION_POINTER_UP:
3915 // Handle multi-point gestures. Keep min and max offset positions.
3916 // Only activated for devices that correctly handle multi-touch.
3917 if (mTextView.getContext().getPackageManager().hasSystemFeature(
3918 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
3919 updateMinAndMaxOffsets(event);
3920 }
3921 break;
3922
3923 case MotionEvent.ACTION_MOVE:
3924 if (mGestureStayedInTapRegion) {
3925 final float deltaX = event.getX() - mDownPositionX;
3926 final float deltaY = event.getY() - mDownPositionY;
3927 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3928
3929 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3930 mTextView.getContext());
3931 int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
3932
3933 if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
3934 mGestureStayedInTapRegion = false;
3935 }
3936 }
3937 break;
3938
3939 case MotionEvent.ACTION_UP:
3940 mPreviousTapUpTime = SystemClock.uptimeMillis();
3941 break;
3942 }
3943 }
3944
3945 /**
3946 * @param event
3947 */
3948 private void updateMinAndMaxOffsets(MotionEvent event) {
3949 int pointerCount = event.getPointerCount();
3950 for (int index = 0; index < pointerCount; index++) {
3951 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
3952 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
3953 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
3954 }
3955 }
3956
3957 public int getMinTouchOffset() {
3958 return mMinTouchOffset;
3959 }
3960
3961 public int getMaxTouchOffset() {
3962 return mMaxTouchOffset;
3963 }
3964
3965 public void resetTouchOffsets() {
3966 mMinTouchOffset = mMaxTouchOffset = -1;
3967 }
3968
3969 /**
3970 * @return true iff this controller is currently used to move the selection start.
3971 */
3972 public boolean isSelectionStartDragged() {
3973 return mStartHandle != null && mStartHandle.isDragging();
3974 }
3975
3976 public void onTouchModeChanged(boolean isInTouchMode) {
3977 if (!isInTouchMode) {
3978 hide();
3979 }
3980 }
3981
3982 @Override
3983 public void onDetached() {
3984 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3985 observer.removeOnTouchModeChangeListener(this);
3986
3987 if (mStartHandle != null) mStartHandle.onDetached();
3988 if (mEndHandle != null) mEndHandle.onDetached();
3989 }
3990 }
3991
3992 private class CorrectionHighlighter {
3993 private final Path mPath = new Path();
3994 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
3995 private int mStart, mEnd;
3996 private long mFadingStartTime;
3997 private RectF mTempRectF;
3998 private final static int FADE_OUT_DURATION = 400;
3999
4000 public CorrectionHighlighter() {
4001 mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
4002 applicationScale);
4003 mPaint.setStyle(Paint.Style.FILL);
4004 }
4005
4006 public void highlight(CorrectionInfo info) {
4007 mStart = info.getOffset();
4008 mEnd = mStart + info.getNewText().length();
4009 mFadingStartTime = SystemClock.uptimeMillis();
4010
4011 if (mStart < 0 || mEnd < 0) {
4012 stopAnimation();
4013 }
4014 }
4015
4016 public void draw(Canvas canvas, int cursorOffsetVertical) {
4017 if (updatePath() && updatePaint()) {
4018 if (cursorOffsetVertical != 0) {
4019 canvas.translate(0, cursorOffsetVertical);
4020 }
4021
4022 canvas.drawPath(mPath, mPaint);
4023
4024 if (cursorOffsetVertical != 0) {
4025 canvas.translate(0, -cursorOffsetVertical);
4026 }
4027 invalidate(true); // TODO invalidate cursor region only
4028 } else {
4029 stopAnimation();
4030 invalidate(false); // TODO invalidate cursor region only
4031 }
4032 }
4033
4034 private boolean updatePaint() {
4035 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
4036 if (duration > FADE_OUT_DURATION) return false;
4037
4038 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
4039 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
4040 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
4041 ((int) (highlightColorAlpha * coef) << 24);
4042 mPaint.setColor(color);
4043 return true;
4044 }
4045
4046 private boolean updatePath() {
4047 final Layout layout = mTextView.getLayout();
4048 if (layout == null) return false;
4049
4050 // Update in case text is edited while the animation is run
4051 final int length = mTextView.getText().length();
4052 int start = Math.min(length, mStart);
4053 int end = Math.min(length, mEnd);
4054
4055 mPath.reset();
4056 layout.getSelectionPath(start, end, mPath);
4057 return true;
4058 }
4059
4060 private void invalidate(boolean delayed) {
4061 if (mTextView.getLayout() == null) return;
4062
4063 if (mTempRectF == null) mTempRectF = new RectF();
4064 mPath.computeBounds(mTempRectF, false);
4065
4066 int left = mTextView.getCompoundPaddingLeft();
4067 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
4068
4069 if (delayed) {
4070 mTextView.postInvalidateOnAnimation(
4071 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
4072 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
4073 } else {
4074 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
4075 (int) mTempRectF.right, (int) mTempRectF.bottom);
4076 }
4077 }
4078
4079 private void stopAnimation() {
4080 Editor.this.mCorrectionHighlighter = null;
4081 }
4082 }
4083
4084 private static class ErrorPopup extends PopupWindow {
4085 private boolean mAbove = false;
4086 private final TextView mView;
4087 private int mPopupInlineErrorBackgroundId = 0;
4088 private int mPopupInlineErrorAboveBackgroundId = 0;
4089
4090 ErrorPopup(TextView v, int width, int height) {
4091 super(v, width, height);
4092 mView = v;
4093 // 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 -08004094 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07004095 // dimensions identical to the above version for this to work (and is more likely).
4096 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4097 com.android.internal.R.styleable.Theme_errorMessageBackground);
4098 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
4099 }
4100
4101 void fixDirection(boolean above) {
4102 mAbove = above;
4103
4104 if (above) {
4105 mPopupInlineErrorAboveBackgroundId =
4106 getResourceId(mPopupInlineErrorAboveBackgroundId,
4107 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
4108 } else {
4109 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4110 com.android.internal.R.styleable.Theme_errorMessageBackground);
4111 }
4112
4113 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
4114 mPopupInlineErrorBackgroundId);
4115 }
4116
4117 private int getResourceId(int currentId, int index) {
4118 if (currentId == 0) {
4119 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
4120 R.styleable.Theme);
4121 currentId = styledAttributes.getResourceId(index, 0);
4122 styledAttributes.recycle();
4123 }
4124 return currentId;
4125 }
4126
4127 @Override
4128 public void update(int x, int y, int w, int h, boolean force) {
4129 super.update(x, y, w, h, force);
4130
4131 boolean above = isAboveAnchor();
4132 if (above != mAbove) {
4133 fixDirection(above);
4134 }
4135 }
4136 }
4137
4138 static class InputContentType {
4139 int imeOptions = EditorInfo.IME_NULL;
4140 String privateImeOptions;
4141 CharSequence imeActionLabel;
4142 int imeActionId;
4143 Bundle extras;
4144 OnEditorActionListener onEditorActionListener;
4145 boolean enterDown;
4146 }
4147
4148 static class InputMethodState {
4149 Rect mCursorRectInWindow = new Rect();
Gilles Debunned88876a2012-03-16 17:34:04 -07004150 float[] mTmpOffset = new float[2];
Gilles Debunnec62589c2012-04-12 14:50:23 -07004151 ExtractedTextRequest mExtractedTextRequest;
4152 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07004153 int mBatchEditNesting;
4154 boolean mCursorChanged;
4155 boolean mSelectionModeChanged;
4156 boolean mContentChanged;
4157 int mChangedStart, mChangedEnd, mChangedDelta;
4158 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09004159
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004160 public static class UndoInputFilter implements InputFilter {
4161 final Editor mEditor;
4162
4163 public UndoInputFilter(Editor editor) {
4164 mEditor = editor;
4165 }
4166
4167 @Override
4168 public CharSequence filter(CharSequence source, int start, int end,
4169 Spanned dest, int dstart, int dend) {
4170 if (DEBUG_UNDO) {
4171 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ")");
4172 Log.d(TAG, "filter: dest=" + dest + " (" + dstart + "-" + dend + ")");
4173 }
4174 final UndoManager um = mEditor.mUndoManager;
4175 if (um.isInUndo()) {
4176 if (DEBUG_UNDO) Log.d(TAG, "*** skipping, currently performing undo/redo");
4177 return null;
4178 }
4179
4180 um.beginUpdate("Edit text");
4181 TextModifyOperation op = um.getLastOperation(
4182 TextModifyOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
4183 if (op != null) {
4184 if (DEBUG_UNDO) Log.d(TAG, "Last op: range=(" + op.mRangeStart + "-" + op.mRangeEnd
4185 + "), oldText=" + op.mOldText);
4186 // See if we can continue modifying this operation.
4187 if (op.mOldText == null) {
4188 // The current operation is an add... are we adding more? We are adding
4189 // more if we are either appending new text to the end of the last edit or
4190 // completely replacing some or all of the last edit.
4191 if (start < end && ((dstart >= op.mRangeStart && dend <= op.mRangeEnd)
4192 || (dstart == op.mRangeEnd && dend == op.mRangeEnd))) {
4193 op.mRangeEnd = dstart + (end-start);
4194 um.endUpdate();
4195 if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, mRangeEnd="
4196 + op.mRangeEnd);
4197 return null;
4198 }
4199 } else {
4200 // The current operation is a delete... can we delete more?
4201 if (start == end && dend == op.mRangeStart-1) {
4202 SpannableStringBuilder str;
4203 if (op.mOldText instanceof SpannableString) {
4204 str = (SpannableStringBuilder)op.mOldText;
4205 } else {
4206 str = new SpannableStringBuilder(op.mOldText);
4207 }
4208 str.insert(0, dest, dstart, dend);
4209 op.mRangeStart = dstart;
4210 op.mOldText = str;
4211 um.endUpdate();
4212 if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, range=("
4213 + op.mRangeStart + "-" + op.mRangeEnd
4214 + "), oldText=" + op.mOldText);
4215 return null;
4216 }
4217 }
4218
4219 // Couldn't add to the current undo operation, need to start a new
4220 // undo state for a new undo operation.
4221 um.commitState(null);
4222 um.setUndoLabel("Edit text");
4223 }
4224
4225 // Create a new undo state reflecting the operation being performed.
4226 op = new TextModifyOperation(mEditor.mUndoOwner);
4227 op.mRangeStart = dstart;
4228 if (start < end) {
4229 op.mRangeEnd = dstart + (end-start);
4230 } else {
4231 op.mRangeEnd = dstart;
4232 }
4233 if (dstart < dend) {
4234 op.mOldText = dest.subSequence(dstart, dend);
4235 }
4236 if (DEBUG_UNDO) Log.d(TAG, "*** adding new op, range=(" + op.mRangeStart
4237 + "-" + op.mRangeEnd + "), oldText=" + op.mOldText);
4238 um.addOperation(op, UndoManager.MERGE_MODE_NONE);
4239 um.endUpdate();
4240 return null;
4241 }
4242 }
4243
4244 public static class TextModifyOperation extends UndoOperation<TextView> {
4245 int mRangeStart, mRangeEnd;
4246 CharSequence mOldText;
4247
4248 public TextModifyOperation(UndoOwner owner) {
4249 super(owner);
4250 }
4251
4252 public TextModifyOperation(Parcel src, ClassLoader loader) {
4253 super(src, loader);
4254 mRangeStart = src.readInt();
4255 mRangeEnd = src.readInt();
4256 mOldText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src);
4257 }
4258
4259 @Override
4260 public void commit() {
4261 }
4262
4263 @Override
4264 public void undo() {
4265 swapText();
4266 }
4267
4268 @Override
4269 public void redo() {
4270 swapText();
4271 }
4272
4273 private void swapText() {
4274 // Both undo and redo involves swapping the contents of the range
4275 // in the text view with our local text.
4276 TextView tv = getOwnerData();
4277 Editable editable = (Editable)tv.getText();
4278 CharSequence curText;
4279 if (mRangeStart >= mRangeEnd) {
4280 curText = null;
4281 } else {
4282 curText = editable.subSequence(mRangeStart, mRangeEnd);
4283 }
4284 if (DEBUG_UNDO) {
4285 Log.d(TAG, "Swap: range=(" + mRangeStart + "-" + mRangeEnd
4286 + "), oldText=" + mOldText);
4287 Log.d(TAG, "Swap: curText=" + curText);
4288 }
4289 if (mOldText == null) {
4290 editable.delete(mRangeStart, mRangeEnd);
4291 mRangeEnd = mRangeStart;
4292 } else {
4293 editable.replace(mRangeStart, mRangeEnd, mOldText);
4294 mRangeEnd = mRangeStart + mOldText.length();
4295 }
4296 mOldText = curText;
4297 }
4298
4299 @Override
4300 public void writeToParcel(Parcel dest, int flags) {
4301 dest.writeInt(mRangeStart);
4302 dest.writeInt(mRangeEnd);
4303 TextUtils.writeToParcel(mOldText, dest, flags);
4304 }
4305
4306 public static final Parcelable.ClassLoaderCreator<TextModifyOperation> CREATOR
4307 = new Parcelable.ClassLoaderCreator<TextModifyOperation>() {
4308 public TextModifyOperation createFromParcel(Parcel in) {
4309 return new TextModifyOperation(in, null);
4310 }
4311
4312 public TextModifyOperation createFromParcel(Parcel in, ClassLoader loader) {
4313 return new TextModifyOperation(in, loader);
4314 }
4315
4316 public TextModifyOperation[] newArray(int size) {
4317 return new TextModifyOperation[size];
4318 }
4319 };
4320 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004321}