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