blob: 880dc345dcdc2f48e69f66bc0f4d4791fb62a138 [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
19import android.R;
20import android.content.ClipData;
21import android.content.ClipData.Item;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.res.TypedArray;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Paint;
29import android.graphics.Path;
30import android.graphics.Rect;
31import android.graphics.RectF;
32import android.graphics.drawable.Drawable;
33import android.inputmethodservice.ExtractEditText;
34import android.os.Bundle;
35import android.os.Handler;
36import android.os.SystemClock;
37import android.provider.Settings;
38import android.text.DynamicLayout;
39import android.text.Editable;
40import android.text.InputType;
41import android.text.Layout;
42import android.text.ParcelableSpan;
43import android.text.Selection;
44import android.text.SpanWatcher;
45import android.text.Spannable;
46import android.text.SpannableStringBuilder;
47import android.text.Spanned;
48import android.text.StaticLayout;
49import android.text.TextUtils;
50import android.text.TextWatcher;
51import android.text.method.KeyListener;
52import android.text.method.MetaKeyKeyListener;
53import android.text.method.MovementMethod;
54import android.text.method.PasswordTransformationMethod;
55import android.text.method.WordIterator;
56import android.text.style.EasyEditSpan;
57import android.text.style.SuggestionRangeSpan;
58import android.text.style.SuggestionSpan;
59import android.text.style.TextAppearanceSpan;
60import android.text.style.URLSpan;
61import android.util.DisplayMetrics;
62import android.util.Log;
63import android.view.ActionMode;
64import android.view.ActionMode.Callback;
65import android.view.DisplayList;
66import android.view.DragEvent;
67import android.view.Gravity;
68import android.view.HardwareCanvas;
69import android.view.LayoutInflater;
70import android.view.Menu;
71import android.view.MenuItem;
72import android.view.MotionEvent;
73import android.view.View;
74import android.view.ViewConfiguration;
75import android.view.ViewGroup;
76import android.view.View.DragShadowBuilder;
77import android.view.View.OnClickListener;
78import android.view.ViewGroup.LayoutParams;
79import android.view.ViewParent;
80import android.view.ViewTreeObserver;
81import android.view.WindowManager;
82import android.view.inputmethod.CorrectionInfo;
83import android.view.inputmethod.EditorInfo;
84import android.view.inputmethod.ExtractedText;
85import android.view.inputmethod.ExtractedTextRequest;
86import android.view.inputmethod.InputConnection;
87import android.view.inputmethod.InputMethodManager;
88import android.widget.AdapterView.OnItemClickListener;
89import android.widget.TextView.Drawables;
90import android.widget.TextView.OnEditorActionListener;
91
92import com.android.internal.util.ArrayUtils;
93import com.android.internal.widget.EditableInputConnection;
94
95import java.text.BreakIterator;
96import java.util.Arrays;
97import java.util.Comparator;
98import java.util.HashMap;
99
100/**
101 * Helper class used by TextView to handle editable text views.
102 *
103 * @hide
104 */
105public class Editor {
106 static final int BLINK = 500;
107 private static final float[] TEMP_POSITION = new float[2];
108 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
109
110 // Cursor Controllers.
111 InsertionPointCursorController mInsertionPointCursorController;
112 SelectionModifierCursorController mSelectionModifierCursorController;
113 ActionMode mSelectionActionMode;
114 boolean mInsertionControllerEnabled;
115 boolean mSelectionControllerEnabled;
116
117 // Used to highlight a word when it is corrected by the IME
118 CorrectionHighlighter mCorrectionHighlighter;
119
120 InputContentType mInputContentType;
121 InputMethodState mInputMethodState;
122
123 DisplayList[] mTextDisplayLists;
124
125 boolean mFrozenWithFocus;
126 boolean mSelectionMoved;
127 boolean mTouchFocusSelected;
128
129 KeyListener mKeyListener;
130 int mInputType = EditorInfo.TYPE_NULL;
131
132 boolean mDiscardNextActionUp;
133 boolean mIgnoreActionUpEvent;
134
135 long mShowCursor;
136 Blink mBlink;
137
138 boolean mCursorVisible = true;
139 boolean mSelectAllOnFocus;
140 boolean mTextIsSelectable;
141
142 CharSequence mError;
143 boolean mErrorWasChanged;
144 ErrorPopup mErrorPopup;
145 /**
146 * This flag is set if the TextView tries to display an error before it
147 * is attached to the window (so its position is still unknown).
148 * It causes the error to be shown later, when onAttachedToWindow()
149 * is called.
150 */
151 boolean mShowErrorAfterAttach;
152
153 boolean mInBatchEditControllers;
154
155 SuggestionsPopupWindow mSuggestionsPopupWindow;
156 SuggestionRangeSpan mSuggestionRangeSpan;
157 Runnable mShowSuggestionRunnable;
158
159 final Drawable[] mCursorDrawable = new Drawable[2];
160 int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
161
162 private Drawable mSelectHandleLeft;
163 private Drawable mSelectHandleRight;
164 private Drawable mSelectHandleCenter;
165
166 // Global listener that detects changes in the global position of the TextView
167 private PositionListener mPositionListener;
168
169 float mLastDownPositionX, mLastDownPositionY;
170 Callback mCustomSelectionActionModeCallback;
171
172 // Set when this TextView gained focus with some text selected. Will start selection mode.
173 boolean mCreatedWithASelection;
174
175 private EasyEditSpanController mEasyEditSpanController;
176
177 WordIterator mWordIterator;
178 SpellChecker mSpellChecker;
179
180 private Rect mTempRect;
181
182 private TextView mTextView;
183
184 Editor(TextView textView) {
185 mTextView = textView;
186 mEasyEditSpanController = new EasyEditSpanController();
187 mTextView.addTextChangedListener(mEasyEditSpanController);
188 }
189
190 void onAttachedToWindow() {
191 if (mShowErrorAfterAttach) {
192 showError();
193 mShowErrorAfterAttach = false;
194 }
195
196 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
197 // No need to create the controller.
198 // The get method will add the listener on controller creation.
199 if (mInsertionPointCursorController != null) {
200 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
201 }
202 if (mSelectionModifierCursorController != null) {
203 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
204 }
205 updateSpellCheckSpans(0, mTextView.getText().length(),
206 true /* create the spell checker if needed */);
207 }
208
209 void onDetachedFromWindow() {
210 if (mError != null) {
211 hideError();
212 }
213
214 if (mBlink != null) {
215 mBlink.removeCallbacks(mBlink);
216 }
217
218 if (mInsertionPointCursorController != null) {
219 mInsertionPointCursorController.onDetached();
220 }
221
222 if (mSelectionModifierCursorController != null) {
223 mSelectionModifierCursorController.onDetached();
224 }
225
226 if (mShowSuggestionRunnable != null) {
227 mTextView.removeCallbacks(mShowSuggestionRunnable);
228 }
229
230 invalidateTextDisplayList();
231
232 if (mSpellChecker != null) {
233 mSpellChecker.closeSession();
234 // Forces the creation of a new SpellChecker next time this window is created.
235 // Will handle the cases where the settings has been changed in the meantime.
236 mSpellChecker = null;
237 }
238
239 hideControllers();
240 }
241
242 private void showError() {
243 if (mTextView.getWindowToken() == null) {
244 mShowErrorAfterAttach = true;
245 return;
246 }
247
248 if (mErrorPopup == null) {
249 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
250 final TextView err = (TextView) inflater.inflate(
251 com.android.internal.R.layout.textview_hint, null);
252
253 final float scale = mTextView.getResources().getDisplayMetrics().density;
254 mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
255 mErrorPopup.setFocusable(false);
256 // The user is entering text, so the input method is needed. We
257 // don't want the popup to be displayed on top of it.
258 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
259 }
260
261 TextView tv = (TextView) mErrorPopup.getContentView();
262 chooseSize(mErrorPopup, mError, tv);
263 tv.setText(mError);
264
265 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
266 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
267 }
268
269 public void setError(CharSequence error, Drawable icon) {
270 mError = TextUtils.stringOrSpannedString(error);
271 mErrorWasChanged = true;
272 final Drawables dr = mTextView.mDrawables;
273 if (dr != null) {
274 switch (mTextView.getResolvedLayoutDirection()) {
275 default:
276 case View.LAYOUT_DIRECTION_LTR:
277 mTextView.setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
278 dr.mDrawableBottom);
279 break;
280 case View.LAYOUT_DIRECTION_RTL:
281 mTextView.setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
282 dr.mDrawableBottom);
283 break;
284 }
285 } else {
286 mTextView.setCompoundDrawables(null, null, icon, null);
287 }
288
289 if (mError == null) {
290 if (mErrorPopup != null) {
291 if (mErrorPopup.isShowing()) {
292 mErrorPopup.dismiss();
293 }
294
295 mErrorPopup = null;
296 }
297 } else {
298 if (mTextView.isFocused()) {
299 showError();
300 }
301 }
302 }
303
304 private void hideError() {
305 if (mErrorPopup != null) {
306 if (mErrorPopup.isShowing()) {
307 mErrorPopup.dismiss();
308 }
309 }
310
311 mShowErrorAfterAttach = false;
312 }
313
314 /**
315 * Returns the Y offset to make the pointy top of the error point
316 * at the middle of the error icon.
317 */
318 private int getErrorX() {
319 /*
320 * The "25" is the distance between the point and the right edge
321 * of the background
322 */
323 final float scale = mTextView.getResources().getDisplayMetrics().density;
324
325 final Drawables dr = mTextView.mDrawables;
326 return mTextView.getWidth() - mErrorPopup.getWidth() - mTextView.getPaddingRight() -
327 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
328 }
329
330 /**
331 * Returns the Y offset to make the pointy top of the error point
332 * at the bottom of the error icon.
333 */
334 private int getErrorY() {
335 /*
336 * Compound, not extended, because the icon is not clipped
337 * if the text height is smaller.
338 */
339 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
340 int vspace = mTextView.getBottom() - mTextView.getTop() -
341 mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
342
343 final Drawables dr = mTextView.mDrawables;
344 int icontop = compoundPaddingTop +
345 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
346
347 /*
348 * The "2" is the distance between the point and the top edge
349 * of the background.
350 */
351 final float scale = mTextView.getResources().getDisplayMetrics().density;
352 return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - mTextView.getHeight() -
353 (int) (2 * scale + 0.5f);
354 }
355
356 void createInputContentTypeIfNeeded() {
357 if (mInputContentType == null) {
358 mInputContentType = new InputContentType();
359 }
360 }
361
362 void createInputMethodStateIfNeeded() {
363 if (mInputMethodState == null) {
364 mInputMethodState = new InputMethodState();
365 }
366 }
367
368 boolean isCursorVisible() {
369 // The default value is true, even when there is no associated Editor
370 return mCursorVisible && mTextView.isTextEditable();
371 }
372
373 void prepareCursorControllers() {
374 boolean windowSupportsHandles = false;
375
376 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
377 if (params instanceof WindowManager.LayoutParams) {
378 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
379 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
380 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
381 }
382
383 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
384 mInsertionControllerEnabled = enabled && isCursorVisible();
385 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
386
387 if (!mInsertionControllerEnabled) {
388 hideInsertionPointCursorController();
389 if (mInsertionPointCursorController != null) {
390 mInsertionPointCursorController.onDetached();
391 mInsertionPointCursorController = null;
392 }
393 }
394
395 if (!mSelectionControllerEnabled) {
396 stopSelectionActionMode();
397 if (mSelectionModifierCursorController != null) {
398 mSelectionModifierCursorController.onDetached();
399 mSelectionModifierCursorController = null;
400 }
401 }
402 }
403
404 private void hideInsertionPointCursorController() {
405 if (mInsertionPointCursorController != null) {
406 mInsertionPointCursorController.hide();
407 }
408 }
409
410 /**
411 * Hides the insertion controller and stops text selection mode, hiding the selection controller
412 */
413 void hideControllers() {
414 hideCursorControllers();
415 hideSpanControllers();
416 }
417
418 private void hideSpanControllers() {
419 if (mEasyEditSpanController != null) {
420 mEasyEditSpanController.hide();
421 }
422 }
423
424 private void hideCursorControllers() {
425 if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
426 // Should be done before hide insertion point controller since it triggers a show of it
427 mSuggestionsPopupWindow.hide();
428 }
429 hideInsertionPointCursorController();
430 stopSelectionActionMode();
431 }
432
433 /**
434 * Create new SpellCheckSpans on the modified region.
435 */
436 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
437 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
438 !(mTextView instanceof ExtractEditText)) {
439 if (mSpellChecker == null && createSpellChecker) {
440 mSpellChecker = new SpellChecker(mTextView);
441 }
442 if (mSpellChecker != null) {
443 mSpellChecker.spellCheck(start, end);
444 }
445 }
446 }
447
448 void onScreenStateChanged(int screenState) {
449 switch (screenState) {
450 case View.SCREEN_STATE_ON:
451 resumeBlink();
452 break;
453 case View.SCREEN_STATE_OFF:
454 suspendBlink();
455 break;
456 }
457 }
458
459 private void suspendBlink() {
460 if (mBlink != null) {
461 mBlink.cancel();
462 }
463 }
464
465 private void resumeBlink() {
466 if (mBlink != null) {
467 mBlink.uncancel();
468 makeBlink();
469 }
470 }
471
472 void adjustInputType(boolean password, boolean passwordInputType,
473 boolean webPasswordInputType, boolean numberPasswordInputType) {
474 // mInputType has been set from inputType, possibly modified by mInputMethod.
475 // Specialize mInputType to [web]password if we have a text class and the original input
476 // type was a password.
477 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
478 if (password || passwordInputType) {
479 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
480 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
481 }
482 if (webPasswordInputType) {
483 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
484 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
485 }
486 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
487 if (numberPasswordInputType) {
488 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
489 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
490 }
491 }
492 }
493
494 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
495 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
496 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
497
498 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
499 com.android.internal.R.dimen.textview_error_popup_default_width);
500 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
501 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
502 float max = 0;
503 for (int i = 0; i < l.getLineCount(); i++) {
504 max = Math.max(max, l.getLineWidth(i));
505 }
506
507 /*
508 * Now set the popup size to be big enough for the text plus the border capped
509 * to DEFAULT_MAX_POPUP_WIDTH
510 */
511 pop.setWidth(wid + (int) Math.ceil(max));
512 pop.setHeight(ht + l.getHeight());
513 }
514
515 void setFrame() {
516 if (mErrorPopup != null) {
517 TextView tv = (TextView) mErrorPopup.getContentView();
518 chooseSize(mErrorPopup, mError, tv);
519 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
520 mErrorPopup.getWidth(), mErrorPopup.getHeight());
521 }
522 }
523
524 /**
525 * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state
526 * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have
527 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
528 */
529 private boolean canSelectText() {
530 return hasSelectionController() && mTextView.getText().length() != 0;
531 }
532
533 /**
534 * It would be better to rely on the input type for everything. A password inputType should have
535 * a password transformation. We should hence use isPasswordInputType instead of this method.
536 *
537 * We should:
538 * - Call setInputType in setKeyListener instead of changing the input type directly (which
539 * would install the correct transformation).
540 * - Refuse the installation of a non-password transformation in setTransformation if the input
541 * type is password.
542 *
543 * However, this is like this for legacy reasons and we cannot break existing apps. This method
544 * is useful since it matches what the user can see (obfuscated text or not).
545 *
546 * @return true if the current transformation method is of the password type.
547 */
548 private boolean hasPasswordTransformationMethod() {
549 return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
550 }
551
552 /**
553 * Adjusts selection to the word under last touch offset.
554 * Return true if the operation was successfully performed.
555 */
556 private boolean selectCurrentWord() {
557 if (!canSelectText()) {
558 return false;
559 }
560
561 if (hasPasswordTransformationMethod()) {
562 // Always select all on a password field.
563 // Cut/copy menu entries are not available for passwords, but being able to select all
564 // is however useful to delete or paste to replace the entire content.
565 return mTextView.selectAllText();
566 }
567
568 int inputType = mTextView.getInputType();
569 int klass = inputType & InputType.TYPE_MASK_CLASS;
570 int variation = inputType & InputType.TYPE_MASK_VARIATION;
571
572 // Specific text field types: select the entire text for these
573 if (klass == InputType.TYPE_CLASS_NUMBER ||
574 klass == InputType.TYPE_CLASS_PHONE ||
575 klass == InputType.TYPE_CLASS_DATETIME ||
576 variation == InputType.TYPE_TEXT_VARIATION_URI ||
577 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
578 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
579 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
580 return mTextView.selectAllText();
581 }
582
583 long lastTouchOffsets = getLastTouchOffsets();
584 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
585 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
586
587 // Safety check in case standard touch event handling has been bypassed
588 if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
589 if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
590
591 int selectionStart, selectionEnd;
592
593 // If a URLSpan (web address, email, phone...) is found at that position, select it.
594 URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
595 getSpans(minOffset, maxOffset, URLSpan.class);
596 if (urlSpans.length >= 1) {
597 URLSpan urlSpan = urlSpans[0];
598 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
599 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
600 } else {
601 final WordIterator wordIterator = getWordIterator();
602 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
603
604 selectionStart = wordIterator.getBeginning(minOffset);
605 selectionEnd = wordIterator.getEnd(maxOffset);
606
607 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
608 selectionStart == selectionEnd) {
609 // Possible when the word iterator does not properly handle the text's language
610 long range = getCharRange(minOffset);
611 selectionStart = TextUtils.unpackRangeStartFromLong(range);
612 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
613 }
614 }
615
616 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
617 return selectionEnd > selectionStart;
618 }
619
620 void onLocaleChanged() {
621 // Will be re-created on demand in getWordIterator with the proper new locale
622 mWordIterator = null;
623 }
624
625 /**
626 * @hide
627 */
628 public WordIterator getWordIterator() {
629 if (mWordIterator == null) {
630 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
631 }
632 return mWordIterator;
633 }
634
635 private long getCharRange(int offset) {
636 final int textLength = mTextView.getText().length();
637 if (offset + 1 < textLength) {
638 final char currentChar = mTextView.getText().charAt(offset);
639 final char nextChar = mTextView.getText().charAt(offset + 1);
640 if (Character.isSurrogatePair(currentChar, nextChar)) {
641 return TextUtils.packRangeInLong(offset, offset + 2);
642 }
643 }
644 if (offset < textLength) {
645 return TextUtils.packRangeInLong(offset, offset + 1);
646 }
647 if (offset - 2 >= 0) {
648 final char previousChar = mTextView.getText().charAt(offset - 1);
649 final char previousPreviousChar = mTextView.getText().charAt(offset - 2);
650 if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
651 return TextUtils.packRangeInLong(offset - 2, offset);
652 }
653 }
654 if (offset - 1 >= 0) {
655 return TextUtils.packRangeInLong(offset - 1, offset);
656 }
657 return TextUtils.packRangeInLong(offset, offset);
658 }
659
660 private boolean touchPositionIsInSelection() {
661 int selectionStart = mTextView.getSelectionStart();
662 int selectionEnd = mTextView.getSelectionEnd();
663
664 if (selectionStart == selectionEnd) {
665 return false;
666 }
667
668 if (selectionStart > selectionEnd) {
669 int tmp = selectionStart;
670 selectionStart = selectionEnd;
671 selectionEnd = tmp;
672 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
673 }
674
675 SelectionModifierCursorController selectionController = getSelectionController();
676 int minOffset = selectionController.getMinTouchOffset();
677 int maxOffset = selectionController.getMaxTouchOffset();
678
679 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
680 }
681
682 private PositionListener getPositionListener() {
683 if (mPositionListener == null) {
684 mPositionListener = new PositionListener();
685 }
686 return mPositionListener;
687 }
688
689 private interface TextViewPositionListener {
690 public void updatePosition(int parentPositionX, int parentPositionY,
691 boolean parentPositionChanged, boolean parentScrolled);
692 }
693
694 private boolean isPositionVisible(int positionX, int positionY) {
695 synchronized (TEMP_POSITION) {
696 final float[] position = TEMP_POSITION;
697 position[0] = positionX;
698 position[1] = positionY;
699 View view = mTextView;
700
701 while (view != null) {
702 if (view != mTextView) {
703 // Local scroll is already taken into account in positionX/Y
704 position[0] -= view.getScrollX();
705 position[1] -= view.getScrollY();
706 }
707
708 if (position[0] < 0 || position[1] < 0 ||
709 position[0] > view.getWidth() || position[1] > view.getHeight()) {
710 return false;
711 }
712
713 if (!view.getMatrix().isIdentity()) {
714 view.getMatrix().mapPoints(position);
715 }
716
717 position[0] += view.getLeft();
718 position[1] += view.getTop();
719
720 final ViewParent parent = view.getParent();
721 if (parent instanceof View) {
722 view = (View) parent;
723 } else {
724 // We've reached the ViewRoot, stop iterating
725 view = null;
726 }
727 }
728 }
729
730 // We've been able to walk up the view hierarchy and the position was never clipped
731 return true;
732 }
733
734 private boolean isOffsetVisible(int offset) {
735 Layout layout = mTextView.getLayout();
736 final int line = layout.getLineForOffset(offset);
737 final int lineBottom = layout.getLineBottom(line);
738 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
739 return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
740 lineBottom + mTextView.viewportToContentVerticalOffset());
741 }
742
743 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
744 * in the view. Returns false when the position is in the empty space of left/right of text.
745 */
746 private boolean isPositionOnText(float x, float y) {
747 Layout layout = mTextView.getLayout();
748 if (layout == null) return false;
749
750 final int line = mTextView.getLineAtCoordinate(y);
751 x = mTextView.convertToLocalHorizontalCoordinate(x);
752
753 if (x < layout.getLineLeft(line)) return false;
754 if (x > layout.getLineRight(line)) return false;
755 return true;
756 }
757
758 public boolean performLongClick(boolean handled) {
759 // Long press in empty space moves cursor and shows the Paste affordance if available.
760 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
761 mInsertionControllerEnabled) {
762 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
763 mLastDownPositionY);
764 stopSelectionActionMode();
765 Selection.setSelection((Spannable) mTextView.getText(), offset);
766 getInsertionController().showWithActionPopup();
767 handled = true;
768 }
769
770 if (!handled && mSelectionActionMode != null) {
771 if (touchPositionIsInSelection()) {
772 // Start a drag
773 final int start = mTextView.getSelectionStart();
774 final int end = mTextView.getSelectionEnd();
775 CharSequence selectedText = mTextView.getTransformedText(start, end);
776 ClipData data = ClipData.newPlainText(null, selectedText);
777 DragLocalState localState = new DragLocalState(mTextView, start, end);
778 mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
779 stopSelectionActionMode();
780 } else {
781 getSelectionController().hide();
782 selectCurrentWord();
783 getSelectionController().show();
784 }
785 handled = true;
786 }
787
788 // Start a new selection
789 if (!handled) {
790 handled = startSelectionActionMode();
791 }
792
793 return handled;
794 }
795
796 private long getLastTouchOffsets() {
797 SelectionModifierCursorController selectionController = getSelectionController();
798 final int minOffset = selectionController.getMinTouchOffset();
799 final int maxOffset = selectionController.getMaxTouchOffset();
800 return TextUtils.packRangeInLong(minOffset, maxOffset);
801 }
802
803 void onFocusChanged(boolean focused, int direction) {
804 mShowCursor = SystemClock.uptimeMillis();
805 ensureEndedBatchEdit();
806
807 if (focused) {
808 int selStart = mTextView.getSelectionStart();
809 int selEnd = mTextView.getSelectionEnd();
810
811 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
812 // mode for these, unless there was a specific selection already started.
813 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
814 selEnd == mTextView.getText().length();
815
816 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
817 !isFocusHighlighted;
818
819 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
820 // If a tap was used to give focus to that view, move cursor at tap position.
821 // Has to be done before onTakeFocus, which can be overloaded.
822 final int lastTapPosition = getLastTapPosition();
823 if (lastTapPosition >= 0) {
824 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
825 }
826
827 // Note this may have to be moved out of the Editor class
828 MovementMethod mMovement = mTextView.getMovementMethod();
829 if (mMovement != null) {
830 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
831 }
832
833 // The DecorView does not have focus when the 'Done' ExtractEditText button is
834 // pressed. Since it is the ViewAncestor's mView, it requests focus before
835 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
836 // This special case ensure that we keep current selection in that case.
837 // It would be better to know why the DecorView does not have focus at that time.
838 if (((mTextView instanceof ExtractEditText) || mSelectionMoved) &&
839 selStart >= 0 && selEnd >= 0) {
840 /*
841 * Someone intentionally set the selection, so let them
842 * do whatever it is that they wanted to do instead of
843 * the default on-focus behavior. We reset the selection
844 * here instead of just skipping the onTakeFocus() call
845 * because some movement methods do something other than
846 * just setting the selection in theirs and we still
847 * need to go through that path.
848 */
849 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
850 }
851
852 if (mSelectAllOnFocus) {
853 mTextView.selectAllText();
854 }
855
856 mTouchFocusSelected = true;
857 }
858
859 mFrozenWithFocus = false;
860 mSelectionMoved = false;
861
862 if (mError != null) {
863 showError();
864 }
865
866 makeBlink();
867 } else {
868 if (mError != null) {
869 hideError();
870 }
871 // Don't leave us in the middle of a batch edit.
872 mTextView.onEndBatchEdit();
873
874 if (mTextView instanceof ExtractEditText) {
875 // terminateTextSelectionMode removes selection, which we want to keep when
876 // ExtractEditText goes out of focus.
877 final int selStart = mTextView.getSelectionStart();
878 final int selEnd = mTextView.getSelectionEnd();
879 hideControllers();
880 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
881 } else {
882 hideControllers();
883 downgradeEasyCorrectionSpans();
884 }
885
886 // No need to create the controller
887 if (mSelectionModifierCursorController != null) {
888 mSelectionModifierCursorController.resetTouchOffsets();
889 }
890 }
891 }
892
893 /**
894 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
895 * span.
896 */
897 private void downgradeEasyCorrectionSpans() {
898 CharSequence text = mTextView.getText();
899 if (text instanceof Spannable) {
900 Spannable spannable = (Spannable) text;
901 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
902 spannable.length(), SuggestionSpan.class);
903 for (int i = 0; i < suggestionSpans.length; i++) {
904 int flags = suggestionSpans[i].getFlags();
905 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
906 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
907 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
908 suggestionSpans[i].setFlags(flags);
909 }
910 }
911 }
912 }
913
914 void sendOnTextChanged(int start, int after) {
915 updateSpellCheckSpans(start, start + after, false);
916
917 // Hide the controllers as soon as text is modified (typing, procedural...)
918 // We do not hide the span controllers, since they can be added when a new text is
919 // inserted into the text view (voice IME).
920 hideCursorControllers();
921 }
922
923 private int getLastTapPosition() {
924 // No need to create the controller at that point, no last tap position saved
925 if (mSelectionModifierCursorController != null) {
926 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
927 if (lastTapPosition >= 0) {
928 // Safety check, should not be possible.
929 if (lastTapPosition > mTextView.getText().length()) {
930 lastTapPosition = mTextView.getText().length();
931 }
932 return lastTapPosition;
933 }
934 }
935
936 return -1;
937 }
938
939 void onWindowFocusChanged(boolean hasWindowFocus) {
940 if (hasWindowFocus) {
941 if (mBlink != null) {
942 mBlink.uncancel();
943 makeBlink();
944 }
945 } else {
946 if (mBlink != null) {
947 mBlink.cancel();
948 }
949 if (mInputContentType != null) {
950 mInputContentType.enterDown = false;
951 }
952 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
953 hideControllers();
954 if (mSuggestionsPopupWindow != null) {
955 mSuggestionsPopupWindow.onParentLostFocus();
956 }
957
958 // Don't leave us in the middle of a batch edit.
959 mTextView.onEndBatchEdit();
960 }
961 }
962
963 void onTouchEvent(MotionEvent event) {
964 if (hasSelectionController()) {
965 getSelectionController().onTouchEvent(event);
966 }
967
968 if (mShowSuggestionRunnable != null) {
969 mTextView.removeCallbacks(mShowSuggestionRunnable);
970 mShowSuggestionRunnable = null;
971 }
972
973 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
974 mLastDownPositionX = event.getX();
975 mLastDownPositionY = event.getY();
976
977 // Reset this state; it will be re-set if super.onTouchEvent
978 // causes focus to move to the view.
979 mTouchFocusSelected = false;
980 mIgnoreActionUpEvent = false;
981 }
982 }
983
984 public void beginBatchEdit() {
985 mInBatchEditControllers = true;
986 final InputMethodState ims = mInputMethodState;
987 if (ims != null) {
988 int nesting = ++ims.mBatchEditNesting;
989 if (nesting == 1) {
990 ims.mCursorChanged = false;
991 ims.mChangedDelta = 0;
992 if (ims.mContentChanged) {
993 // We already have a pending change from somewhere else,
994 // so turn this into a full update.
995 ims.mChangedStart = 0;
996 ims.mChangedEnd = mTextView.getText().length();
997 } else {
998 ims.mChangedStart = EXTRACT_UNKNOWN;
999 ims.mChangedEnd = EXTRACT_UNKNOWN;
1000 ims.mContentChanged = false;
1001 }
1002 mTextView.onBeginBatchEdit();
1003 }
1004 }
1005 }
1006
1007 public void endBatchEdit() {
1008 mInBatchEditControllers = false;
1009 final InputMethodState ims = mInputMethodState;
1010 if (ims != null) {
1011 int nesting = --ims.mBatchEditNesting;
1012 if (nesting == 0) {
1013 finishBatchEdit(ims);
1014 }
1015 }
1016 }
1017
1018 void ensureEndedBatchEdit() {
1019 final InputMethodState ims = mInputMethodState;
1020 if (ims != null && ims.mBatchEditNesting != 0) {
1021 ims.mBatchEditNesting = 0;
1022 finishBatchEdit(ims);
1023 }
1024 }
1025
1026 void finishBatchEdit(final InputMethodState ims) {
1027 mTextView.onEndBatchEdit();
1028
1029 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1030 mTextView.updateAfterEdit();
1031 reportExtractedText();
1032 } else if (ims.mCursorChanged) {
1033 // Cheezy way to get us to report the current cursor location.
1034 mTextView.invalidateCursor();
1035 }
1036 }
1037
1038 static final int EXTRACT_NOTHING = -2;
1039 static final int EXTRACT_UNKNOWN = -1;
1040
1041 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1042 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1043 EXTRACT_UNKNOWN, outText);
1044 }
1045
1046 private boolean extractTextInternal(ExtractedTextRequest request,
1047 int partialStartOffset, int partialEndOffset, int delta,
1048 ExtractedText outText) {
1049 final CharSequence content = mTextView.getText();
1050 if (content != null) {
1051 if (partialStartOffset != EXTRACT_NOTHING) {
1052 final int N = content.length();
1053 if (partialStartOffset < 0) {
1054 outText.partialStartOffset = outText.partialEndOffset = -1;
1055 partialStartOffset = 0;
1056 partialEndOffset = N;
1057 } else {
1058 // Now use the delta to determine the actual amount of text
1059 // we need.
1060 partialEndOffset += delta;
1061 // Adjust offsets to ensure we contain full spans.
1062 if (content instanceof Spanned) {
1063 Spanned spanned = (Spanned)content;
1064 Object[] spans = spanned.getSpans(partialStartOffset,
1065 partialEndOffset, ParcelableSpan.class);
1066 int i = spans.length;
1067 while (i > 0) {
1068 i--;
1069 int j = spanned.getSpanStart(spans[i]);
1070 if (j < partialStartOffset) partialStartOffset = j;
1071 j = spanned.getSpanEnd(spans[i]);
1072 if (j > partialEndOffset) partialEndOffset = j;
1073 }
1074 }
1075 outText.partialStartOffset = partialStartOffset;
1076 outText.partialEndOffset = partialEndOffset - delta;
1077
1078 if (partialStartOffset > N) {
1079 partialStartOffset = N;
1080 } else if (partialStartOffset < 0) {
1081 partialStartOffset = 0;
1082 }
1083 if (partialEndOffset > N) {
1084 partialEndOffset = N;
1085 } else if (partialEndOffset < 0) {
1086 partialEndOffset = 0;
1087 }
1088 }
1089 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
1090 outText.text = content.subSequence(partialStartOffset,
1091 partialEndOffset);
1092 } else {
1093 outText.text = TextUtils.substring(content, partialStartOffset,
1094 partialEndOffset);
1095 }
1096 } else {
1097 outText.partialStartOffset = 0;
1098 outText.partialEndOffset = 0;
1099 outText.text = "";
1100 }
1101 outText.flags = 0;
1102 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1103 outText.flags |= ExtractedText.FLAG_SELECTING;
1104 }
1105 if (mTextView.isSingleLine()) {
1106 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1107 }
1108 outText.startOffset = 0;
1109 outText.selectionStart = mTextView.getSelectionStart();
1110 outText.selectionEnd = mTextView.getSelectionEnd();
1111 return true;
1112 }
1113 return false;
1114 }
1115
1116 boolean reportExtractedText() {
1117 final Editor.InputMethodState ims = mInputMethodState;
1118 if (ims != null) {
1119 final boolean contentChanged = ims.mContentChanged;
1120 if (contentChanged || ims.mSelectionModeChanged) {
1121 ims.mContentChanged = false;
1122 ims.mSelectionModeChanged = false;
1123 final ExtractedTextRequest req = ims.mExtracting;
1124 if (req != null) {
1125 InputMethodManager imm = InputMethodManager.peekInstance();
1126 if (imm != null) {
1127 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1128 "Retrieving extracted start=" + ims.mChangedStart +
1129 " end=" + ims.mChangedEnd +
1130 " delta=" + ims.mChangedDelta);
1131 if (ims.mChangedStart < 0 && !contentChanged) {
1132 ims.mChangedStart = EXTRACT_NOTHING;
1133 }
1134 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
1135 ims.mChangedDelta, ims.mTmpExtracted)) {
1136 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1137 "Reporting extracted start=" +
1138 ims.mTmpExtracted.partialStartOffset +
1139 " end=" + ims.mTmpExtracted.partialEndOffset +
1140 ": " + ims.mTmpExtracted.text);
1141 imm.updateExtractedText(mTextView, req.token, ims.mTmpExtracted);
1142 ims.mChangedStart = EXTRACT_UNKNOWN;
1143 ims.mChangedEnd = EXTRACT_UNKNOWN;
1144 ims.mChangedDelta = 0;
1145 ims.mContentChanged = false;
1146 return true;
1147 }
1148 }
1149 }
1150 }
1151 }
1152 return false;
1153 }
1154
1155 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1156 int cursorOffsetVertical) {
1157 final int selectionStart = mTextView.getSelectionStart();
1158 final int selectionEnd = mTextView.getSelectionEnd();
1159
1160 final InputMethodState ims = mInputMethodState;
1161 if (ims != null && ims.mBatchEditNesting == 0) {
1162 InputMethodManager imm = InputMethodManager.peekInstance();
1163 if (imm != null) {
1164 if (imm.isActive(mTextView)) {
1165 boolean reported = false;
1166 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1167 // We are in extract mode and the content has changed
1168 // in some way... just report complete new text to the
1169 // input method.
1170 reported = reportExtractedText();
1171 }
1172 if (!reported && highlight != null) {
1173 int candStart = -1;
1174 int candEnd = -1;
1175 if (mTextView.getText() instanceof Spannable) {
1176 Spannable sp = (Spannable) mTextView.getText();
1177 candStart = EditableInputConnection.getComposingSpanStart(sp);
1178 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1179 }
1180 imm.updateSelection(mTextView,
1181 selectionStart, selectionEnd, candStart, candEnd);
1182 }
1183 }
1184
1185 if (imm.isWatchingCursor(mTextView) && highlight != null) {
1186 highlight.computeBounds(ims.mTmpRectF, true);
1187 ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
1188
1189 canvas.getMatrix().mapPoints(ims.mTmpOffset);
1190 ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
1191
1192 ims.mTmpRectF.offset(0, cursorOffsetVertical);
1193
1194 ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
1195 (int)(ims.mTmpRectF.top + 0.5),
1196 (int)(ims.mTmpRectF.right + 0.5),
1197 (int)(ims.mTmpRectF.bottom + 0.5));
1198
1199 imm.updateCursor(mTextView,
1200 ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
1201 ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
1202 }
1203 }
1204 }
1205
1206 if (mCorrectionHighlighter != null) {
1207 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1208 }
1209
1210 if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
1211 drawCursor(canvas, cursorOffsetVertical);
1212 // Rely on the drawable entirely, do not draw the cursor line.
1213 // Has to be done after the IMM related code above which relies on the highlight.
1214 highlight = null;
1215 }
1216
1217 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1218 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1219 cursorOffsetVertical);
1220 } else {
1221 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1222 }
1223 }
1224
1225 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1226 Paint highlightPaint, int cursorOffsetVertical) {
1227 final int width = mTextView.getWidth();
1228 final int height = mTextView.getHeight();
1229
1230 final long lineRange = layout.getLineRangeForDraw(canvas);
1231 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1232 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1233 if (lastLine < 0) return;
1234
1235 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1236 firstLine, lastLine);
1237
1238 if (layout instanceof DynamicLayout) {
1239 if (mTextDisplayLists == null) {
1240 mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
1241 }
1242
1243 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1244 int[] blockEnds = dynamicLayout.getBlockEnds();
1245 int[] blockIndices = dynamicLayout.getBlockIndices();
1246 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1247
1248 final int mScrollX = mTextView.getScrollX();
1249 final int mScrollY = mTextView.getScrollY();
1250 canvas.translate(mScrollX, mScrollY);
1251 int endOfPreviousBlock = -1;
1252 int searchStartIndex = 0;
1253 for (int i = 0; i < numberOfBlocks; i++) {
1254 int blockEnd = blockEnds[i];
1255 int blockIndex = blockIndices[i];
1256
1257 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1258 if (blockIsInvalid) {
1259 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1260 searchStartIndex);
1261 // Dynamic layout internal block indices structure is updated from Editor
1262 blockIndices[i] = blockIndex;
1263 searchStartIndex = blockIndex + 1;
1264 }
1265
1266 DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
1267 if (blockDisplayList == null) {
1268 blockDisplayList = mTextDisplayLists[blockIndex] =
1269 mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex);
1270 } else {
1271 if (blockIsInvalid) blockDisplayList.invalidate();
1272 }
1273
1274 if (!blockDisplayList.isValid()) {
1275 final HardwareCanvas hardwareCanvas = blockDisplayList.start();
1276 try {
1277 hardwareCanvas.setViewport(width, height);
1278 // The dirty rect should always be null for a display list
1279 hardwareCanvas.onPreDraw(null);
1280 hardwareCanvas.translate(-mScrollX, -mScrollY);
1281 layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd);
1282 hardwareCanvas.translate(mScrollX, mScrollY);
1283 } finally {
1284 hardwareCanvas.onPostDraw();
1285 blockDisplayList.end();
1286 if (View.USE_DISPLAY_LIST_PROPERTIES) {
1287 blockDisplayList.setLeftTopRightBottom(0, 0, width, height);
1288 }
1289 }
1290 }
1291
1292 ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null,
1293 DisplayList.FLAG_CLIP_CHILDREN);
1294 endOfPreviousBlock = blockEnd;
1295 }
1296 canvas.translate(-mScrollX, -mScrollY);
1297 } else {
1298 // Boring layout is used for empty and hint text
1299 layout.drawText(canvas, firstLine, lastLine);
1300 }
1301 }
1302
1303 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1304 int searchStartIndex) {
1305 int length = mTextDisplayLists.length;
1306 for (int i = searchStartIndex; i < length; i++) {
1307 boolean blockIndexFound = false;
1308 for (int j = 0; j < numberOfBlocks; j++) {
1309 if (blockIndices[j] == i) {
1310 blockIndexFound = true;
1311 break;
1312 }
1313 }
1314 if (blockIndexFound) continue;
1315 return i;
1316 }
1317
1318 // No available index found, the pool has to grow
1319 int newSize = ArrayUtils.idealIntArraySize(length + 1);
1320 DisplayList[] displayLists = new DisplayList[newSize];
1321 System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
1322 mTextDisplayLists = displayLists;
1323 return length;
1324 }
1325
1326 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1327 final boolean translate = cursorOffsetVertical != 0;
1328 if (translate) canvas.translate(0, cursorOffsetVertical);
1329 for (int i = 0; i < mCursorCount; i++) {
1330 mCursorDrawable[i].draw(canvas);
1331 }
1332 if (translate) canvas.translate(0, -cursorOffsetVertical);
1333 }
1334
1335 void invalidateTextDisplayList() {
1336 if (mTextDisplayLists != null) {
1337 for (int i = 0; i < mTextDisplayLists.length; i++) {
1338 if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
1339 }
1340 }
1341 }
1342
1343 void updateCursorsPositions() {
1344 if (mTextView.mCursorDrawableRes == 0) {
1345 mCursorCount = 0;
1346 return;
1347 }
1348
1349 Layout layout = mTextView.getLayout();
1350 final int offset = mTextView.getSelectionStart();
1351 final int line = layout.getLineForOffset(offset);
1352 final int top = layout.getLineTop(line);
1353 final int bottom = layout.getLineTop(line + 1);
1354
1355 mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
1356
1357 int middle = bottom;
1358 if (mCursorCount == 2) {
1359 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
1360 middle = (top + bottom) >> 1;
1361 }
1362
1363 updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset));
1364
1365 if (mCursorCount == 2) {
1366 updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset));
1367 }
1368 }
1369
1370 /**
1371 * @return true if the selection mode was actually started.
1372 */
1373 boolean startSelectionActionMode() {
1374 if (mSelectionActionMode != null) {
1375 // Selection action mode is already started
1376 return false;
1377 }
1378
1379 if (!canSelectText() || !mTextView.requestFocus()) {
1380 Log.w(TextView.LOG_TAG,
1381 "TextView does not support text selection. Action mode cancelled.");
1382 return false;
1383 }
1384
1385 if (!mTextView.hasSelection()) {
1386 // There may already be a selection on device rotation
1387 if (!selectCurrentWord()) {
1388 // No word found under cursor or text selection not permitted.
1389 return false;
1390 }
1391 }
1392
1393 boolean willExtract = extractedTextModeWillBeStarted();
1394
1395 // Do not start the action mode when extracted text will show up full screen, which would
1396 // immediately hide the newly created action bar and would be visually distracting.
1397 if (!willExtract) {
1398 ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
1399 mSelectionActionMode = mTextView.startActionMode(actionModeCallback);
1400 }
1401
1402 final boolean selectionStarted = mSelectionActionMode != null || willExtract;
1403 if (selectionStarted && !mTextView.isTextSelectable()) {
1404 // Show the IME to be able to replace text, except when selecting non editable text.
1405 final InputMethodManager imm = InputMethodManager.peekInstance();
1406 if (imm != null) {
1407 imm.showSoftInput(mTextView, 0, null);
1408 }
1409 }
1410
1411 return selectionStarted;
1412 }
1413
1414 private boolean extractedTextModeWillBeStarted() {
1415 if (!(mTextView instanceof ExtractEditText)) {
1416 final InputMethodManager imm = InputMethodManager.peekInstance();
1417 return imm != null && imm.isFullscreenMode();
1418 }
1419 return false;
1420 }
1421
1422 /**
1423 * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
1424 */
1425 private boolean isCursorInsideSuggestionSpan() {
1426 CharSequence text = mTextView.getText();
1427 if (!(text instanceof Spannable)) return false;
1428
1429 SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans(
1430 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class);
1431 return (suggestionSpans.length > 0);
1432 }
1433
1434 /**
1435 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
1436 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
1437 */
1438 private boolean isCursorInsideEasyCorrectionSpan() {
1439 Spannable spannable = (Spannable) mTextView.getText();
1440 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
1441 mTextView.getSelectionEnd(), SuggestionSpan.class);
1442 for (int i = 0; i < suggestionSpans.length; i++) {
1443 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
1444 return true;
1445 }
1446 }
1447 return false;
1448 }
1449
1450 void onTouchUpEvent(MotionEvent event) {
1451 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
1452 hideControllers();
1453 CharSequence text = mTextView.getText();
1454 if (!selectAllGotFocus && text.length() > 0) {
1455 // Move cursor
1456 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1457 Selection.setSelection((Spannable) text, offset);
1458 if (mSpellChecker != null) {
1459 // When the cursor moves, the word that was typed may need spell check
1460 mSpellChecker.onSelectionChanged();
1461 }
1462 if (!extractedTextModeWillBeStarted()) {
1463 if (isCursorInsideEasyCorrectionSpan()) {
1464 mShowSuggestionRunnable = new Runnable() {
1465 public void run() {
1466 showSuggestions();
1467 }
1468 };
1469 // removeCallbacks is performed on every touch
1470 mTextView.postDelayed(mShowSuggestionRunnable,
1471 ViewConfiguration.getDoubleTapTimeout());
1472 } else if (hasInsertionController()) {
1473 getInsertionController().show();
1474 }
1475 }
1476 }
1477 }
1478
1479 protected void stopSelectionActionMode() {
1480 if (mSelectionActionMode != null) {
1481 // This will hide the mSelectionModifierCursorController
1482 mSelectionActionMode.finish();
1483 }
1484 }
1485
1486 /**
1487 * @return True if this view supports insertion handles.
1488 */
1489 boolean hasInsertionController() {
1490 return mInsertionControllerEnabled;
1491 }
1492
1493 /**
1494 * @return True if this view supports selection handles.
1495 */
1496 boolean hasSelectionController() {
1497 return mSelectionControllerEnabled;
1498 }
1499
1500 InsertionPointCursorController getInsertionController() {
1501 if (!mInsertionControllerEnabled) {
1502 return null;
1503 }
1504
1505 if (mInsertionPointCursorController == null) {
1506 mInsertionPointCursorController = new InsertionPointCursorController();
1507
1508 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1509 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
1510 }
1511
1512 return mInsertionPointCursorController;
1513 }
1514
1515 SelectionModifierCursorController getSelectionController() {
1516 if (!mSelectionControllerEnabled) {
1517 return null;
1518 }
1519
1520 if (mSelectionModifierCursorController == null) {
1521 mSelectionModifierCursorController = new SelectionModifierCursorController();
1522
1523 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1524 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
1525 }
1526
1527 return mSelectionModifierCursorController;
1528 }
1529
1530 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
1531 if (mCursorDrawable[cursorIndex] == null)
1532 mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable(
1533 mTextView.mCursorDrawableRes);
1534
1535 if (mTempRect == null) mTempRect = new Rect();
1536 mCursorDrawable[cursorIndex].getPadding(mTempRect);
1537 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
1538 horizontal = Math.max(0.5f, horizontal - 0.5f);
1539 final int left = (int) (horizontal) - mTempRect.left;
1540 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
1541 bottom + mTempRect.bottom);
1542 }
1543
1544 /**
1545 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
1546 * a dictionnary) from the current input method, provided by it calling
1547 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
1548 * implementation flashes the background of the corrected word to provide feedback to the user.
1549 *
1550 * @param info The auto correct info about the text that was corrected.
1551 */
1552 public void onCommitCorrection(CorrectionInfo info) {
1553 if (mCorrectionHighlighter == null) {
1554 mCorrectionHighlighter = new CorrectionHighlighter();
1555 } else {
1556 mCorrectionHighlighter.invalidate(false);
1557 }
1558
1559 mCorrectionHighlighter.highlight(info);
1560 }
1561
1562 void showSuggestions() {
1563 if (mSuggestionsPopupWindow == null) {
1564 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
1565 }
1566 hideControllers();
1567 mSuggestionsPopupWindow.show();
1568 }
1569
1570 boolean areSuggestionsShown() {
1571 return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
1572 }
1573
1574 void onScrollChanged() {
1575 if (mPositionListener != null) {
1576 mPositionListener.onScrollChanged();
1577 }
1578 // Internal scroll affects the clip boundaries
1579 invalidateTextDisplayList();
1580 }
1581
1582 /**
1583 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
1584 */
1585 private boolean shouldBlink() {
1586 if (!isCursorVisible() || !mTextView.isFocused()) return false;
1587
1588 final int start = mTextView.getSelectionStart();
1589 if (start < 0) return false;
1590
1591 final int end = mTextView.getSelectionEnd();
1592 if (end < 0) return false;
1593
1594 return start == end;
1595 }
1596
1597 void makeBlink() {
1598 if (shouldBlink()) {
1599 mShowCursor = SystemClock.uptimeMillis();
1600 if (mBlink == null) mBlink = new Blink();
1601 mBlink.removeCallbacks(mBlink);
1602 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
1603 } else {
1604 if (mBlink != null) mBlink.removeCallbacks(mBlink);
1605 }
1606 }
1607
1608 private class Blink extends Handler implements Runnable {
1609 private boolean mCancelled;
1610
1611 public void run() {
1612 Log.d("GILLES", "blinking !!!");
1613 if (mCancelled) {
1614 return;
1615 }
1616
1617 removeCallbacks(Blink.this);
1618
1619 if (shouldBlink()) {
1620 if (mTextView.getLayout() != null) {
1621 mTextView.invalidateCursorPath();
1622 }
1623
1624 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
1625 }
1626 }
1627
1628 void cancel() {
1629 if (!mCancelled) {
1630 removeCallbacks(Blink.this);
1631 mCancelled = true;
1632 }
1633 }
1634
1635 void uncancel() {
1636 mCancelled = false;
1637 }
1638 }
1639
1640 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
1641 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
1642 com.android.internal.R.layout.text_drag_thumbnail, null);
1643
1644 if (shadowView == null) {
1645 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
1646 }
1647
1648 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
1649 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
1650 }
1651 shadowView.setText(text);
1652 shadowView.setTextColor(mTextView.getTextColors());
1653
1654 shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge);
1655 shadowView.setGravity(Gravity.CENTER);
1656
1657 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
1658 ViewGroup.LayoutParams.WRAP_CONTENT));
1659
1660 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
1661 shadowView.measure(size, size);
1662
1663 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
1664 shadowView.invalidate();
1665 return new DragShadowBuilder(shadowView);
1666 }
1667
1668 private static class DragLocalState {
1669 public TextView sourceTextView;
1670 public int start, end;
1671
1672 public DragLocalState(TextView sourceTextView, int start, int end) {
1673 this.sourceTextView = sourceTextView;
1674 this.start = start;
1675 this.end = end;
1676 }
1677 }
1678
1679 void onDrop(DragEvent event) {
1680 StringBuilder content = new StringBuilder("");
1681 ClipData clipData = event.getClipData();
1682 final int itemCount = clipData.getItemCount();
1683 for (int i=0; i < itemCount; i++) {
1684 Item item = clipData.getItemAt(i);
1685 content.append(item.coerceToText(mTextView.getContext()));
1686 }
1687
1688 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1689
1690 Object localState = event.getLocalState();
1691 DragLocalState dragLocalState = null;
1692 if (localState instanceof DragLocalState) {
1693 dragLocalState = (DragLocalState) localState;
1694 }
1695 boolean dragDropIntoItself = dragLocalState != null &&
1696 dragLocalState.sourceTextView == mTextView;
1697
1698 if (dragDropIntoItself) {
1699 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
1700 // A drop inside the original selection discards the drop.
1701 return;
1702 }
1703 }
1704
1705 final int originalLength = mTextView.getText().length();
1706 long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content);
1707 int min = TextUtils.unpackRangeStartFromLong(minMax);
1708 int max = TextUtils.unpackRangeEndFromLong(minMax);
1709
1710 Selection.setSelection((Spannable) mTextView.getText(), max);
1711 mTextView.replaceText_internal(min, max, content);
1712
1713 if (dragDropIntoItself) {
1714 int dragSourceStart = dragLocalState.start;
1715 int dragSourceEnd = dragLocalState.end;
1716 if (max <= dragSourceStart) {
1717 // Inserting text before selection has shifted positions
1718 final int shift = mTextView.getText().length() - originalLength;
1719 dragSourceStart += shift;
1720 dragSourceEnd += shift;
1721 }
1722
1723 // Delete original selection
1724 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
1725
1726 // Make sure we do not leave two adjacent spaces.
1727 CharSequence t = mTextView.getTransformedText(dragSourceStart - 1, dragSourceStart + 1);
1728 if ( (dragSourceStart == 0 || Character.isSpaceChar(t.charAt(0))) &&
1729 (dragSourceStart == mTextView.getText().length() ||
1730 Character.isSpaceChar(t.charAt(1))) ) {
1731 final int pos = dragSourceStart == mTextView.getText().length() ?
1732 dragSourceStart - 1 : dragSourceStart;
1733 mTextView.deleteText_internal(pos, pos + 1);
1734 }
1735 }
1736 }
1737
1738 /**
1739 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
1740 * pop-up should be displayed.
1741 */
1742 class EasyEditSpanController implements TextWatcher {
1743
1744 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
1745
1746 private EasyEditPopupWindow mPopupWindow;
1747
1748 private EasyEditSpan mEasyEditSpan;
1749
1750 private Runnable mHidePopup;
1751
1752 public void hide() {
1753 if (mPopupWindow != null) {
1754 mPopupWindow.hide();
1755 mTextView.removeCallbacks(mHidePopup);
1756 }
1757 removeSpans(mTextView.getText());
1758 mEasyEditSpan = null;
1759 }
1760
1761 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1762 // Intentionally empty
1763 }
1764
1765 public void afterTextChanged(Editable s) {
1766 // Intentionally empty
1767 }
1768
1769 /**
1770 * Monitors the changes in the text.
1771 *
1772 * <p>{@link SpanWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
1773 * as the notifications are not sent when a spannable (with spans) is inserted.
1774 */
1775 public void onTextChanged(CharSequence buffer, int start, int before, int after) {
1776 adjustSpans(buffer, start, after);
1777
1778 if (mTextView.getWindowVisibility() != View.VISIBLE) {
1779 // The window is not visible yet, ignore the text change.
1780 return;
1781 }
1782
1783 if (mTextView.getLayout() == null) {
1784 // The view has not been layout yet, ignore the text change
1785 return;
1786 }
1787
1788 InputMethodManager imm = InputMethodManager.peekInstance();
1789 if (!(mTextView instanceof ExtractEditText) && imm != null && imm.isFullscreenMode()) {
1790 // The input is in extract mode. We do not have to handle the easy edit in the
1791 // original TextView, as the ExtractEditText will do
1792 return;
1793 }
1794
1795 // Remove the current easy edit span, as the text changed, and remove the pop-up
1796 // (if any)
1797 if (mEasyEditSpan != null) {
1798 if (buffer instanceof Spannable) {
1799 ((Spannable) buffer).removeSpan(mEasyEditSpan);
1800 }
1801 mEasyEditSpan = null;
1802 }
1803 if (mPopupWindow != null && mPopupWindow.isShowing()) {
1804 mPopupWindow.hide();
1805 }
1806
1807 // Display the new easy edit span (if any).
1808 if (buffer instanceof Spanned) {
1809 mEasyEditSpan = getSpan((Spanned) buffer);
1810 if (mEasyEditSpan != null) {
1811 if (mPopupWindow == null) {
1812 mPopupWindow = new EasyEditPopupWindow();
1813 mHidePopup = new Runnable() {
1814 @Override
1815 public void run() {
1816 hide();
1817 }
1818 };
1819 }
1820 mPopupWindow.show(mEasyEditSpan);
1821 mTextView.removeCallbacks(mHidePopup);
1822 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
1823 }
1824 }
1825 }
1826
1827 /**
1828 * Adjusts the spans by removing all of them except the last one.
1829 */
1830 private void adjustSpans(CharSequence buffer, int start, int after) {
1831 // This method enforces that only one easy edit span is attached to the text.
1832 // A better way to enforce this would be to listen for onSpanAdded, but this method
1833 // cannot be used in this scenario as no notification is triggered when a text with
1834 // spans is inserted into a text.
1835 if (buffer instanceof Spannable) {
1836 Spannable spannable = (Spannable) buffer;
1837 EasyEditSpan[] spans = spannable.getSpans(start, start + after, EasyEditSpan.class);
1838 if (spans.length > 0) {
1839 // Assuming there was only one EasyEditSpan before, we only need check to
1840 // check for a duplicate if a new one is found in the modified interval
1841 spans = spannable.getSpans(0, spannable.length(), EasyEditSpan.class);
1842 for (int i = 1; i < spans.length; i++) {
1843 spannable.removeSpan(spans[i]);
1844 }
1845 }
1846 }
1847 }
1848
1849 /**
1850 * Removes all the {@link EasyEditSpan} currently attached.
1851 */
1852 private void removeSpans(CharSequence buffer) {
1853 if (buffer instanceof Spannable) {
1854 Spannable spannable = (Spannable) buffer;
1855 EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
1856 EasyEditSpan.class);
1857 for (int i = 0; i < spans.length; i++) {
1858 spannable.removeSpan(spans[i]);
1859 }
1860 }
1861 }
1862
1863 private EasyEditSpan getSpan(Spanned spanned) {
1864 EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
1865 EasyEditSpan.class);
1866 if (easyEditSpans.length == 0) {
1867 return null;
1868 } else {
1869 return easyEditSpans[0];
1870 }
1871 }
1872 }
1873
1874 /**
1875 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
1876 * by {@link EasyEditSpanController}.
1877 */
1878 private class EasyEditPopupWindow extends PinnedPopupWindow
1879 implements OnClickListener {
1880 private static final int POPUP_TEXT_LAYOUT =
1881 com.android.internal.R.layout.text_edit_action_popup_text;
1882 private TextView mDeleteTextView;
1883 private EasyEditSpan mEasyEditSpan;
1884
1885 @Override
1886 protected void createPopupWindow() {
1887 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
1888 com.android.internal.R.attr.textSelectHandleWindowStyle);
1889 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1890 mPopupWindow.setClippingEnabled(true);
1891 }
1892
1893 @Override
1894 protected void initContentView() {
1895 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
1896 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
1897 mContentView = linearLayout;
1898 mContentView.setBackgroundResource(
1899 com.android.internal.R.drawable.text_edit_side_paste_window);
1900
1901 LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
1902 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1903
1904 LayoutParams wrapContent = new LayoutParams(
1905 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
1906
1907 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
1908 mDeleteTextView.setLayoutParams(wrapContent);
1909 mDeleteTextView.setText(com.android.internal.R.string.delete);
1910 mDeleteTextView.setOnClickListener(this);
1911 mContentView.addView(mDeleteTextView);
1912 }
1913
1914 public void show(EasyEditSpan easyEditSpan) {
1915 mEasyEditSpan = easyEditSpan;
1916 super.show();
1917 }
1918
1919 @Override
1920 public void onClick(View view) {
1921 if (view == mDeleteTextView) {
1922 Editable editable = (Editable) mTextView.getText();
1923 int start = editable.getSpanStart(mEasyEditSpan);
1924 int end = editable.getSpanEnd(mEasyEditSpan);
1925 if (start >= 0 && end >= 0) {
1926 mTextView.deleteText_internal(start, end);
1927 }
1928 }
1929 }
1930
1931 @Override
1932 protected int getTextOffset() {
1933 // Place the pop-up at the end of the span
1934 Editable editable = (Editable) mTextView.getText();
1935 return editable.getSpanEnd(mEasyEditSpan);
1936 }
1937
1938 @Override
1939 protected int getVerticalLocalPosition(int line) {
1940 return mTextView.getLayout().getLineBottom(line);
1941 }
1942
1943 @Override
1944 protected int clipVertically(int positionY) {
1945 // As we display the pop-up below the span, no vertical clipping is required.
1946 return positionY;
1947 }
1948 }
1949
1950 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
1951 // 3 handles
1952 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
1953 private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
1954 private TextViewPositionListener[] mPositionListeners =
1955 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
1956 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
1957 private boolean mPositionHasChanged = true;
1958 // Absolute position of the TextView with respect to its parent window
1959 private int mPositionX, mPositionY;
1960 private int mNumberOfListeners;
1961 private boolean mScrollHasChanged;
1962 final int[] mTempCoords = new int[2];
1963
1964 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
1965 if (mNumberOfListeners == 0) {
1966 updatePosition();
1967 ViewTreeObserver vto = mTextView.getViewTreeObserver();
1968 vto.addOnPreDrawListener(this);
1969 }
1970
1971 int emptySlotIndex = -1;
1972 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
1973 TextViewPositionListener listener = mPositionListeners[i];
1974 if (listener == positionListener) {
1975 return;
1976 } else if (emptySlotIndex < 0 && listener == null) {
1977 emptySlotIndex = i;
1978 }
1979 }
1980
1981 mPositionListeners[emptySlotIndex] = positionListener;
1982 mCanMove[emptySlotIndex] = canMove;
1983 mNumberOfListeners++;
1984 }
1985
1986 public void removeSubscriber(TextViewPositionListener positionListener) {
1987 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
1988 if (mPositionListeners[i] == positionListener) {
1989 mPositionListeners[i] = null;
1990 mNumberOfListeners--;
1991 break;
1992 }
1993 }
1994
1995 if (mNumberOfListeners == 0) {
1996 ViewTreeObserver vto = mTextView.getViewTreeObserver();
1997 vto.removeOnPreDrawListener(this);
1998 }
1999 }
2000
2001 public int getPositionX() {
2002 return mPositionX;
2003 }
2004
2005 public int getPositionY() {
2006 return mPositionY;
2007 }
2008
2009 @Override
2010 public boolean onPreDraw() {
2011 updatePosition();
2012
2013 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2014 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
2015 TextViewPositionListener positionListener = mPositionListeners[i];
2016 if (positionListener != null) {
2017 positionListener.updatePosition(mPositionX, mPositionY,
2018 mPositionHasChanged, mScrollHasChanged);
2019 }
2020 }
2021 }
2022
2023 mScrollHasChanged = false;
2024 return true;
2025 }
2026
2027 private void updatePosition() {
2028 mTextView.getLocationInWindow(mTempCoords);
2029
2030 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
2031
2032 mPositionX = mTempCoords[0];
2033 mPositionY = mTempCoords[1];
2034 }
2035
2036 public void onScrollChanged() {
2037 mScrollHasChanged = true;
2038 }
2039 }
2040
2041 private abstract class PinnedPopupWindow implements TextViewPositionListener {
2042 protected PopupWindow mPopupWindow;
2043 protected ViewGroup mContentView;
2044 int mPositionX, mPositionY;
2045
2046 protected abstract void createPopupWindow();
2047 protected abstract void initContentView();
2048 protected abstract int getTextOffset();
2049 protected abstract int getVerticalLocalPosition(int line);
2050 protected abstract int clipVertically(int positionY);
2051
2052 public PinnedPopupWindow() {
2053 createPopupWindow();
2054
2055 mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
2056 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
2057 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
2058
2059 initContentView();
2060
2061 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2062 ViewGroup.LayoutParams.WRAP_CONTENT);
2063 mContentView.setLayoutParams(wrapContent);
2064
2065 mPopupWindow.setContentView(mContentView);
2066 }
2067
2068 public void show() {
2069 getPositionListener().addSubscriber(this, false /* offset is fixed */);
2070
2071 computeLocalPosition();
2072
2073 final PositionListener positionListener = getPositionListener();
2074 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
2075 }
2076
2077 protected void measureContent() {
2078 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2079 mContentView.measure(
2080 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
2081 View.MeasureSpec.AT_MOST),
2082 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
2083 View.MeasureSpec.AT_MOST));
2084 }
2085
2086 /* The popup window will be horizontally centered on the getTextOffset() and vertically
2087 * positioned according to viewportToContentHorizontalOffset.
2088 *
2089 * This method assumes that mContentView has properly been measured from its content. */
2090 private void computeLocalPosition() {
2091 measureContent();
2092 final int width = mContentView.getMeasuredWidth();
2093 final int offset = getTextOffset();
2094 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
2095 mPositionX += mTextView.viewportToContentHorizontalOffset();
2096
2097 final int line = mTextView.getLayout().getLineForOffset(offset);
2098 mPositionY = getVerticalLocalPosition(line);
2099 mPositionY += mTextView.viewportToContentVerticalOffset();
2100 }
2101
2102 private void updatePosition(int parentPositionX, int parentPositionY) {
2103 int positionX = parentPositionX + mPositionX;
2104 int positionY = parentPositionY + mPositionY;
2105
2106 positionY = clipVertically(positionY);
2107
2108 // Horizontal clipping
2109 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2110 final int width = mContentView.getMeasuredWidth();
2111 positionX = Math.min(displayMetrics.widthPixels - width, positionX);
2112 positionX = Math.max(0, positionX);
2113
2114 if (isShowing()) {
2115 mPopupWindow.update(positionX, positionY, -1, -1);
2116 } else {
2117 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
2118 positionX, positionY);
2119 }
2120 }
2121
2122 public void hide() {
2123 mPopupWindow.dismiss();
2124 getPositionListener().removeSubscriber(this);
2125 }
2126
2127 @Override
2128 public void updatePosition(int parentPositionX, int parentPositionY,
2129 boolean parentPositionChanged, boolean parentScrolled) {
2130 // Either parentPositionChanged or parentScrolled is true, check if still visible
2131 if (isShowing() && isOffsetVisible(getTextOffset())) {
2132 if (parentScrolled) computeLocalPosition();
2133 updatePosition(parentPositionX, parentPositionY);
2134 } else {
2135 hide();
2136 }
2137 }
2138
2139 public boolean isShowing() {
2140 return mPopupWindow.isShowing();
2141 }
2142 }
2143
2144 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
2145 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
2146 private static final int ADD_TO_DICTIONARY = -1;
2147 private static final int DELETE_TEXT = -2;
2148 private SuggestionInfo[] mSuggestionInfos;
2149 private int mNumberOfSuggestions;
2150 private boolean mCursorWasVisibleBeforeSuggestions;
2151 private boolean mIsShowingUp = false;
2152 private SuggestionAdapter mSuggestionsAdapter;
2153 private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
2154 private final HashMap<SuggestionSpan, Integer> mSpansLengths;
2155
2156 private class CustomPopupWindow extends PopupWindow {
2157 public CustomPopupWindow(Context context, int defStyle) {
2158 super(context, null, defStyle);
2159 }
2160
2161 @Override
2162 public void dismiss() {
2163 super.dismiss();
2164
2165 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
2166
2167 // Safe cast since show() checks that mTextView.getText() is an Editable
2168 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
2169
2170 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
2171 if (hasInsertionController()) {
2172 getInsertionController().show();
2173 }
2174 }
2175 }
2176
2177 public SuggestionsPopupWindow() {
2178 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2179 mSuggestionSpanComparator = new SuggestionSpanComparator();
2180 mSpansLengths = new HashMap<SuggestionSpan, Integer>();
2181 }
2182
2183 @Override
2184 protected void createPopupWindow() {
2185 mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
2186 com.android.internal.R.attr.textSuggestionsWindowStyle);
2187 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2188 mPopupWindow.setFocusable(true);
2189 mPopupWindow.setClippingEnabled(false);
2190 }
2191
2192 @Override
2193 protected void initContentView() {
2194 ListView listView = new ListView(mTextView.getContext());
2195 mSuggestionsAdapter = new SuggestionAdapter();
2196 listView.setAdapter(mSuggestionsAdapter);
2197 listView.setOnItemClickListener(this);
2198 mContentView = listView;
2199
2200 // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
2201 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
2202 for (int i = 0; i < mSuggestionInfos.length; i++) {
2203 mSuggestionInfos[i] = new SuggestionInfo();
2204 }
2205 }
2206
2207 public boolean isShowingUp() {
2208 return mIsShowingUp;
2209 }
2210
2211 public void onParentLostFocus() {
2212 mIsShowingUp = false;
2213 }
2214
2215 private class SuggestionInfo {
2216 int suggestionStart, suggestionEnd; // range of actual suggestion within text
2217 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
2218 int suggestionIndex; // the index of this suggestion inside suggestionSpan
2219 SpannableStringBuilder text = new SpannableStringBuilder();
2220 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(),
2221 android.R.style.TextAppearance_SuggestionHighlight);
2222 }
2223
2224 private class SuggestionAdapter extends BaseAdapter {
2225 private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
2226 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2227
2228 @Override
2229 public int getCount() {
2230 return mNumberOfSuggestions;
2231 }
2232
2233 @Override
2234 public Object getItem(int position) {
2235 return mSuggestionInfos[position];
2236 }
2237
2238 @Override
2239 public long getItemId(int position) {
2240 return position;
2241 }
2242
2243 @Override
2244 public View getView(int position, View convertView, ViewGroup parent) {
2245 TextView textView = (TextView) convertView;
2246
2247 if (textView == null) {
2248 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
2249 parent, false);
2250 }
2251
2252 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2253 textView.setText(suggestionInfo.text);
2254
2255 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
2256 textView.setCompoundDrawablesWithIntrinsicBounds(
2257 com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
2258 } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
2259 textView.setCompoundDrawablesWithIntrinsicBounds(
2260 com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
2261 } else {
2262 textView.setCompoundDrawables(null, null, null, null);
2263 }
2264
2265 return textView;
2266 }
2267 }
2268
2269 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
2270 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
2271 final int flag1 = span1.getFlags();
2272 final int flag2 = span2.getFlags();
2273 if (flag1 != flag2) {
2274 // The order here should match what is used in updateDrawState
2275 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2276 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2277 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2278 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2279 if (easy1 && !misspelled1) return -1;
2280 if (easy2 && !misspelled2) return 1;
2281 if (misspelled1) return -1;
2282 if (misspelled2) return 1;
2283 }
2284
2285 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
2286 }
2287 }
2288
2289 /**
2290 * Returns the suggestion spans that cover the current cursor position. The suggestion
2291 * spans are sorted according to the length of text that they are attached to.
2292 */
2293 private SuggestionSpan[] getSuggestionSpans() {
2294 int pos = mTextView.getSelectionStart();
2295 Spannable spannable = (Spannable) mTextView.getText();
2296 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
2297
2298 mSpansLengths.clear();
2299 for (SuggestionSpan suggestionSpan : suggestionSpans) {
2300 int start = spannable.getSpanStart(suggestionSpan);
2301 int end = spannable.getSpanEnd(suggestionSpan);
2302 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
2303 }
2304
2305 // The suggestions are sorted according to their types (easy correction first, then
2306 // misspelled) and to the length of the text that they cover (shorter first).
2307 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
2308 return suggestionSpans;
2309 }
2310
2311 @Override
2312 public void show() {
2313 if (!(mTextView.getText() instanceof Editable)) return;
2314
2315 if (updateSuggestions()) {
2316 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2317 mTextView.setCursorVisible(false);
2318 mIsShowingUp = true;
2319 super.show();
2320 }
2321 }
2322
2323 @Override
2324 protected void measureContent() {
2325 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2326 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
2327 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
2328 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
2329 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
2330
2331 int width = 0;
2332 View view = null;
2333 for (int i = 0; i < mNumberOfSuggestions; i++) {
2334 view = mSuggestionsAdapter.getView(i, view, mContentView);
2335 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
2336 view.measure(horizontalMeasure, verticalMeasure);
2337 width = Math.max(width, view.getMeasuredWidth());
2338 }
2339
2340 // Enforce the width based on actual text widths
2341 mContentView.measure(
2342 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
2343 verticalMeasure);
2344
2345 Drawable popupBackground = mPopupWindow.getBackground();
2346 if (popupBackground != null) {
2347 if (mTempRect == null) mTempRect = new Rect();
2348 popupBackground.getPadding(mTempRect);
2349 width += mTempRect.left + mTempRect.right;
2350 }
2351 mPopupWindow.setWidth(width);
2352 }
2353
2354 @Override
2355 protected int getTextOffset() {
2356 return mTextView.getSelectionStart();
2357 }
2358
2359 @Override
2360 protected int getVerticalLocalPosition(int line) {
2361 return mTextView.getLayout().getLineBottom(line);
2362 }
2363
2364 @Override
2365 protected int clipVertically(int positionY) {
2366 final int height = mContentView.getMeasuredHeight();
2367 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2368 return Math.min(positionY, displayMetrics.heightPixels - height);
2369 }
2370
2371 @Override
2372 public void hide() {
2373 super.hide();
2374 }
2375
2376 private boolean updateSuggestions() {
2377 Spannable spannable = (Spannable) mTextView.getText();
2378 SuggestionSpan[] suggestionSpans = getSuggestionSpans();
2379
2380 final int nbSpans = suggestionSpans.length;
2381 // Suggestions are shown after a delay: the underlying spans may have been removed
2382 if (nbSpans == 0) return false;
2383
2384 mNumberOfSuggestions = 0;
2385 int spanUnionStart = mTextView.getText().length();
2386 int spanUnionEnd = 0;
2387
2388 SuggestionSpan misspelledSpan = null;
2389 int underlineColor = 0;
2390
2391 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
2392 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
2393 final int spanStart = spannable.getSpanStart(suggestionSpan);
2394 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
2395 spanUnionStart = Math.min(spanStart, spanUnionStart);
2396 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
2397
2398 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2399 misspelledSpan = suggestionSpan;
2400 }
2401
2402 // The first span dictates the background color of the highlighted text
2403 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
2404
2405 String[] suggestions = suggestionSpan.getSuggestions();
2406 int nbSuggestions = suggestions.length;
2407 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
2408 String suggestion = suggestions[suggestionIndex];
2409
2410 boolean suggestionIsDuplicate = false;
2411 for (int i = 0; i < mNumberOfSuggestions; i++) {
2412 if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
2413 SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
2414 final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
2415 final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
2416 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
2417 suggestionIsDuplicate = true;
2418 break;
2419 }
2420 }
2421 }
2422
2423 if (!suggestionIsDuplicate) {
2424 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2425 suggestionInfo.suggestionSpan = suggestionSpan;
2426 suggestionInfo.suggestionIndex = suggestionIndex;
2427 suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
2428
2429 mNumberOfSuggestions++;
2430
2431 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
2432 // Also end outer for loop
2433 spanIndex = nbSpans;
2434 break;
2435 }
2436 }
2437 }
2438 }
2439
2440 for (int i = 0; i < mNumberOfSuggestions; i++) {
2441 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
2442 }
2443
2444 // Add "Add to dictionary" item if there is a span with the misspelled flag
2445 if (misspelledSpan != null) {
2446 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
2447 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
2448 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
2449 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2450 suggestionInfo.suggestionSpan = misspelledSpan;
2451 suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
2452 suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView.
2453 getContext().getString(com.android.internal.R.string.addToDictionary));
2454 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2455 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2456
2457 mNumberOfSuggestions++;
2458 }
2459 }
2460
2461 // Delete item
2462 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2463 suggestionInfo.suggestionSpan = null;
2464 suggestionInfo.suggestionIndex = DELETE_TEXT;
2465 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
2466 mTextView.getContext().getString(com.android.internal.R.string.deleteText));
2467 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2468 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2469 mNumberOfSuggestions++;
2470
2471 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
2472 if (underlineColor == 0) {
2473 // Fallback on the default highlight color when the first span does not provide one
2474 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
2475 } else {
2476 final float BACKGROUND_TRANSPARENCY = 0.4f;
2477 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
2478 mSuggestionRangeSpan.setBackgroundColor(
2479 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
2480 }
2481 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
2482 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2483
2484 mSuggestionsAdapter.notifyDataSetChanged();
2485 return true;
2486 }
2487
2488 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
2489 int unionEnd) {
2490 final Spannable text = (Spannable) mTextView.getText();
2491 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
2492 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
2493
2494 // Adjust the start/end of the suggestion span
2495 suggestionInfo.suggestionStart = spanStart - unionStart;
2496 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
2497 + suggestionInfo.text.length();
2498
2499 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
2500 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2501
2502 // Add the text before and after the span.
2503 final String textAsString = text.toString();
2504 suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
2505 suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
2506 }
2507
2508 @Override
2509 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
2510 Editable editable = (Editable) mTextView.getText();
2511 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2512
2513 if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
2514 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
2515 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
2516 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
2517 // Do not leave two adjacent spaces after deletion, or one at beginning of text
2518 if (spanUnionEnd < editable.length() &&
2519 Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
2520 (spanUnionStart == 0 ||
2521 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
2522 spanUnionEnd = spanUnionEnd + 1;
2523 }
2524 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
2525 }
2526 hide();
2527 return;
2528 }
2529
2530 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
2531 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
2532 if (spanStart < 0 || spanEnd <= spanStart) {
2533 // Span has been removed
2534 hide();
2535 return;
2536 }
2537
2538 final String originalText = editable.toString().substring(spanStart, spanEnd);
2539
2540 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
2541 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
2542 intent.putExtra("word", originalText);
2543 intent.putExtra("locale", mTextView.getTextServicesLocale().toString());
2544 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
2545 mTextView.getContext().startActivity(intent);
2546 // There is no way to know if the word was indeed added. Re-check.
2547 // TODO The ExtractEditText should remove the span in the original text instead
2548 editable.removeSpan(suggestionInfo.suggestionSpan);
2549 updateSpellCheckSpans(spanStart, spanEnd, false);
2550 } else {
2551 // SuggestionSpans are removed by replace: save them before
2552 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2553 SuggestionSpan.class);
2554 final int length = suggestionSpans.length;
2555 int[] suggestionSpansStarts = new int[length];
2556 int[] suggestionSpansEnds = new int[length];
2557 int[] suggestionSpansFlags = new int[length];
2558 for (int i = 0; i < length; i++) {
2559 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2560 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2561 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2562 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2563
2564 // Remove potential misspelled flags
2565 int suggestionSpanFlags = suggestionSpan.getFlags();
2566 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
2567 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2568 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2569 suggestionSpan.setFlags(suggestionSpanFlags);
2570 }
2571 }
2572
2573 final int suggestionStart = suggestionInfo.suggestionStart;
2574 final int suggestionEnd = suggestionInfo.suggestionEnd;
2575 final String suggestion = suggestionInfo.text.subSequence(
2576 suggestionStart, suggestionEnd).toString();
2577 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2578
2579 // Notify source IME of the suggestion pick. Do this before swaping texts.
2580 if (!TextUtils.isEmpty(
2581 suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
2582 InputMethodManager imm = InputMethodManager.peekInstance();
2583 if (imm != null) {
2584 imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
2585 suggestionInfo.suggestionIndex);
2586 }
2587 }
2588
2589 // Swap text content between actual text and Suggestion span
2590 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
2591 suggestions[suggestionInfo.suggestionIndex] = originalText;
2592
2593 // Restore previous SuggestionSpans
2594 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
2595 for (int i = 0; i < length; i++) {
2596 // Only spans that include the modified region make sense after replacement
2597 // Spans partially included in the replaced region are removed, there is no
2598 // way to assign them a valid range after replacement
2599 if (suggestionSpansStarts[i] <= spanStart &&
2600 suggestionSpansEnds[i] >= spanEnd) {
2601 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2602 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
2603 }
2604 }
2605
2606 // Move cursor at the end of the replaced word
2607 final int newCursorPosition = spanEnd + lengthDifference;
2608 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2609 }
2610
2611 hide();
2612 }
2613 }
2614
2615 /**
2616 * An ActionMode Callback class that is used to provide actions while in text selection mode.
2617 *
2618 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
2619 * on which of these this TextView supports.
2620 */
2621 private class SelectionActionModeCallback implements ActionMode.Callback {
2622
2623 @Override
2624 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
2625 TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes(
2626 com.android.internal.R.styleable.SelectionModeDrawables);
2627
2628 boolean allowText = mTextView.getContext().getResources().getBoolean(
2629 com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
2630
2631 mode.setTitle(mTextView.getContext().getString(
2632 com.android.internal.R.string.textSelectionCABTitle));
2633 mode.setSubtitle(null);
2634 mode.setTitleOptionalHint(true);
2635
2636 int selectAllIconId = 0; // No icon by default
2637 if (!allowText) {
2638 // Provide an icon, text will not be displayed on smaller screens.
2639 selectAllIconId = styledAttributes.getResourceId(
2640 R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
2641 }
2642
2643 menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
2644 setIcon(selectAllIconId).
2645 setAlphabeticShortcut('a').
2646 setShowAsAction(
2647 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2648
2649 if (mTextView.canCut()) {
2650 menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut).
2651 setIcon(styledAttributes.getResourceId(
2652 R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
2653 setAlphabeticShortcut('x').
2654 setShowAsAction(
2655 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2656 }
2657
2658 if (mTextView.canCopy()) {
2659 menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy).
2660 setIcon(styledAttributes.getResourceId(
2661 R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
2662 setAlphabeticShortcut('c').
2663 setShowAsAction(
2664 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2665 }
2666
2667 if (mTextView.canPaste()) {
2668 menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste).
2669 setIcon(styledAttributes.getResourceId(
2670 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
2671 setAlphabeticShortcut('v').
2672 setShowAsAction(
2673 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2674 }
2675
2676 styledAttributes.recycle();
2677
2678 if (mCustomSelectionActionModeCallback != null) {
2679 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
2680 // The custom mode can choose to cancel the action mode
2681 return false;
2682 }
2683 }
2684
2685 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
2686 getSelectionController().show();
2687 return true;
2688 } else {
2689 return false;
2690 }
2691 }
2692
2693 @Override
2694 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
2695 if (mCustomSelectionActionModeCallback != null) {
2696 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
2697 }
2698 return true;
2699 }
2700
2701 @Override
2702 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
2703 if (mCustomSelectionActionModeCallback != null &&
2704 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
2705 return true;
2706 }
2707 return mTextView.onTextContextMenuItem(item.getItemId());
2708 }
2709
2710 @Override
2711 public void onDestroyActionMode(ActionMode mode) {
2712 if (mCustomSelectionActionModeCallback != null) {
2713 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
2714 }
2715 Selection.setSelection((Spannable) mTextView.getText(), mTextView.getSelectionEnd());
2716
2717 if (mSelectionModifierCursorController != null) {
2718 mSelectionModifierCursorController.hide();
2719 }
2720
2721 mSelectionActionMode = null;
2722 }
2723 }
2724
2725 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
2726 private static final int POPUP_TEXT_LAYOUT =
2727 com.android.internal.R.layout.text_edit_action_popup_text;
2728 private TextView mPasteTextView;
2729 private TextView mReplaceTextView;
2730
2731 @Override
2732 protected void createPopupWindow() {
2733 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2734 com.android.internal.R.attr.textSelectHandleWindowStyle);
2735 mPopupWindow.setClippingEnabled(true);
2736 }
2737
2738 @Override
2739 protected void initContentView() {
2740 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2741 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2742 mContentView = linearLayout;
2743 mContentView.setBackgroundResource(
2744 com.android.internal.R.drawable.text_edit_paste_window);
2745
2746 LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
2747 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2748
2749 LayoutParams wrapContent = new LayoutParams(
2750 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2751
2752 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2753 mPasteTextView.setLayoutParams(wrapContent);
2754 mContentView.addView(mPasteTextView);
2755 mPasteTextView.setText(com.android.internal.R.string.paste);
2756 mPasteTextView.setOnClickListener(this);
2757
2758 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2759 mReplaceTextView.setLayoutParams(wrapContent);
2760 mContentView.addView(mReplaceTextView);
2761 mReplaceTextView.setText(com.android.internal.R.string.replace);
2762 mReplaceTextView.setOnClickListener(this);
2763 }
2764
2765 @Override
2766 public void show() {
2767 boolean canPaste = mTextView.canPaste();
2768 boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
2769 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
2770 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
2771
2772 if (!canPaste && !canSuggest) return;
2773
2774 super.show();
2775 }
2776
2777 @Override
2778 public void onClick(View view) {
2779 if (view == mPasteTextView && mTextView.canPaste()) {
2780 mTextView.onTextContextMenuItem(TextView.ID_PASTE);
2781 hide();
2782 } else if (view == mReplaceTextView) {
2783 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2784 stopSelectionActionMode();
2785 Selection.setSelection((Spannable) mTextView.getText(), middle);
2786 showSuggestions();
2787 }
2788 }
2789
2790 @Override
2791 protected int getTextOffset() {
2792 return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2793 }
2794
2795 @Override
2796 protected int getVerticalLocalPosition(int line) {
2797 return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight();
2798 }
2799
2800 @Override
2801 protected int clipVertically(int positionY) {
2802 if (positionY < 0) {
2803 final int offset = getTextOffset();
2804 final Layout layout = mTextView.getLayout();
2805 final int line = layout.getLineForOffset(offset);
2806 positionY += layout.getLineBottom(line) - layout.getLineTop(line);
2807 positionY += mContentView.getMeasuredHeight();
2808
2809 // Assumes insertion and selection handles share the same height
2810 final Drawable handle = mTextView.getResources().getDrawable(
2811 mTextView.mTextSelectHandleRes);
2812 positionY += handle.getIntrinsicHeight();
2813 }
2814
2815 return positionY;
2816 }
2817 }
2818
2819 private abstract class HandleView extends View implements TextViewPositionListener {
2820 protected Drawable mDrawable;
2821 protected Drawable mDrawableLtr;
2822 protected Drawable mDrawableRtl;
2823 private final PopupWindow mContainer;
2824 // Position with respect to the parent TextView
2825 private int mPositionX, mPositionY;
2826 private boolean mIsDragging;
2827 // Offset from touch position to mPosition
2828 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
2829 protected int mHotspotX;
2830 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
2831 private float mTouchOffsetY;
2832 // Where the touch position should be on the handle to ensure a maximum cursor visibility
2833 private float mIdealVerticalOffset;
2834 // Parent's (TextView) previous position in window
2835 private int mLastParentX, mLastParentY;
2836 // Transient action popup window for Paste and Replace actions
2837 protected ActionPopupWindow mActionPopupWindow;
2838 // Previous text character offset
2839 private int mPreviousOffset = -1;
2840 // Previous text character offset
2841 private boolean mPositionHasChanged = true;
2842 // Used to delay the appearance of the action popup window
2843 private Runnable mActionPopupShower;
2844
2845 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
2846 super(mTextView.getContext());
2847 mContainer = new PopupWindow(mTextView.getContext(), null,
2848 com.android.internal.R.attr.textSelectHandleWindowStyle);
2849 mContainer.setSplitTouchEnabled(true);
2850 mContainer.setClippingEnabled(false);
2851 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
2852 mContainer.setContentView(this);
2853
2854 mDrawableLtr = drawableLtr;
2855 mDrawableRtl = drawableRtl;
2856
2857 updateDrawable();
2858
2859 final int handleHeight = mDrawable.getIntrinsicHeight();
2860 mTouchOffsetY = -0.3f * handleHeight;
2861 mIdealVerticalOffset = 0.7f * handleHeight;
2862 }
2863
2864 protected void updateDrawable() {
2865 final int offset = getCurrentCursorOffset();
2866 final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
2867 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
2868 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
2869 }
2870
2871 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
2872
2873 // Touch-up filter: number of previous positions remembered
2874 private static final int HISTORY_SIZE = 5;
2875 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
2876 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
2877 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
2878 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
2879 private int mPreviousOffsetIndex = 0;
2880 private int mNumberPreviousOffsets = 0;
2881
2882 private void startTouchUpFilter(int offset) {
2883 mNumberPreviousOffsets = 0;
2884 addPositionToTouchUpFilter(offset);
2885 }
2886
2887 private void addPositionToTouchUpFilter(int offset) {
2888 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
2889 mPreviousOffsets[mPreviousOffsetIndex] = offset;
2890 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
2891 mNumberPreviousOffsets++;
2892 }
2893
2894 private void filterOnTouchUp() {
2895 final long now = SystemClock.uptimeMillis();
2896 int i = 0;
2897 int index = mPreviousOffsetIndex;
2898 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
2899 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
2900 i++;
2901 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
2902 }
2903
2904 if (i > 0 && i < iMax &&
2905 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
2906 positionAtCursorOffset(mPreviousOffsets[index], false);
2907 }
2908 }
2909
2910 public boolean offsetHasBeenChanged() {
2911 return mNumberPreviousOffsets > 1;
2912 }
2913
2914 @Override
2915 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
2916 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
2917 }
2918
2919 public void show() {
2920 if (isShowing()) return;
2921
2922 getPositionListener().addSubscriber(this, true /* local position may change */);
2923
2924 // Make sure the offset is always considered new, even when focusing at same position
2925 mPreviousOffset = -1;
2926 positionAtCursorOffset(getCurrentCursorOffset(), false);
2927
2928 hideActionPopupWindow();
2929 }
2930
2931 protected void dismiss() {
2932 mIsDragging = false;
2933 mContainer.dismiss();
2934 onDetached();
2935 }
2936
2937 public void hide() {
2938 dismiss();
2939
2940 getPositionListener().removeSubscriber(this);
2941 }
2942
2943 void showActionPopupWindow(int delay) {
2944 if (mActionPopupWindow == null) {
2945 mActionPopupWindow = new ActionPopupWindow();
2946 }
2947 if (mActionPopupShower == null) {
2948 mActionPopupShower = new Runnable() {
2949 public void run() {
2950 mActionPopupWindow.show();
2951 }
2952 };
2953 } else {
2954 mTextView.removeCallbacks(mActionPopupShower);
2955 }
2956 mTextView.postDelayed(mActionPopupShower, delay);
2957 }
2958
2959 protected void hideActionPopupWindow() {
2960 if (mActionPopupShower != null) {
2961 mTextView.removeCallbacks(mActionPopupShower);
2962 }
2963 if (mActionPopupWindow != null) {
2964 mActionPopupWindow.hide();
2965 }
2966 }
2967
2968 public boolean isShowing() {
2969 return mContainer.isShowing();
2970 }
2971
2972 private boolean isVisible() {
2973 // Always show a dragging handle.
2974 if (mIsDragging) {
2975 return true;
2976 }
2977
2978 if (mTextView.isInBatchEditMode()) {
2979 return false;
2980 }
2981
2982 return isPositionVisible(mPositionX + mHotspotX, mPositionY);
2983 }
2984
2985 public abstract int getCurrentCursorOffset();
2986
2987 protected abstract void updateSelection(int offset);
2988
2989 public abstract void updatePosition(float x, float y);
2990
2991 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
2992 // A HandleView relies on the layout, which may be nulled by external methods
2993 Layout layout = mTextView.getLayout();
2994 if (layout == null) {
2995 // Will update controllers' state, hiding them and stopping selection mode if needed
2996 prepareCursorControllers();
2997 return;
2998 }
2999
3000 boolean offsetChanged = offset != mPreviousOffset;
3001 if (offsetChanged || parentScrolled) {
3002 if (offsetChanged) {
3003 updateSelection(offset);
3004 addPositionToTouchUpFilter(offset);
3005 }
3006 final int line = layout.getLineForOffset(offset);
3007
3008 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
3009 mPositionY = layout.getLineBottom(line);
3010
3011 // Take TextView's padding and scroll into account.
3012 mPositionX += mTextView.viewportToContentHorizontalOffset();
3013 mPositionY += mTextView.viewportToContentVerticalOffset();
3014
3015 mPreviousOffset = offset;
3016 mPositionHasChanged = true;
3017 }
3018 }
3019
3020 public void updatePosition(int parentPositionX, int parentPositionY,
3021 boolean parentPositionChanged, boolean parentScrolled) {
3022 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
3023 if (parentPositionChanged || mPositionHasChanged) {
3024 if (mIsDragging) {
3025 // Update touchToWindow offset in case of parent scrolling while dragging
3026 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
3027 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
3028 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
3029 mLastParentX = parentPositionX;
3030 mLastParentY = parentPositionY;
3031 }
3032
3033 onHandleMoved();
3034 }
3035
3036 if (isVisible()) {
3037 final int positionX = parentPositionX + mPositionX;
3038 final int positionY = parentPositionY + mPositionY;
3039 if (isShowing()) {
3040 mContainer.update(positionX, positionY, -1, -1);
3041 } else {
3042 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3043 positionX, positionY);
3044 }
3045 } else {
3046 if (isShowing()) {
3047 dismiss();
3048 }
3049 }
3050
3051 mPositionHasChanged = false;
3052 }
3053 }
3054
3055 @Override
3056 protected void onDraw(Canvas c) {
3057 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
3058 mDrawable.draw(c);
3059 }
3060
3061 @Override
3062 public boolean onTouchEvent(MotionEvent ev) {
3063 switch (ev.getActionMasked()) {
3064 case MotionEvent.ACTION_DOWN: {
3065 startTouchUpFilter(getCurrentCursorOffset());
3066 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
3067 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
3068
3069 final PositionListener positionListener = getPositionListener();
3070 mLastParentX = positionListener.getPositionX();
3071 mLastParentY = positionListener.getPositionY();
3072 mIsDragging = true;
3073 break;
3074 }
3075
3076 case MotionEvent.ACTION_MOVE: {
3077 final float rawX = ev.getRawX();
3078 final float rawY = ev.getRawY();
3079
3080 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
3081 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
3082 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
3083 float newVerticalOffset;
3084 if (previousVerticalOffset < mIdealVerticalOffset) {
3085 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
3086 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
3087 } else {
3088 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
3089 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
3090 }
3091 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
3092
3093 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
3094 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
3095
3096 updatePosition(newPosX, newPosY);
3097 break;
3098 }
3099
3100 case MotionEvent.ACTION_UP:
3101 filterOnTouchUp();
3102 mIsDragging = false;
3103 break;
3104
3105 case MotionEvent.ACTION_CANCEL:
3106 mIsDragging = false;
3107 break;
3108 }
3109 return true;
3110 }
3111
3112 public boolean isDragging() {
3113 return mIsDragging;
3114 }
3115
3116 void onHandleMoved() {
3117 hideActionPopupWindow();
3118 }
3119
3120 public void onDetached() {
3121 hideActionPopupWindow();
3122 }
3123 }
3124
3125 private class InsertionHandleView extends HandleView {
3126 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
3127 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
3128
3129 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
3130 private float mDownPositionX, mDownPositionY;
3131 private Runnable mHider;
3132
3133 public InsertionHandleView(Drawable drawable) {
3134 super(drawable, drawable);
3135 }
3136
3137 @Override
3138 public void show() {
3139 super.show();
3140
3141 final long durationSinceCutOrCopy =
3142 SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME;
3143 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
3144 showActionPopupWindow(0);
3145 }
3146
3147 hideAfterDelay();
3148 }
3149
3150 public void showWithActionPopup() {
3151 show();
3152 showActionPopupWindow(0);
3153 }
3154
3155 private void hideAfterDelay() {
3156 if (mHider == null) {
3157 mHider = new Runnable() {
3158 public void run() {
3159 hide();
3160 }
3161 };
3162 } else {
3163 removeHiderCallback();
3164 }
3165 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
3166 }
3167
3168 private void removeHiderCallback() {
3169 if (mHider != null) {
3170 mTextView.removeCallbacks(mHider);
3171 }
3172 }
3173
3174 @Override
3175 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3176 return drawable.getIntrinsicWidth() / 2;
3177 }
3178
3179 @Override
3180 public boolean onTouchEvent(MotionEvent ev) {
3181 final boolean result = super.onTouchEvent(ev);
3182
3183 switch (ev.getActionMasked()) {
3184 case MotionEvent.ACTION_DOWN:
3185 mDownPositionX = ev.getRawX();
3186 mDownPositionY = ev.getRawY();
3187 break;
3188
3189 case MotionEvent.ACTION_UP:
3190 if (!offsetHasBeenChanged()) {
3191 final float deltaX = mDownPositionX - ev.getRawX();
3192 final float deltaY = mDownPositionY - ev.getRawY();
3193 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3194
3195 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3196 mTextView.getContext());
3197 final int touchSlop = viewConfiguration.getScaledTouchSlop();
3198
3199 if (distanceSquared < touchSlop * touchSlop) {
3200 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
3201 // Tapping on the handle dismisses the displayed action popup
3202 mActionPopupWindow.hide();
3203 } else {
3204 showWithActionPopup();
3205 }
3206 }
3207 }
3208 hideAfterDelay();
3209 break;
3210
3211 case MotionEvent.ACTION_CANCEL:
3212 hideAfterDelay();
3213 break;
3214
3215 default:
3216 break;
3217 }
3218
3219 return result;
3220 }
3221
3222 @Override
3223 public int getCurrentCursorOffset() {
3224 return mTextView.getSelectionStart();
3225 }
3226
3227 @Override
3228 public void updateSelection(int offset) {
3229 Selection.setSelection((Spannable) mTextView.getText(), offset);
3230 }
3231
3232 @Override
3233 public void updatePosition(float x, float y) {
3234 positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
3235 }
3236
3237 @Override
3238 void onHandleMoved() {
3239 super.onHandleMoved();
3240 removeHiderCallback();
3241 }
3242
3243 @Override
3244 public void onDetached() {
3245 super.onDetached();
3246 removeHiderCallback();
3247 }
3248 }
3249
3250 private class SelectionStartHandleView extends HandleView {
3251
3252 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3253 super(drawableLtr, drawableRtl);
3254 }
3255
3256 @Override
3257 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3258 if (isRtlRun) {
3259 return drawable.getIntrinsicWidth() / 4;
3260 } else {
3261 return (drawable.getIntrinsicWidth() * 3) / 4;
3262 }
3263 }
3264
3265 @Override
3266 public int getCurrentCursorOffset() {
3267 return mTextView.getSelectionStart();
3268 }
3269
3270 @Override
3271 public void updateSelection(int offset) {
3272 Selection.setSelection((Spannable) mTextView.getText(), offset,
3273 mTextView.getSelectionEnd());
3274 updateDrawable();
3275 }
3276
3277 @Override
3278 public void updatePosition(float x, float y) {
3279 int offset = mTextView.getOffsetForPosition(x, y);
3280
3281 // Handles can not cross and selection is at least one character
3282 final int selectionEnd = mTextView.getSelectionEnd();
3283 if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
3284
3285 positionAtCursorOffset(offset, false);
3286 }
3287
3288 public ActionPopupWindow getActionPopupWindow() {
3289 return mActionPopupWindow;
3290 }
3291 }
3292
3293 private class SelectionEndHandleView extends HandleView {
3294
3295 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3296 super(drawableLtr, drawableRtl);
3297 }
3298
3299 @Override
3300 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3301 if (isRtlRun) {
3302 return (drawable.getIntrinsicWidth() * 3) / 4;
3303 } else {
3304 return drawable.getIntrinsicWidth() / 4;
3305 }
3306 }
3307
3308 @Override
3309 public int getCurrentCursorOffset() {
3310 return mTextView.getSelectionEnd();
3311 }
3312
3313 @Override
3314 public void updateSelection(int offset) {
3315 Selection.setSelection((Spannable) mTextView.getText(),
3316 mTextView.getSelectionStart(), offset);
3317 updateDrawable();
3318 }
3319
3320 @Override
3321 public void updatePosition(float x, float y) {
3322 int offset = mTextView.getOffsetForPosition(x, y);
3323
3324 // Handles can not cross and selection is at least one character
3325 final int selectionStart = mTextView.getSelectionStart();
3326 if (offset <= selectionStart) {
3327 offset = Math.min(selectionStart + 1, mTextView.getText().length());
3328 }
3329
3330 positionAtCursorOffset(offset, false);
3331 }
3332
3333 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
3334 mActionPopupWindow = actionPopupWindow;
3335 }
3336 }
3337
3338 /**
3339 * A CursorController instance can be used to control a cursor in the text.
3340 */
3341 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
3342 /**
3343 * Makes the cursor controller visible on screen.
3344 * See also {@link #hide()}.
3345 */
3346 public void show();
3347
3348 /**
3349 * Hide the cursor controller from screen.
3350 * See also {@link #show()}.
3351 */
3352 public void hide();
3353
3354 /**
3355 * Called when the view is detached from window. Perform house keeping task, such as
3356 * stopping Runnable thread that would otherwise keep a reference on the context, thus
3357 * preventing the activity from being recycled.
3358 */
3359 public void onDetached();
3360 }
3361
3362 private class InsertionPointCursorController implements CursorController {
3363 private InsertionHandleView mHandle;
3364
3365 public void show() {
3366 getHandle().show();
3367 }
3368
3369 public void showWithActionPopup() {
3370 getHandle().showWithActionPopup();
3371 }
3372
3373 public void hide() {
3374 if (mHandle != null) {
3375 mHandle.hide();
3376 }
3377 }
3378
3379 public void onTouchModeChanged(boolean isInTouchMode) {
3380 if (!isInTouchMode) {
3381 hide();
3382 }
3383 }
3384
3385 private InsertionHandleView getHandle() {
3386 if (mSelectHandleCenter == null) {
3387 mSelectHandleCenter = mTextView.getResources().getDrawable(
3388 mTextView.mTextSelectHandleRes);
3389 }
3390 if (mHandle == null) {
3391 mHandle = new InsertionHandleView(mSelectHandleCenter);
3392 }
3393 return mHandle;
3394 }
3395
3396 @Override
3397 public void onDetached() {
3398 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3399 observer.removeOnTouchModeChangeListener(this);
3400
3401 if (mHandle != null) mHandle.onDetached();
3402 }
3403 }
3404
3405 class SelectionModifierCursorController implements CursorController {
3406 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
3407 // The cursor controller handles, lazily created when shown.
3408 private SelectionStartHandleView mStartHandle;
3409 private SelectionEndHandleView mEndHandle;
3410 // The offsets of that last touch down event. Remembered to start selection there.
3411 private int mMinTouchOffset, mMaxTouchOffset;
3412
3413 // Double tap detection
3414 private long mPreviousTapUpTime = 0;
3415 private float mDownPositionX, mDownPositionY;
3416 private boolean mGestureStayedInTapRegion;
3417
3418 SelectionModifierCursorController() {
3419 resetTouchOffsets();
3420 }
3421
3422 public void show() {
3423 if (mTextView.isInBatchEditMode()) {
3424 return;
3425 }
3426 initDrawables();
3427 initHandles();
3428 hideInsertionPointCursorController();
3429 }
3430
3431 private void initDrawables() {
3432 if (mSelectHandleLeft == null) {
3433 mSelectHandleLeft = mTextView.getContext().getResources().getDrawable(
3434 mTextView.mTextSelectHandleLeftRes);
3435 }
3436 if (mSelectHandleRight == null) {
3437 mSelectHandleRight = mTextView.getContext().getResources().getDrawable(
3438 mTextView.mTextSelectHandleRightRes);
3439 }
3440 }
3441
3442 private void initHandles() {
3443 // Lazy object creation has to be done before updatePosition() is called.
3444 if (mStartHandle == null) {
3445 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
3446 }
3447 if (mEndHandle == null) {
3448 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
3449 }
3450
3451 mStartHandle.show();
3452 mEndHandle.show();
3453
3454 // Make sure both left and right handles share the same ActionPopupWindow (so that
3455 // moving any of the handles hides the action popup).
3456 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
3457 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
3458
3459 hideInsertionPointCursorController();
3460 }
3461
3462 public void hide() {
3463 if (mStartHandle != null) mStartHandle.hide();
3464 if (mEndHandle != null) mEndHandle.hide();
3465 }
3466
3467 public void onTouchEvent(MotionEvent event) {
3468 // This is done even when the View does not have focus, so that long presses can start
3469 // selection and tap can move cursor from this tap position.
3470 switch (event.getActionMasked()) {
3471 case MotionEvent.ACTION_DOWN:
3472 final float x = event.getX();
3473 final float y = event.getY();
3474
3475 // Remember finger down position, to be able to start selection from there
3476 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
3477
3478 // Double tap detection
3479 if (mGestureStayedInTapRegion) {
3480 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
3481 if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
3482 final float deltaX = x - mDownPositionX;
3483 final float deltaY = y - mDownPositionY;
3484 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3485
3486 ViewConfiguration viewConfiguration = ViewConfiguration.get(
3487 mTextView.getContext());
3488 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
3489 boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
3490
3491 if (stayedInArea && isPositionOnText(x, y)) {
3492 startSelectionActionMode();
3493 mDiscardNextActionUp = true;
3494 }
3495 }
3496 }
3497
3498 mDownPositionX = x;
3499 mDownPositionY = y;
3500 mGestureStayedInTapRegion = true;
3501 break;
3502
3503 case MotionEvent.ACTION_POINTER_DOWN:
3504 case MotionEvent.ACTION_POINTER_UP:
3505 // Handle multi-point gestures. Keep min and max offset positions.
3506 // Only activated for devices that correctly handle multi-touch.
3507 if (mTextView.getContext().getPackageManager().hasSystemFeature(
3508 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
3509 updateMinAndMaxOffsets(event);
3510 }
3511 break;
3512
3513 case MotionEvent.ACTION_MOVE:
3514 if (mGestureStayedInTapRegion) {
3515 final float deltaX = event.getX() - mDownPositionX;
3516 final float deltaY = event.getY() - mDownPositionY;
3517 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3518
3519 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3520 mTextView.getContext());
3521 int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
3522
3523 if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
3524 mGestureStayedInTapRegion = false;
3525 }
3526 }
3527 break;
3528
3529 case MotionEvent.ACTION_UP:
3530 mPreviousTapUpTime = SystemClock.uptimeMillis();
3531 break;
3532 }
3533 }
3534
3535 /**
3536 * @param event
3537 */
3538 private void updateMinAndMaxOffsets(MotionEvent event) {
3539 int pointerCount = event.getPointerCount();
3540 for (int index = 0; index < pointerCount; index++) {
3541 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
3542 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
3543 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
3544 }
3545 }
3546
3547 public int getMinTouchOffset() {
3548 return mMinTouchOffset;
3549 }
3550
3551 public int getMaxTouchOffset() {
3552 return mMaxTouchOffset;
3553 }
3554
3555 public void resetTouchOffsets() {
3556 mMinTouchOffset = mMaxTouchOffset = -1;
3557 }
3558
3559 /**
3560 * @return true iff this controller is currently used to move the selection start.
3561 */
3562 public boolean isSelectionStartDragged() {
3563 return mStartHandle != null && mStartHandle.isDragging();
3564 }
3565
3566 public void onTouchModeChanged(boolean isInTouchMode) {
3567 if (!isInTouchMode) {
3568 hide();
3569 }
3570 }
3571
3572 @Override
3573 public void onDetached() {
3574 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3575 observer.removeOnTouchModeChangeListener(this);
3576
3577 if (mStartHandle != null) mStartHandle.onDetached();
3578 if (mEndHandle != null) mEndHandle.onDetached();
3579 }
3580 }
3581
3582 private class CorrectionHighlighter {
3583 private final Path mPath = new Path();
3584 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
3585 private int mStart, mEnd;
3586 private long mFadingStartTime;
3587 private RectF mTempRectF;
3588 private final static int FADE_OUT_DURATION = 400;
3589
3590 public CorrectionHighlighter() {
3591 mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
3592 applicationScale);
3593 mPaint.setStyle(Paint.Style.FILL);
3594 }
3595
3596 public void highlight(CorrectionInfo info) {
3597 mStart = info.getOffset();
3598 mEnd = mStart + info.getNewText().length();
3599 mFadingStartTime = SystemClock.uptimeMillis();
3600
3601 if (mStart < 0 || mEnd < 0) {
3602 stopAnimation();
3603 }
3604 }
3605
3606 public void draw(Canvas canvas, int cursorOffsetVertical) {
3607 if (updatePath() && updatePaint()) {
3608 if (cursorOffsetVertical != 0) {
3609 canvas.translate(0, cursorOffsetVertical);
3610 }
3611
3612 canvas.drawPath(mPath, mPaint);
3613
3614 if (cursorOffsetVertical != 0) {
3615 canvas.translate(0, -cursorOffsetVertical);
3616 }
3617 invalidate(true); // TODO invalidate cursor region only
3618 } else {
3619 stopAnimation();
3620 invalidate(false); // TODO invalidate cursor region only
3621 }
3622 }
3623
3624 private boolean updatePaint() {
3625 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
3626 if (duration > FADE_OUT_DURATION) return false;
3627
3628 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
3629 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
3630 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
3631 ((int) (highlightColorAlpha * coef) << 24);
3632 mPaint.setColor(color);
3633 return true;
3634 }
3635
3636 private boolean updatePath() {
3637 final Layout layout = mTextView.getLayout();
3638 if (layout == null) return false;
3639
3640 // Update in case text is edited while the animation is run
3641 final int length = mTextView.getText().length();
3642 int start = Math.min(length, mStart);
3643 int end = Math.min(length, mEnd);
3644
3645 mPath.reset();
3646 layout.getSelectionPath(start, end, mPath);
3647 return true;
3648 }
3649
3650 private void invalidate(boolean delayed) {
3651 if (mTextView.getLayout() == null) return;
3652
3653 if (mTempRectF == null) mTempRectF = new RectF();
3654 mPath.computeBounds(mTempRectF, false);
3655
3656 int left = mTextView.getCompoundPaddingLeft();
3657 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
3658
3659 if (delayed) {
3660 mTextView.postInvalidateOnAnimation(
3661 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
3662 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
3663 } else {
3664 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
3665 (int) mTempRectF.right, (int) mTempRectF.bottom);
3666 }
3667 }
3668
3669 private void stopAnimation() {
3670 Editor.this.mCorrectionHighlighter = null;
3671 }
3672 }
3673
3674 private static class ErrorPopup extends PopupWindow {
3675 private boolean mAbove = false;
3676 private final TextView mView;
3677 private int mPopupInlineErrorBackgroundId = 0;
3678 private int mPopupInlineErrorAboveBackgroundId = 0;
3679
3680 ErrorPopup(TextView v, int width, int height) {
3681 super(v, width, height);
3682 mView = v;
3683 // Make sure the TextView has a background set as it will be used the first time it is
3684 // shown and positionned. Initialized with below background, which should have
3685 // dimensions identical to the above version for this to work (and is more likely).
3686 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3687 com.android.internal.R.styleable.Theme_errorMessageBackground);
3688 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
3689 }
3690
3691 void fixDirection(boolean above) {
3692 mAbove = above;
3693
3694 if (above) {
3695 mPopupInlineErrorAboveBackgroundId =
3696 getResourceId(mPopupInlineErrorAboveBackgroundId,
3697 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
3698 } else {
3699 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3700 com.android.internal.R.styleable.Theme_errorMessageBackground);
3701 }
3702
3703 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
3704 mPopupInlineErrorBackgroundId);
3705 }
3706
3707 private int getResourceId(int currentId, int index) {
3708 if (currentId == 0) {
3709 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
3710 R.styleable.Theme);
3711 currentId = styledAttributes.getResourceId(index, 0);
3712 styledAttributes.recycle();
3713 }
3714 return currentId;
3715 }
3716
3717 @Override
3718 public void update(int x, int y, int w, int h, boolean force) {
3719 super.update(x, y, w, h, force);
3720
3721 boolean above = isAboveAnchor();
3722 if (above != mAbove) {
3723 fixDirection(above);
3724 }
3725 }
3726 }
3727
3728 static class InputContentType {
3729 int imeOptions = EditorInfo.IME_NULL;
3730 String privateImeOptions;
3731 CharSequence imeActionLabel;
3732 int imeActionId;
3733 Bundle extras;
3734 OnEditorActionListener onEditorActionListener;
3735 boolean enterDown;
3736 }
3737
3738 static class InputMethodState {
3739 Rect mCursorRectInWindow = new Rect();
3740 RectF mTmpRectF = new RectF();
3741 float[] mTmpOffset = new float[2];
3742 ExtractedTextRequest mExtracting;
3743 final ExtractedText mTmpExtracted = new ExtractedText();
3744 int mBatchEditNesting;
3745 boolean mCursorChanged;
3746 boolean mSelectionModeChanged;
3747 boolean mContentChanged;
3748 int mChangedStart, mChangedEnd, mChangedDelta;
3749 }
3750}