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