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