blob: 50d28abc25adcf3a0c8d99a3b153137ae82286d8 [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
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +0900208 private final UserDictionaryListener mUserDictionaryListener = new UserDictionaryListener();
209
Gilles Debunned88876a2012-03-16 17:34:04 -0700210 Editor(TextView textView) {
211 mTextView = textView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700212 }
213
214 void onAttachedToWindow() {
215 if (mShowErrorAfterAttach) {
216 showError();
217 mShowErrorAfterAttach = false;
218 }
Adam Powell057a5852012-05-11 10:28:38 -0700219 mTemporaryDetach = false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700220
221 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
222 // No need to create the controller.
223 // The get method will add the listener on controller creation.
224 if (mInsertionPointCursorController != null) {
225 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
226 }
227 if (mSelectionModifierCursorController != null) {
Adam Powell057a5852012-05-11 10:28:38 -0700228 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunned88876a2012-03-16 17:34:04 -0700229 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
230 }
231 updateSpellCheckSpans(0, mTextView.getText().length(),
232 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700233
234 if (mTextView.hasTransientState() &&
235 mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
236 // Since transient state is reference counted make sure it stays matched
237 // with our own calls to it for managing selection.
238 // The action mode callback will set this back again when/if the action mode starts.
239 mTextView.setHasTransientState(false);
240
241 // We had an active selection from before, start the selection mode.
242 startSelectionActionMode();
243 }
Gilles Debunned88876a2012-03-16 17:34:04 -0700244 }
245
246 void onDetachedFromWindow() {
247 if (mError != null) {
248 hideError();
249 }
250
251 if (mBlink != null) {
252 mBlink.removeCallbacks(mBlink);
253 }
254
255 if (mInsertionPointCursorController != null) {
256 mInsertionPointCursorController.onDetached();
257 }
258
259 if (mSelectionModifierCursorController != null) {
260 mSelectionModifierCursorController.onDetached();
261 }
262
263 if (mShowSuggestionRunnable != null) {
264 mTextView.removeCallbacks(mShowSuggestionRunnable);
265 }
266
267 invalidateTextDisplayList();
268
269 if (mSpellChecker != null) {
270 mSpellChecker.closeSession();
271 // Forces the creation of a new SpellChecker next time this window is created.
272 // Will handle the cases where the settings has been changed in the meantime.
273 mSpellChecker = null;
274 }
275
Adam Powell057a5852012-05-11 10:28:38 -0700276 mPreserveDetachedSelection = true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700277 hideControllers();
Adam Powell057a5852012-05-11 10:28:38 -0700278 mPreserveDetachedSelection = false;
279 mTemporaryDetach = false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700280 }
281
282 private void showError() {
283 if (mTextView.getWindowToken() == null) {
284 mShowErrorAfterAttach = true;
285 return;
286 }
287
288 if (mErrorPopup == null) {
289 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
290 final TextView err = (TextView) inflater.inflate(
291 com.android.internal.R.layout.textview_hint, null);
292
293 final float scale = mTextView.getResources().getDisplayMetrics().density;
294 mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
295 mErrorPopup.setFocusable(false);
296 // The user is entering text, so the input method is needed. We
297 // don't want the popup to be displayed on top of it.
298 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
299 }
300
301 TextView tv = (TextView) mErrorPopup.getContentView();
302 chooseSize(mErrorPopup, mError, tv);
303 tv.setText(mError);
304
305 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
306 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
307 }
308
309 public void setError(CharSequence error, Drawable icon) {
310 mError = TextUtils.stringOrSpannedString(error);
311 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800312
Gilles Debunned88876a2012-03-16 17:34:04 -0700313 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800314 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700315 if (mErrorPopup != null) {
316 if (mErrorPopup.isShowing()) {
317 mErrorPopup.dismiss();
318 }
319
320 mErrorPopup = null;
321 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800322
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800323 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800324 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800325 if (mTextView.isFocused()) {
326 showError();
327 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800328 }
329 }
330
331 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800332 Drawables dr = mTextView.mDrawables;
333 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700334 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700335 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800336 dr.setErrorDrawable(icon, mTextView);
337
338 mTextView.resetResolvedDrawables();
339 mTextView.invalidate();
340 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700341 }
342
343 private void hideError() {
344 if (mErrorPopup != null) {
345 if (mErrorPopup.isShowing()) {
346 mErrorPopup.dismiss();
347 }
348 }
349
350 mShowErrorAfterAttach = false;
351 }
352
353 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800354 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700355 * at the middle of the error icon.
356 */
357 private int getErrorX() {
358 /*
359 * The "25" is the distance between the point and the right edge
360 * of the background
361 */
362 final float scale = mTextView.getResources().getDisplayMetrics().density;
363
364 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800365
366 final int layoutDirection = mTextView.getLayoutDirection();
367 int errorX;
368 int offset;
369 switch (layoutDirection) {
370 default:
371 case View.LAYOUT_DIRECTION_LTR:
372 offset = - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
373 errorX = mTextView.getWidth() - mErrorPopup.getWidth() -
374 mTextView.getPaddingRight() + offset;
375 break;
376 case View.LAYOUT_DIRECTION_RTL:
377 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
378 errorX = mTextView.getPaddingLeft() + offset;
379 break;
380 }
381 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700382 }
383
384 /**
385 * Returns the Y offset to make the pointy top of the error point
386 * at the bottom of the error icon.
387 */
388 private int getErrorY() {
389 /*
390 * Compound, not extended, because the icon is not clipped
391 * if the text height is smaller.
392 */
393 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
394 int vspace = mTextView.getBottom() - mTextView.getTop() -
395 mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
396
397 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800398
399 final int layoutDirection = mTextView.getLayoutDirection();
400 int height;
401 switch (layoutDirection) {
402 default:
403 case View.LAYOUT_DIRECTION_LTR:
404 height = (dr != null ? dr.mDrawableHeightRight : 0);
405 break;
406 case View.LAYOUT_DIRECTION_RTL:
407 height = (dr != null ? dr.mDrawableHeightLeft : 0);
408 break;
409 }
410
411 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700412
413 /*
414 * The "2" is the distance between the point and the top edge
415 * of the background.
416 */
417 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800418 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700419 }
420
421 void createInputContentTypeIfNeeded() {
422 if (mInputContentType == null) {
423 mInputContentType = new InputContentType();
424 }
425 }
426
427 void createInputMethodStateIfNeeded() {
428 if (mInputMethodState == null) {
429 mInputMethodState = new InputMethodState();
430 }
431 }
432
433 boolean isCursorVisible() {
434 // The default value is true, even when there is no associated Editor
435 return mCursorVisible && mTextView.isTextEditable();
436 }
437
438 void prepareCursorControllers() {
439 boolean windowSupportsHandles = false;
440
441 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
442 if (params instanceof WindowManager.LayoutParams) {
443 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
444 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
445 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
446 }
447
448 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
449 mInsertionControllerEnabled = enabled && isCursorVisible();
450 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
451
452 if (!mInsertionControllerEnabled) {
453 hideInsertionPointCursorController();
454 if (mInsertionPointCursorController != null) {
455 mInsertionPointCursorController.onDetached();
456 mInsertionPointCursorController = null;
457 }
458 }
459
460 if (!mSelectionControllerEnabled) {
461 stopSelectionActionMode();
462 if (mSelectionModifierCursorController != null) {
463 mSelectionModifierCursorController.onDetached();
464 mSelectionModifierCursorController = null;
465 }
466 }
467 }
468
469 private void hideInsertionPointCursorController() {
470 if (mInsertionPointCursorController != null) {
471 mInsertionPointCursorController.hide();
472 }
473 }
474
475 /**
476 * Hides the insertion controller and stops text selection mode, hiding the selection controller
477 */
478 void hideControllers() {
479 hideCursorControllers();
480 hideSpanControllers();
481 }
482
483 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800484 if (mSpanController != null) {
485 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700486 }
487 }
488
489 private void hideCursorControllers() {
490 if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
491 // Should be done before hide insertion point controller since it triggers a show of it
492 mSuggestionsPopupWindow.hide();
493 }
494 hideInsertionPointCursorController();
495 stopSelectionActionMode();
496 }
497
498 /**
499 * Create new SpellCheckSpans on the modified region.
500 */
501 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900502 // Remove spans whose adjacent characters are text not punctuation
503 mTextView.removeAdjacentSuggestionSpans(start);
504 mTextView.removeAdjacentSuggestionSpans(end);
505
Gilles Debunned88876a2012-03-16 17:34:04 -0700506 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
507 !(mTextView instanceof ExtractEditText)) {
508 if (mSpellChecker == null && createSpellChecker) {
509 mSpellChecker = new SpellChecker(mTextView);
510 }
511 if (mSpellChecker != null) {
512 mSpellChecker.spellCheck(start, end);
513 }
514 }
515 }
516
517 void onScreenStateChanged(int screenState) {
518 switch (screenState) {
519 case View.SCREEN_STATE_ON:
520 resumeBlink();
521 break;
522 case View.SCREEN_STATE_OFF:
523 suspendBlink();
524 break;
525 }
526 }
527
528 private void suspendBlink() {
529 if (mBlink != null) {
530 mBlink.cancel();
531 }
532 }
533
534 private void resumeBlink() {
535 if (mBlink != null) {
536 mBlink.uncancel();
537 makeBlink();
538 }
539 }
540
541 void adjustInputType(boolean password, boolean passwordInputType,
542 boolean webPasswordInputType, boolean numberPasswordInputType) {
543 // mInputType has been set from inputType, possibly modified by mInputMethod.
544 // Specialize mInputType to [web]password if we have a text class and the original input
545 // type was a password.
546 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
547 if (password || passwordInputType) {
548 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
549 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
550 }
551 if (webPasswordInputType) {
552 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
553 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
554 }
555 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
556 if (numberPasswordInputType) {
557 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
558 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
559 }
560 }
561 }
562
563 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
564 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
565 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
566
567 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
568 com.android.internal.R.dimen.textview_error_popup_default_width);
569 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
570 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
571 float max = 0;
572 for (int i = 0; i < l.getLineCount(); i++) {
573 max = Math.max(max, l.getLineWidth(i));
574 }
575
576 /*
577 * Now set the popup size to be big enough for the text plus the border capped
578 * to DEFAULT_MAX_POPUP_WIDTH
579 */
580 pop.setWidth(wid + (int) Math.ceil(max));
581 pop.setHeight(ht + l.getHeight());
582 }
583
584 void setFrame() {
585 if (mErrorPopup != null) {
586 TextView tv = (TextView) mErrorPopup.getContentView();
587 chooseSize(mErrorPopup, mError, tv);
588 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
589 mErrorPopup.getWidth(), mErrorPopup.getHeight());
590 }
591 }
592
593 /**
594 * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state
595 * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have
596 * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
597 */
598 private boolean canSelectText() {
599 return hasSelectionController() && mTextView.getText().length() != 0;
600 }
601
602 /**
603 * It would be better to rely on the input type for everything. A password inputType should have
604 * a password transformation. We should hence use isPasswordInputType instead of this method.
605 *
606 * We should:
607 * - Call setInputType in setKeyListener instead of changing the input type directly (which
608 * would install the correct transformation).
609 * - Refuse the installation of a non-password transformation in setTransformation if the input
610 * type is password.
611 *
612 * However, this is like this for legacy reasons and we cannot break existing apps. This method
613 * is useful since it matches what the user can see (obfuscated text or not).
614 *
615 * @return true if the current transformation method is of the password type.
616 */
617 private boolean hasPasswordTransformationMethod() {
618 return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
619 }
620
621 /**
622 * Adjusts selection to the word under last touch offset.
623 * Return true if the operation was successfully performed.
624 */
625 private boolean selectCurrentWord() {
626 if (!canSelectText()) {
627 return false;
628 }
629
630 if (hasPasswordTransformationMethod()) {
631 // Always select all on a password field.
632 // Cut/copy menu entries are not available for passwords, but being able to select all
633 // is however useful to delete or paste to replace the entire content.
634 return mTextView.selectAllText();
635 }
636
637 int inputType = mTextView.getInputType();
638 int klass = inputType & InputType.TYPE_MASK_CLASS;
639 int variation = inputType & InputType.TYPE_MASK_VARIATION;
640
641 // Specific text field types: select the entire text for these
642 if (klass == InputType.TYPE_CLASS_NUMBER ||
643 klass == InputType.TYPE_CLASS_PHONE ||
644 klass == InputType.TYPE_CLASS_DATETIME ||
645 variation == InputType.TYPE_TEXT_VARIATION_URI ||
646 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
647 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
648 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
649 return mTextView.selectAllText();
650 }
651
652 long lastTouchOffsets = getLastTouchOffsets();
653 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
654 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
655
656 // Safety check in case standard touch event handling has been bypassed
657 if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
658 if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
659
660 int selectionStart, selectionEnd;
661
662 // If a URLSpan (web address, email, phone...) is found at that position, select it.
663 URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
664 getSpans(minOffset, maxOffset, URLSpan.class);
665 if (urlSpans.length >= 1) {
666 URLSpan urlSpan = urlSpans[0];
667 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
668 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
669 } else {
670 final WordIterator wordIterator = getWordIterator();
671 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
672
673 selectionStart = wordIterator.getBeginning(minOffset);
674 selectionEnd = wordIterator.getEnd(maxOffset);
675
676 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
677 selectionStart == selectionEnd) {
678 // Possible when the word iterator does not properly handle the text's language
679 long range = getCharRange(minOffset);
680 selectionStart = TextUtils.unpackRangeStartFromLong(range);
681 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
682 }
683 }
684
685 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
686 return selectionEnd > selectionStart;
687 }
688
689 void onLocaleChanged() {
690 // Will be re-created on demand in getWordIterator with the proper new locale
691 mWordIterator = null;
692 }
693
694 /**
695 * @hide
696 */
697 public WordIterator getWordIterator() {
698 if (mWordIterator == null) {
699 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
700 }
701 return mWordIterator;
702 }
703
704 private long getCharRange(int offset) {
705 final int textLength = mTextView.getText().length();
706 if (offset + 1 < textLength) {
707 final char currentChar = mTextView.getText().charAt(offset);
708 final char nextChar = mTextView.getText().charAt(offset + 1);
709 if (Character.isSurrogatePair(currentChar, nextChar)) {
710 return TextUtils.packRangeInLong(offset, offset + 2);
711 }
712 }
713 if (offset < textLength) {
714 return TextUtils.packRangeInLong(offset, offset + 1);
715 }
716 if (offset - 2 >= 0) {
717 final char previousChar = mTextView.getText().charAt(offset - 1);
718 final char previousPreviousChar = mTextView.getText().charAt(offset - 2);
719 if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
720 return TextUtils.packRangeInLong(offset - 2, offset);
721 }
722 }
723 if (offset - 1 >= 0) {
724 return TextUtils.packRangeInLong(offset - 1, offset);
725 }
726 return TextUtils.packRangeInLong(offset, offset);
727 }
728
729 private boolean touchPositionIsInSelection() {
730 int selectionStart = mTextView.getSelectionStart();
731 int selectionEnd = mTextView.getSelectionEnd();
732
733 if (selectionStart == selectionEnd) {
734 return false;
735 }
736
737 if (selectionStart > selectionEnd) {
738 int tmp = selectionStart;
739 selectionStart = selectionEnd;
740 selectionEnd = tmp;
741 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
742 }
743
744 SelectionModifierCursorController selectionController = getSelectionController();
745 int minOffset = selectionController.getMinTouchOffset();
746 int maxOffset = selectionController.getMaxTouchOffset();
747
748 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
749 }
750
751 private PositionListener getPositionListener() {
752 if (mPositionListener == null) {
753 mPositionListener = new PositionListener();
754 }
755 return mPositionListener;
756 }
757
758 private interface TextViewPositionListener {
759 public void updatePosition(int parentPositionX, int parentPositionY,
760 boolean parentPositionChanged, boolean parentScrolled);
761 }
762
763 private boolean isPositionVisible(int positionX, int positionY) {
764 synchronized (TEMP_POSITION) {
765 final float[] position = TEMP_POSITION;
766 position[0] = positionX;
767 position[1] = positionY;
768 View view = mTextView;
769
770 while (view != null) {
771 if (view != mTextView) {
772 // Local scroll is already taken into account in positionX/Y
773 position[0] -= view.getScrollX();
774 position[1] -= view.getScrollY();
775 }
776
777 if (position[0] < 0 || position[1] < 0 ||
778 position[0] > view.getWidth() || position[1] > view.getHeight()) {
779 return false;
780 }
781
782 if (!view.getMatrix().isIdentity()) {
783 view.getMatrix().mapPoints(position);
784 }
785
786 position[0] += view.getLeft();
787 position[1] += view.getTop();
788
789 final ViewParent parent = view.getParent();
790 if (parent instanceof View) {
791 view = (View) parent;
792 } else {
793 // We've reached the ViewRoot, stop iterating
794 view = null;
795 }
796 }
797 }
798
799 // We've been able to walk up the view hierarchy and the position was never clipped
800 return true;
801 }
802
803 private boolean isOffsetVisible(int offset) {
804 Layout layout = mTextView.getLayout();
805 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.
2710 mUserDictionaryListener.waitForUserDictionaryAdded(
2711 mTextView, originalText, spanStart, spanEnd);
2712 intent.putExtra("listener", new Messenger(mUserDictionaryListener));
Gilles Debunned88876a2012-03-16 17:34:04 -07002713 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
2714 mTextView.getContext().startActivity(intent);
2715 // There is no way to know if the word was indeed added. Re-check.
2716 // TODO The ExtractEditText should remove the span in the original text instead
2717 editable.removeSpan(suggestionInfo.suggestionSpan);
Gilles Debunne2eb70fb2012-04-18 17:57:45 -07002718 Selection.setSelection(editable, spanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002719 updateSpellCheckSpans(spanStart, spanEnd, false);
2720 } else {
2721 // SuggestionSpans are removed by replace: save them before
2722 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2723 SuggestionSpan.class);
2724 final int length = suggestionSpans.length;
2725 int[] suggestionSpansStarts = new int[length];
2726 int[] suggestionSpansEnds = new int[length];
2727 int[] suggestionSpansFlags = new int[length];
2728 for (int i = 0; i < length; i++) {
2729 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2730 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2731 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2732 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2733
2734 // Remove potential misspelled flags
2735 int suggestionSpanFlags = suggestionSpan.getFlags();
2736 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
2737 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2738 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2739 suggestionSpan.setFlags(suggestionSpanFlags);
2740 }
2741 }
2742
2743 final int suggestionStart = suggestionInfo.suggestionStart;
2744 final int suggestionEnd = suggestionInfo.suggestionEnd;
2745 final String suggestion = suggestionInfo.text.subSequence(
2746 suggestionStart, suggestionEnd).toString();
2747 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2748
Luca Zanolin0c96b81f2012-08-29 11:33:12 +01002749 // Notify source IME of the suggestion pick. Do this before
2750 // swaping texts.
2751 suggestionInfo.suggestionSpan.notifySelection(
2752 mTextView.getContext(), originalText, suggestionInfo.suggestionIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07002753
2754 // Swap text content between actual text and Suggestion span
2755 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
2756 suggestions[suggestionInfo.suggestionIndex] = originalText;
2757
2758 // Restore previous SuggestionSpans
2759 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
2760 for (int i = 0; i < length; i++) {
2761 // Only spans that include the modified region make sense after replacement
2762 // Spans partially included in the replaced region are removed, there is no
2763 // way to assign them a valid range after replacement
2764 if (suggestionSpansStarts[i] <= spanStart &&
2765 suggestionSpansEnds[i] >= spanEnd) {
2766 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2767 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
2768 }
2769 }
2770
2771 // Move cursor at the end of the replaced word
2772 final int newCursorPosition = spanEnd + lengthDifference;
2773 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2774 }
2775
2776 hide();
2777 }
2778 }
2779
2780 /**
2781 * An ActionMode Callback class that is used to provide actions while in text selection mode.
2782 *
2783 * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
2784 * on which of these this TextView supports.
2785 */
2786 private class SelectionActionModeCallback implements ActionMode.Callback {
2787
2788 @Override
2789 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
2790 TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes(
2791 com.android.internal.R.styleable.SelectionModeDrawables);
2792
Gilles Debunned88876a2012-03-16 17:34:04 -07002793 mode.setTitle(mTextView.getContext().getString(
2794 com.android.internal.R.string.textSelectionCABTitle));
2795 mode.setSubtitle(null);
2796 mode.setTitleOptionalHint(true);
2797
Gilles Debunned88876a2012-03-16 17:34:04 -07002798 menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
Sungmin Choif0369202013-01-25 21:39:01 +09002799 setIcon(styledAttributes.getResourceId(
2800 R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0)).
Gilles Debunned88876a2012-03-16 17:34:04 -07002801 setAlphabeticShortcut('a').
2802 setShowAsAction(
2803 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2804
2805 if (mTextView.canCut()) {
2806 menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut).
2807 setIcon(styledAttributes.getResourceId(
2808 R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
2809 setAlphabeticShortcut('x').
2810 setShowAsAction(
2811 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2812 }
2813
2814 if (mTextView.canCopy()) {
2815 menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy).
2816 setIcon(styledAttributes.getResourceId(
2817 R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
2818 setAlphabeticShortcut('c').
2819 setShowAsAction(
2820 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2821 }
2822
2823 if (mTextView.canPaste()) {
2824 menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste).
2825 setIcon(styledAttributes.getResourceId(
2826 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
2827 setAlphabeticShortcut('v').
2828 setShowAsAction(
2829 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
2830 }
2831
2832 styledAttributes.recycle();
2833
2834 if (mCustomSelectionActionModeCallback != null) {
2835 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
2836 // The custom mode can choose to cancel the action mode
2837 return false;
2838 }
2839 }
2840
2841 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
2842 getSelectionController().show();
Adam Powell057a5852012-05-11 10:28:38 -07002843 mTextView.setHasTransientState(true);
Gilles Debunned88876a2012-03-16 17:34:04 -07002844 return true;
2845 } else {
2846 return false;
2847 }
2848 }
2849
2850 @Override
2851 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
2852 if (mCustomSelectionActionModeCallback != null) {
2853 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
2854 }
2855 return true;
2856 }
2857
2858 @Override
2859 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
2860 if (mCustomSelectionActionModeCallback != null &&
2861 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
2862 return true;
2863 }
2864 return mTextView.onTextContextMenuItem(item.getItemId());
2865 }
2866
2867 @Override
2868 public void onDestroyActionMode(ActionMode mode) {
2869 if (mCustomSelectionActionModeCallback != null) {
2870 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
2871 }
Adam Powell057a5852012-05-11 10:28:38 -07002872
2873 /*
2874 * If we're ending this mode because we're detaching from a window,
2875 * we still have selection state to preserve. Don't clear it, we'll
2876 * bring back the selection mode when (if) we get reattached.
2877 */
2878 if (!mPreserveDetachedSelection) {
2879 Selection.setSelection((Spannable) mTextView.getText(),
2880 mTextView.getSelectionEnd());
2881 mTextView.setHasTransientState(false);
2882 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002883
2884 if (mSelectionModifierCursorController != null) {
2885 mSelectionModifierCursorController.hide();
2886 }
2887
2888 mSelectionActionMode = null;
2889 }
2890 }
2891
2892 private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
2893 private static final int POPUP_TEXT_LAYOUT =
2894 com.android.internal.R.layout.text_edit_action_popup_text;
2895 private TextView mPasteTextView;
2896 private TextView mReplaceTextView;
2897
2898 @Override
2899 protected void createPopupWindow() {
2900 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2901 com.android.internal.R.attr.textSelectHandleWindowStyle);
2902 mPopupWindow.setClippingEnabled(true);
2903 }
2904
2905 @Override
2906 protected void initContentView() {
2907 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2908 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2909 mContentView = linearLayout;
2910 mContentView.setBackgroundResource(
2911 com.android.internal.R.drawable.text_edit_paste_window);
2912
2913 LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
2914 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2915
2916 LayoutParams wrapContent = new LayoutParams(
2917 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2918
2919 mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2920 mPasteTextView.setLayoutParams(wrapContent);
2921 mContentView.addView(mPasteTextView);
2922 mPasteTextView.setText(com.android.internal.R.string.paste);
2923 mPasteTextView.setOnClickListener(this);
2924
2925 mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2926 mReplaceTextView.setLayoutParams(wrapContent);
2927 mContentView.addView(mReplaceTextView);
2928 mReplaceTextView.setText(com.android.internal.R.string.replace);
2929 mReplaceTextView.setOnClickListener(this);
2930 }
2931
2932 @Override
2933 public void show() {
2934 boolean canPaste = mTextView.canPaste();
2935 boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
2936 mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
2937 mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
2938
2939 if (!canPaste && !canSuggest) return;
2940
2941 super.show();
2942 }
2943
2944 @Override
2945 public void onClick(View view) {
2946 if (view == mPasteTextView && mTextView.canPaste()) {
2947 mTextView.onTextContextMenuItem(TextView.ID_PASTE);
2948 hide();
2949 } else if (view == mReplaceTextView) {
2950 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2951 stopSelectionActionMode();
2952 Selection.setSelection((Spannable) mTextView.getText(), middle);
2953 showSuggestions();
2954 }
2955 }
2956
2957 @Override
2958 protected int getTextOffset() {
2959 return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
2960 }
2961
2962 @Override
2963 protected int getVerticalLocalPosition(int line) {
2964 return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight();
2965 }
2966
2967 @Override
2968 protected int clipVertically(int positionY) {
2969 if (positionY < 0) {
2970 final int offset = getTextOffset();
2971 final Layout layout = mTextView.getLayout();
2972 final int line = layout.getLineForOffset(offset);
2973 positionY += layout.getLineBottom(line) - layout.getLineTop(line);
2974 positionY += mContentView.getMeasuredHeight();
2975
2976 // Assumes insertion and selection handles share the same height
2977 final Drawable handle = mTextView.getResources().getDrawable(
2978 mTextView.mTextSelectHandleRes);
2979 positionY += handle.getIntrinsicHeight();
2980 }
2981
2982 return positionY;
2983 }
2984 }
2985
2986 private abstract class HandleView extends View implements TextViewPositionListener {
2987 protected Drawable mDrawable;
2988 protected Drawable mDrawableLtr;
2989 protected Drawable mDrawableRtl;
2990 private final PopupWindow mContainer;
2991 // Position with respect to the parent TextView
2992 private int mPositionX, mPositionY;
2993 private boolean mIsDragging;
2994 // Offset from touch position to mPosition
2995 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
2996 protected int mHotspotX;
2997 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
2998 private float mTouchOffsetY;
2999 // Where the touch position should be on the handle to ensure a maximum cursor visibility
3000 private float mIdealVerticalOffset;
3001 // Parent's (TextView) previous position in window
3002 private int mLastParentX, mLastParentY;
3003 // Transient action popup window for Paste and Replace actions
3004 protected ActionPopupWindow mActionPopupWindow;
3005 // Previous text character offset
3006 private int mPreviousOffset = -1;
3007 // Previous text character offset
3008 private boolean mPositionHasChanged = true;
3009 // Used to delay the appearance of the action popup window
3010 private Runnable mActionPopupShower;
3011
3012 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
3013 super(mTextView.getContext());
3014 mContainer = new PopupWindow(mTextView.getContext(), null,
3015 com.android.internal.R.attr.textSelectHandleWindowStyle);
3016 mContainer.setSplitTouchEnabled(true);
3017 mContainer.setClippingEnabled(false);
3018 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
3019 mContainer.setContentView(this);
3020
3021 mDrawableLtr = drawableLtr;
3022 mDrawableRtl = drawableRtl;
3023
3024 updateDrawable();
3025
3026 final int handleHeight = mDrawable.getIntrinsicHeight();
3027 mTouchOffsetY = -0.3f * handleHeight;
3028 mIdealVerticalOffset = 0.7f * handleHeight;
3029 }
3030
3031 protected void updateDrawable() {
3032 final int offset = getCurrentCursorOffset();
3033 final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
3034 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
3035 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
3036 }
3037
3038 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
3039
3040 // Touch-up filter: number of previous positions remembered
3041 private static final int HISTORY_SIZE = 5;
3042 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
3043 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
3044 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
3045 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
3046 private int mPreviousOffsetIndex = 0;
3047 private int mNumberPreviousOffsets = 0;
3048
3049 private void startTouchUpFilter(int offset) {
3050 mNumberPreviousOffsets = 0;
3051 addPositionToTouchUpFilter(offset);
3052 }
3053
3054 private void addPositionToTouchUpFilter(int offset) {
3055 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
3056 mPreviousOffsets[mPreviousOffsetIndex] = offset;
3057 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
3058 mNumberPreviousOffsets++;
3059 }
3060
3061 private void filterOnTouchUp() {
3062 final long now = SystemClock.uptimeMillis();
3063 int i = 0;
3064 int index = mPreviousOffsetIndex;
3065 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
3066 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
3067 i++;
3068 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
3069 }
3070
3071 if (i > 0 && i < iMax &&
3072 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
3073 positionAtCursorOffset(mPreviousOffsets[index], false);
3074 }
3075 }
3076
3077 public boolean offsetHasBeenChanged() {
3078 return mNumberPreviousOffsets > 1;
3079 }
3080
3081 @Override
3082 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3083 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
3084 }
3085
3086 public void show() {
3087 if (isShowing()) return;
3088
3089 getPositionListener().addSubscriber(this, true /* local position may change */);
3090
3091 // Make sure the offset is always considered new, even when focusing at same position
3092 mPreviousOffset = -1;
3093 positionAtCursorOffset(getCurrentCursorOffset(), false);
3094
3095 hideActionPopupWindow();
3096 }
3097
3098 protected void dismiss() {
3099 mIsDragging = false;
3100 mContainer.dismiss();
3101 onDetached();
3102 }
3103
3104 public void hide() {
3105 dismiss();
3106
3107 getPositionListener().removeSubscriber(this);
3108 }
3109
3110 void showActionPopupWindow(int delay) {
3111 if (mActionPopupWindow == null) {
3112 mActionPopupWindow = new ActionPopupWindow();
3113 }
3114 if (mActionPopupShower == null) {
3115 mActionPopupShower = new Runnable() {
3116 public void run() {
3117 mActionPopupWindow.show();
3118 }
3119 };
3120 } else {
3121 mTextView.removeCallbacks(mActionPopupShower);
3122 }
3123 mTextView.postDelayed(mActionPopupShower, delay);
3124 }
3125
3126 protected void hideActionPopupWindow() {
3127 if (mActionPopupShower != null) {
3128 mTextView.removeCallbacks(mActionPopupShower);
3129 }
3130 if (mActionPopupWindow != null) {
3131 mActionPopupWindow.hide();
3132 }
3133 }
3134
3135 public boolean isShowing() {
3136 return mContainer.isShowing();
3137 }
3138
3139 private boolean isVisible() {
3140 // Always show a dragging handle.
3141 if (mIsDragging) {
3142 return true;
3143 }
3144
3145 if (mTextView.isInBatchEditMode()) {
3146 return false;
3147 }
3148
3149 return isPositionVisible(mPositionX + mHotspotX, mPositionY);
3150 }
3151
3152 public abstract int getCurrentCursorOffset();
3153
3154 protected abstract void updateSelection(int offset);
3155
3156 public abstract void updatePosition(float x, float y);
3157
3158 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
3159 // A HandleView relies on the layout, which may be nulled by external methods
3160 Layout layout = mTextView.getLayout();
3161 if (layout == null) {
3162 // Will update controllers' state, hiding them and stopping selection mode if needed
3163 prepareCursorControllers();
3164 return;
3165 }
3166
3167 boolean offsetChanged = offset != mPreviousOffset;
3168 if (offsetChanged || parentScrolled) {
3169 if (offsetChanged) {
3170 updateSelection(offset);
3171 addPositionToTouchUpFilter(offset);
3172 }
3173 final int line = layout.getLineForOffset(offset);
3174
3175 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
3176 mPositionY = layout.getLineBottom(line);
3177
3178 // Take TextView's padding and scroll into account.
3179 mPositionX += mTextView.viewportToContentHorizontalOffset();
3180 mPositionY += mTextView.viewportToContentVerticalOffset();
3181
3182 mPreviousOffset = offset;
3183 mPositionHasChanged = true;
3184 }
3185 }
3186
3187 public void updatePosition(int parentPositionX, int parentPositionY,
3188 boolean parentPositionChanged, boolean parentScrolled) {
3189 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
3190 if (parentPositionChanged || mPositionHasChanged) {
3191 if (mIsDragging) {
3192 // Update touchToWindow offset in case of parent scrolling while dragging
3193 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
3194 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
3195 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
3196 mLastParentX = parentPositionX;
3197 mLastParentY = parentPositionY;
3198 }
3199
3200 onHandleMoved();
3201 }
3202
3203 if (isVisible()) {
3204 final int positionX = parentPositionX + mPositionX;
3205 final int positionY = parentPositionY + mPositionY;
3206 if (isShowing()) {
3207 mContainer.update(positionX, positionY, -1, -1);
3208 } else {
3209 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3210 positionX, positionY);
3211 }
3212 } else {
3213 if (isShowing()) {
3214 dismiss();
3215 }
3216 }
3217
3218 mPositionHasChanged = false;
3219 }
3220 }
3221
3222 @Override
3223 protected void onDraw(Canvas c) {
3224 mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
3225 mDrawable.draw(c);
3226 }
3227
3228 @Override
3229 public boolean onTouchEvent(MotionEvent ev) {
3230 switch (ev.getActionMasked()) {
3231 case MotionEvent.ACTION_DOWN: {
3232 startTouchUpFilter(getCurrentCursorOffset());
3233 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
3234 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
3235
3236 final PositionListener positionListener = getPositionListener();
3237 mLastParentX = positionListener.getPositionX();
3238 mLastParentY = positionListener.getPositionY();
3239 mIsDragging = true;
3240 break;
3241 }
3242
3243 case MotionEvent.ACTION_MOVE: {
3244 final float rawX = ev.getRawX();
3245 final float rawY = ev.getRawY();
3246
3247 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
3248 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
3249 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
3250 float newVerticalOffset;
3251 if (previousVerticalOffset < mIdealVerticalOffset) {
3252 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
3253 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
3254 } else {
3255 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
3256 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
3257 }
3258 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
3259
3260 final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
3261 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
3262
3263 updatePosition(newPosX, newPosY);
3264 break;
3265 }
3266
3267 case MotionEvent.ACTION_UP:
3268 filterOnTouchUp();
3269 mIsDragging = false;
3270 break;
3271
3272 case MotionEvent.ACTION_CANCEL:
3273 mIsDragging = false;
3274 break;
3275 }
3276 return true;
3277 }
3278
3279 public boolean isDragging() {
3280 return mIsDragging;
3281 }
3282
3283 void onHandleMoved() {
3284 hideActionPopupWindow();
3285 }
3286
3287 public void onDetached() {
3288 hideActionPopupWindow();
3289 }
3290 }
3291
3292 private class InsertionHandleView extends HandleView {
3293 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
3294 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
3295
3296 // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
3297 private float mDownPositionX, mDownPositionY;
3298 private Runnable mHider;
3299
3300 public InsertionHandleView(Drawable drawable) {
3301 super(drawable, drawable);
3302 }
3303
3304 @Override
3305 public void show() {
3306 super.show();
3307
3308 final long durationSinceCutOrCopy =
3309 SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME;
3310 if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
3311 showActionPopupWindow(0);
3312 }
3313
3314 hideAfterDelay();
3315 }
3316
3317 public void showWithActionPopup() {
3318 show();
3319 showActionPopupWindow(0);
3320 }
3321
3322 private void hideAfterDelay() {
3323 if (mHider == null) {
3324 mHider = new Runnable() {
3325 public void run() {
3326 hide();
3327 }
3328 };
3329 } else {
3330 removeHiderCallback();
3331 }
3332 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
3333 }
3334
3335 private void removeHiderCallback() {
3336 if (mHider != null) {
3337 mTextView.removeCallbacks(mHider);
3338 }
3339 }
3340
3341 @Override
3342 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3343 return drawable.getIntrinsicWidth() / 2;
3344 }
3345
3346 @Override
3347 public boolean onTouchEvent(MotionEvent ev) {
3348 final boolean result = super.onTouchEvent(ev);
3349
3350 switch (ev.getActionMasked()) {
3351 case MotionEvent.ACTION_DOWN:
3352 mDownPositionX = ev.getRawX();
3353 mDownPositionY = ev.getRawY();
3354 break;
3355
3356 case MotionEvent.ACTION_UP:
3357 if (!offsetHasBeenChanged()) {
3358 final float deltaX = mDownPositionX - ev.getRawX();
3359 final float deltaY = mDownPositionY - ev.getRawY();
3360 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3361
3362 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3363 mTextView.getContext());
3364 final int touchSlop = viewConfiguration.getScaledTouchSlop();
3365
3366 if (distanceSquared < touchSlop * touchSlop) {
3367 if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
3368 // Tapping on the handle dismisses the displayed action popup
3369 mActionPopupWindow.hide();
3370 } else {
3371 showWithActionPopup();
3372 }
3373 }
3374 }
3375 hideAfterDelay();
3376 break;
3377
3378 case MotionEvent.ACTION_CANCEL:
3379 hideAfterDelay();
3380 break;
3381
3382 default:
3383 break;
3384 }
3385
3386 return result;
3387 }
3388
3389 @Override
3390 public int getCurrentCursorOffset() {
3391 return mTextView.getSelectionStart();
3392 }
3393
3394 @Override
3395 public void updateSelection(int offset) {
3396 Selection.setSelection((Spannable) mTextView.getText(), offset);
3397 }
3398
3399 @Override
3400 public void updatePosition(float x, float y) {
3401 positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
3402 }
3403
3404 @Override
3405 void onHandleMoved() {
3406 super.onHandleMoved();
3407 removeHiderCallback();
3408 }
3409
3410 @Override
3411 public void onDetached() {
3412 super.onDetached();
3413 removeHiderCallback();
3414 }
3415 }
3416
3417 private class SelectionStartHandleView extends HandleView {
3418
3419 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3420 super(drawableLtr, drawableRtl);
3421 }
3422
3423 @Override
3424 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3425 if (isRtlRun) {
3426 return drawable.getIntrinsicWidth() / 4;
3427 } else {
3428 return (drawable.getIntrinsicWidth() * 3) / 4;
3429 }
3430 }
3431
3432 @Override
3433 public int getCurrentCursorOffset() {
3434 return mTextView.getSelectionStart();
3435 }
3436
3437 @Override
3438 public void updateSelection(int offset) {
3439 Selection.setSelection((Spannable) mTextView.getText(), offset,
3440 mTextView.getSelectionEnd());
3441 updateDrawable();
3442 }
3443
3444 @Override
3445 public void updatePosition(float x, float y) {
3446 int offset = mTextView.getOffsetForPosition(x, y);
3447
3448 // Handles can not cross and selection is at least one character
3449 final int selectionEnd = mTextView.getSelectionEnd();
3450 if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
3451
3452 positionAtCursorOffset(offset, false);
3453 }
3454
3455 public ActionPopupWindow getActionPopupWindow() {
3456 return mActionPopupWindow;
3457 }
3458 }
3459
3460 private class SelectionEndHandleView extends HandleView {
3461
3462 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
3463 super(drawableLtr, drawableRtl);
3464 }
3465
3466 @Override
3467 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3468 if (isRtlRun) {
3469 return (drawable.getIntrinsicWidth() * 3) / 4;
3470 } else {
3471 return drawable.getIntrinsicWidth() / 4;
3472 }
3473 }
3474
3475 @Override
3476 public int getCurrentCursorOffset() {
3477 return mTextView.getSelectionEnd();
3478 }
3479
3480 @Override
3481 public void updateSelection(int offset) {
3482 Selection.setSelection((Spannable) mTextView.getText(),
3483 mTextView.getSelectionStart(), offset);
3484 updateDrawable();
3485 }
3486
3487 @Override
3488 public void updatePosition(float x, float y) {
3489 int offset = mTextView.getOffsetForPosition(x, y);
3490
3491 // Handles can not cross and selection is at least one character
3492 final int selectionStart = mTextView.getSelectionStart();
3493 if (offset <= selectionStart) {
3494 offset = Math.min(selectionStart + 1, mTextView.getText().length());
3495 }
3496
3497 positionAtCursorOffset(offset, false);
3498 }
3499
3500 public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
3501 mActionPopupWindow = actionPopupWindow;
3502 }
3503 }
3504
3505 /**
3506 * A CursorController instance can be used to control a cursor in the text.
3507 */
3508 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
3509 /**
3510 * Makes the cursor controller visible on screen.
3511 * See also {@link #hide()}.
3512 */
3513 public void show();
3514
3515 /**
3516 * Hide the cursor controller from screen.
3517 * See also {@link #show()}.
3518 */
3519 public void hide();
3520
3521 /**
3522 * Called when the view is detached from window. Perform house keeping task, such as
3523 * stopping Runnable thread that would otherwise keep a reference on the context, thus
3524 * preventing the activity from being recycled.
3525 */
3526 public void onDetached();
3527 }
3528
3529 private class InsertionPointCursorController implements CursorController {
3530 private InsertionHandleView mHandle;
3531
3532 public void show() {
3533 getHandle().show();
3534 }
3535
3536 public void showWithActionPopup() {
3537 getHandle().showWithActionPopup();
3538 }
3539
3540 public void hide() {
3541 if (mHandle != null) {
3542 mHandle.hide();
3543 }
3544 }
3545
3546 public void onTouchModeChanged(boolean isInTouchMode) {
3547 if (!isInTouchMode) {
3548 hide();
3549 }
3550 }
3551
3552 private InsertionHandleView getHandle() {
3553 if (mSelectHandleCenter == null) {
3554 mSelectHandleCenter = mTextView.getResources().getDrawable(
3555 mTextView.mTextSelectHandleRes);
3556 }
3557 if (mHandle == null) {
3558 mHandle = new InsertionHandleView(mSelectHandleCenter);
3559 }
3560 return mHandle;
3561 }
3562
3563 @Override
3564 public void onDetached() {
3565 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3566 observer.removeOnTouchModeChangeListener(this);
3567
3568 if (mHandle != null) mHandle.onDetached();
3569 }
3570 }
3571
3572 class SelectionModifierCursorController implements CursorController {
3573 private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
3574 // The cursor controller handles, lazily created when shown.
3575 private SelectionStartHandleView mStartHandle;
3576 private SelectionEndHandleView mEndHandle;
3577 // The offsets of that last touch down event. Remembered to start selection there.
3578 private int mMinTouchOffset, mMaxTouchOffset;
3579
3580 // Double tap detection
3581 private long mPreviousTapUpTime = 0;
3582 private float mDownPositionX, mDownPositionY;
3583 private boolean mGestureStayedInTapRegion;
3584
3585 SelectionModifierCursorController() {
3586 resetTouchOffsets();
3587 }
3588
3589 public void show() {
3590 if (mTextView.isInBatchEditMode()) {
3591 return;
3592 }
3593 initDrawables();
3594 initHandles();
3595 hideInsertionPointCursorController();
3596 }
3597
3598 private void initDrawables() {
3599 if (mSelectHandleLeft == null) {
3600 mSelectHandleLeft = mTextView.getContext().getResources().getDrawable(
3601 mTextView.mTextSelectHandleLeftRes);
3602 }
3603 if (mSelectHandleRight == null) {
3604 mSelectHandleRight = mTextView.getContext().getResources().getDrawable(
3605 mTextView.mTextSelectHandleRightRes);
3606 }
3607 }
3608
3609 private void initHandles() {
3610 // Lazy object creation has to be done before updatePosition() is called.
3611 if (mStartHandle == null) {
3612 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
3613 }
3614 if (mEndHandle == null) {
3615 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
3616 }
3617
3618 mStartHandle.show();
3619 mEndHandle.show();
3620
3621 // Make sure both left and right handles share the same ActionPopupWindow (so that
3622 // moving any of the handles hides the action popup).
3623 mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
3624 mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
3625
3626 hideInsertionPointCursorController();
3627 }
3628
3629 public void hide() {
3630 if (mStartHandle != null) mStartHandle.hide();
3631 if (mEndHandle != null) mEndHandle.hide();
3632 }
3633
3634 public void onTouchEvent(MotionEvent event) {
3635 // This is done even when the View does not have focus, so that long presses can start
3636 // selection and tap can move cursor from this tap position.
3637 switch (event.getActionMasked()) {
3638 case MotionEvent.ACTION_DOWN:
3639 final float x = event.getX();
3640 final float y = event.getY();
3641
3642 // Remember finger down position, to be able to start selection from there
3643 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
3644
3645 // Double tap detection
3646 if (mGestureStayedInTapRegion) {
3647 long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
3648 if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
3649 final float deltaX = x - mDownPositionX;
3650 final float deltaY = y - mDownPositionY;
3651 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3652
3653 ViewConfiguration viewConfiguration = ViewConfiguration.get(
3654 mTextView.getContext());
3655 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
3656 boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
3657
3658 if (stayedInArea && isPositionOnText(x, y)) {
3659 startSelectionActionMode();
3660 mDiscardNextActionUp = true;
3661 }
3662 }
3663 }
3664
3665 mDownPositionX = x;
3666 mDownPositionY = y;
3667 mGestureStayedInTapRegion = true;
3668 break;
3669
3670 case MotionEvent.ACTION_POINTER_DOWN:
3671 case MotionEvent.ACTION_POINTER_UP:
3672 // Handle multi-point gestures. Keep min and max offset positions.
3673 // Only activated for devices that correctly handle multi-touch.
3674 if (mTextView.getContext().getPackageManager().hasSystemFeature(
3675 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
3676 updateMinAndMaxOffsets(event);
3677 }
3678 break;
3679
3680 case MotionEvent.ACTION_MOVE:
3681 if (mGestureStayedInTapRegion) {
3682 final float deltaX = event.getX() - mDownPositionX;
3683 final float deltaY = event.getY() - mDownPositionY;
3684 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3685
3686 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3687 mTextView.getContext());
3688 int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
3689
3690 if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
3691 mGestureStayedInTapRegion = false;
3692 }
3693 }
3694 break;
3695
3696 case MotionEvent.ACTION_UP:
3697 mPreviousTapUpTime = SystemClock.uptimeMillis();
3698 break;
3699 }
3700 }
3701
3702 /**
3703 * @param event
3704 */
3705 private void updateMinAndMaxOffsets(MotionEvent event) {
3706 int pointerCount = event.getPointerCount();
3707 for (int index = 0; index < pointerCount; index++) {
3708 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
3709 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
3710 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
3711 }
3712 }
3713
3714 public int getMinTouchOffset() {
3715 return mMinTouchOffset;
3716 }
3717
3718 public int getMaxTouchOffset() {
3719 return mMaxTouchOffset;
3720 }
3721
3722 public void resetTouchOffsets() {
3723 mMinTouchOffset = mMaxTouchOffset = -1;
3724 }
3725
3726 /**
3727 * @return true iff this controller is currently used to move the selection start.
3728 */
3729 public boolean isSelectionStartDragged() {
3730 return mStartHandle != null && mStartHandle.isDragging();
3731 }
3732
3733 public void onTouchModeChanged(boolean isInTouchMode) {
3734 if (!isInTouchMode) {
3735 hide();
3736 }
3737 }
3738
3739 @Override
3740 public void onDetached() {
3741 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
3742 observer.removeOnTouchModeChangeListener(this);
3743
3744 if (mStartHandle != null) mStartHandle.onDetached();
3745 if (mEndHandle != null) mEndHandle.onDetached();
3746 }
3747 }
3748
3749 private class CorrectionHighlighter {
3750 private final Path mPath = new Path();
3751 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
3752 private int mStart, mEnd;
3753 private long mFadingStartTime;
3754 private RectF mTempRectF;
3755 private final static int FADE_OUT_DURATION = 400;
3756
3757 public CorrectionHighlighter() {
3758 mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
3759 applicationScale);
3760 mPaint.setStyle(Paint.Style.FILL);
3761 }
3762
3763 public void highlight(CorrectionInfo info) {
3764 mStart = info.getOffset();
3765 mEnd = mStart + info.getNewText().length();
3766 mFadingStartTime = SystemClock.uptimeMillis();
3767
3768 if (mStart < 0 || mEnd < 0) {
3769 stopAnimation();
3770 }
3771 }
3772
3773 public void draw(Canvas canvas, int cursorOffsetVertical) {
3774 if (updatePath() && updatePaint()) {
3775 if (cursorOffsetVertical != 0) {
3776 canvas.translate(0, cursorOffsetVertical);
3777 }
3778
3779 canvas.drawPath(mPath, mPaint);
3780
3781 if (cursorOffsetVertical != 0) {
3782 canvas.translate(0, -cursorOffsetVertical);
3783 }
3784 invalidate(true); // TODO invalidate cursor region only
3785 } else {
3786 stopAnimation();
3787 invalidate(false); // TODO invalidate cursor region only
3788 }
3789 }
3790
3791 private boolean updatePaint() {
3792 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
3793 if (duration > FADE_OUT_DURATION) return false;
3794
3795 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
3796 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
3797 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
3798 ((int) (highlightColorAlpha * coef) << 24);
3799 mPaint.setColor(color);
3800 return true;
3801 }
3802
3803 private boolean updatePath() {
3804 final Layout layout = mTextView.getLayout();
3805 if (layout == null) return false;
3806
3807 // Update in case text is edited while the animation is run
3808 final int length = mTextView.getText().length();
3809 int start = Math.min(length, mStart);
3810 int end = Math.min(length, mEnd);
3811
3812 mPath.reset();
3813 layout.getSelectionPath(start, end, mPath);
3814 return true;
3815 }
3816
3817 private void invalidate(boolean delayed) {
3818 if (mTextView.getLayout() == null) return;
3819
3820 if (mTempRectF == null) mTempRectF = new RectF();
3821 mPath.computeBounds(mTempRectF, false);
3822
3823 int left = mTextView.getCompoundPaddingLeft();
3824 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
3825
3826 if (delayed) {
3827 mTextView.postInvalidateOnAnimation(
3828 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
3829 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
3830 } else {
3831 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
3832 (int) mTempRectF.right, (int) mTempRectF.bottom);
3833 }
3834 }
3835
3836 private void stopAnimation() {
3837 Editor.this.mCorrectionHighlighter = null;
3838 }
3839 }
3840
3841 private static class ErrorPopup extends PopupWindow {
3842 private boolean mAbove = false;
3843 private final TextView mView;
3844 private int mPopupInlineErrorBackgroundId = 0;
3845 private int mPopupInlineErrorAboveBackgroundId = 0;
3846
3847 ErrorPopup(TextView v, int width, int height) {
3848 super(v, width, height);
3849 mView = v;
3850 // 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 -08003851 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07003852 // dimensions identical to the above version for this to work (and is more likely).
3853 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3854 com.android.internal.R.styleable.Theme_errorMessageBackground);
3855 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
3856 }
3857
3858 void fixDirection(boolean above) {
3859 mAbove = above;
3860
3861 if (above) {
3862 mPopupInlineErrorAboveBackgroundId =
3863 getResourceId(mPopupInlineErrorAboveBackgroundId,
3864 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
3865 } else {
3866 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3867 com.android.internal.R.styleable.Theme_errorMessageBackground);
3868 }
3869
3870 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
3871 mPopupInlineErrorBackgroundId);
3872 }
3873
3874 private int getResourceId(int currentId, int index) {
3875 if (currentId == 0) {
3876 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
3877 R.styleable.Theme);
3878 currentId = styledAttributes.getResourceId(index, 0);
3879 styledAttributes.recycle();
3880 }
3881 return currentId;
3882 }
3883
3884 @Override
3885 public void update(int x, int y, int w, int h, boolean force) {
3886 super.update(x, y, w, h, force);
3887
3888 boolean above = isAboveAnchor();
3889 if (above != mAbove) {
3890 fixDirection(above);
3891 }
3892 }
3893 }
3894
3895 static class InputContentType {
3896 int imeOptions = EditorInfo.IME_NULL;
3897 String privateImeOptions;
3898 CharSequence imeActionLabel;
3899 int imeActionId;
3900 Bundle extras;
3901 OnEditorActionListener onEditorActionListener;
3902 boolean enterDown;
3903 }
3904
3905 static class InputMethodState {
3906 Rect mCursorRectInWindow = new Rect();
3907 RectF mTmpRectF = new RectF();
3908 float[] mTmpOffset = new float[2];
Gilles Debunnec62589c2012-04-12 14:50:23 -07003909 ExtractedTextRequest mExtractedTextRequest;
3910 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07003911 int mBatchEditNesting;
3912 boolean mCursorChanged;
3913 boolean mSelectionModeChanged;
3914 boolean mContentChanged;
3915 int mChangedStart, mChangedEnd, mChangedDelta;
3916 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09003917
3918 /**
3919 * @hide
3920 */
3921 public static class UserDictionaryListener extends Handler {
3922 public TextView mTextView;
3923 public String mOriginalWord;
3924 public int mWordStart;
3925 public int mWordEnd;
3926
3927 public void waitForUserDictionaryAdded(
3928 TextView tv, String originalWord, int spanStart, int spanEnd) {
3929 mTextView = tv;
3930 mOriginalWord = originalWord;
3931 mWordStart = spanStart;
3932 mWordEnd = spanEnd;
3933 }
3934
3935 @Override
3936 public void handleMessage(Message msg) {
satok6bad2f22012-12-17 14:57:54 +09003937 switch(msg.what) {
3938 case 0: /* CODE_WORD_ADDED */
3939 case 2: /* CODE_ALREADY_PRESENT */
3940 if (!(msg.obj instanceof Bundle)) {
3941 Log.w(TAG, "Illegal message. Abort handling onUserDictionaryAdded.");
3942 return;
3943 }
3944 final Bundle bundle = (Bundle)msg.obj;
3945 final String originalWord = bundle.getString("originalWord");
3946 final String addedWord = bundle.getString("word");
3947 onUserDictionaryAdded(originalWord, addedWord);
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09003948 return;
satok6bad2f22012-12-17 14:57:54 +09003949 default:
3950 return;
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09003951 }
3952 }
3953
3954 private void onUserDictionaryAdded(String originalWord, String addedWord) {
3955 if (TextUtils.isEmpty(mOriginalWord) || TextUtils.isEmpty(addedWord)) {
3956 return;
3957 }
3958 if (mWordStart < 0 || mWordEnd >= mTextView.length()) {
3959 return;
3960 }
3961 if (!mOriginalWord.equals(originalWord)) {
3962 return;
3963 }
3964 if (originalWord.equals(addedWord)) {
3965 return;
3966 }
3967 final Editable editable = (Editable) mTextView.getText();
3968 final String currentWord = editable.toString().substring(mWordStart, mWordEnd);
3969 if (!currentWord.equals(originalWord)) {
3970 return;
3971 }
3972 mTextView.replaceText_internal(mWordStart, mWordEnd, addedWord);
3973 // Move cursor at the end of the replaced word
3974 final int newCursorPosition = mWordStart + addedWord.length();
3975 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
3976 }
3977 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07003978
3979 public static class UndoInputFilter implements InputFilter {
3980 final Editor mEditor;
3981
3982 public UndoInputFilter(Editor editor) {
3983 mEditor = editor;
3984 }
3985
3986 @Override
3987 public CharSequence filter(CharSequence source, int start, int end,
3988 Spanned dest, int dstart, int dend) {
3989 if (DEBUG_UNDO) {
3990 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ")");
3991 Log.d(TAG, "filter: dest=" + dest + " (" + dstart + "-" + dend + ")");
3992 }
3993 final UndoManager um = mEditor.mUndoManager;
3994 if (um.isInUndo()) {
3995 if (DEBUG_UNDO) Log.d(TAG, "*** skipping, currently performing undo/redo");
3996 return null;
3997 }
3998
3999 um.beginUpdate("Edit text");
4000 TextModifyOperation op = um.getLastOperation(
4001 TextModifyOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
4002 if (op != null) {
4003 if (DEBUG_UNDO) Log.d(TAG, "Last op: range=(" + op.mRangeStart + "-" + op.mRangeEnd
4004 + "), oldText=" + op.mOldText);
4005 // See if we can continue modifying this operation.
4006 if (op.mOldText == null) {
4007 // The current operation is an add... are we adding more? We are adding
4008 // more if we are either appending new text to the end of the last edit or
4009 // completely replacing some or all of the last edit.
4010 if (start < end && ((dstart >= op.mRangeStart && dend <= op.mRangeEnd)
4011 || (dstart == op.mRangeEnd && dend == op.mRangeEnd))) {
4012 op.mRangeEnd = dstart + (end-start);
4013 um.endUpdate();
4014 if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, mRangeEnd="
4015 + op.mRangeEnd);
4016 return null;
4017 }
4018 } else {
4019 // The current operation is a delete... can we delete more?
4020 if (start == end && dend == op.mRangeStart-1) {
4021 SpannableStringBuilder str;
4022 if (op.mOldText instanceof SpannableString) {
4023 str = (SpannableStringBuilder)op.mOldText;
4024 } else {
4025 str = new SpannableStringBuilder(op.mOldText);
4026 }
4027 str.insert(0, dest, dstart, dend);
4028 op.mRangeStart = dstart;
4029 op.mOldText = str;
4030 um.endUpdate();
4031 if (DEBUG_UNDO) Log.d(TAG, "*** merging with last op, range=("
4032 + op.mRangeStart + "-" + op.mRangeEnd
4033 + "), oldText=" + op.mOldText);
4034 return null;
4035 }
4036 }
4037
4038 // Couldn't add to the current undo operation, need to start a new
4039 // undo state for a new undo operation.
4040 um.commitState(null);
4041 um.setUndoLabel("Edit text");
4042 }
4043
4044 // Create a new undo state reflecting the operation being performed.
4045 op = new TextModifyOperation(mEditor.mUndoOwner);
4046 op.mRangeStart = dstart;
4047 if (start < end) {
4048 op.mRangeEnd = dstart + (end-start);
4049 } else {
4050 op.mRangeEnd = dstart;
4051 }
4052 if (dstart < dend) {
4053 op.mOldText = dest.subSequence(dstart, dend);
4054 }
4055 if (DEBUG_UNDO) Log.d(TAG, "*** adding new op, range=(" + op.mRangeStart
4056 + "-" + op.mRangeEnd + "), oldText=" + op.mOldText);
4057 um.addOperation(op, UndoManager.MERGE_MODE_NONE);
4058 um.endUpdate();
4059 return null;
4060 }
4061 }
4062
4063 public static class TextModifyOperation extends UndoOperation<TextView> {
4064 int mRangeStart, mRangeEnd;
4065 CharSequence mOldText;
4066
4067 public TextModifyOperation(UndoOwner owner) {
4068 super(owner);
4069 }
4070
4071 public TextModifyOperation(Parcel src, ClassLoader loader) {
4072 super(src, loader);
4073 mRangeStart = src.readInt();
4074 mRangeEnd = src.readInt();
4075 mOldText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src);
4076 }
4077
4078 @Override
4079 public void commit() {
4080 }
4081
4082 @Override
4083 public void undo() {
4084 swapText();
4085 }
4086
4087 @Override
4088 public void redo() {
4089 swapText();
4090 }
4091
4092 private void swapText() {
4093 // Both undo and redo involves swapping the contents of the range
4094 // in the text view with our local text.
4095 TextView tv = getOwnerData();
4096 Editable editable = (Editable)tv.getText();
4097 CharSequence curText;
4098 if (mRangeStart >= mRangeEnd) {
4099 curText = null;
4100 } else {
4101 curText = editable.subSequence(mRangeStart, mRangeEnd);
4102 }
4103 if (DEBUG_UNDO) {
4104 Log.d(TAG, "Swap: range=(" + mRangeStart + "-" + mRangeEnd
4105 + "), oldText=" + mOldText);
4106 Log.d(TAG, "Swap: curText=" + curText);
4107 }
4108 if (mOldText == null) {
4109 editable.delete(mRangeStart, mRangeEnd);
4110 mRangeEnd = mRangeStart;
4111 } else {
4112 editable.replace(mRangeStart, mRangeEnd, mOldText);
4113 mRangeEnd = mRangeStart + mOldText.length();
4114 }
4115 mOldText = curText;
4116 }
4117
4118 @Override
4119 public void writeToParcel(Parcel dest, int flags) {
4120 dest.writeInt(mRangeStart);
4121 dest.writeInt(mRangeEnd);
4122 TextUtils.writeToParcel(mOldText, dest, flags);
4123 }
4124
4125 public static final Parcelable.ClassLoaderCreator<TextModifyOperation> CREATOR
4126 = new Parcelable.ClassLoaderCreator<TextModifyOperation>() {
4127 public TextModifyOperation createFromParcel(Parcel in) {
4128 return new TextModifyOperation(in, null);
4129 }
4130
4131 public TextModifyOperation createFromParcel(Parcel in, ClassLoader loader) {
4132 return new TextModifyOperation(in, loader);
4133 }
4134
4135 public TextModifyOperation[] newArray(int size) {
4136 return new TextModifyOperation[size];
4137 }
4138 };
4139 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004140}