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