blob: e43237aa56c237802dc4c669613cf3ce507d6508 [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
19import android.R;
Yoshiki Iguchiee147722015-04-14 00:12:44 +090020import android.annotation.Nullable;
Luca Zanolin1b15ba52013-02-20 14:31:37 +000021import android.app.PendingIntent;
22import android.app.PendingIntent.CanceledException;
Gilles Debunned88876a2012-03-16 17:34:04 -070023import android.content.ClipData;
24import android.content.ClipData.Item;
25import android.content.Context;
26import android.content.Intent;
Raph Levien26d443a2015-03-30 14:18:32 -070027import android.content.UndoManager;
28import android.content.UndoOperation;
29import android.content.UndoOwner;
Gilles Debunned88876a2012-03-16 17:34:04 -070030import android.content.pm.PackageManager;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +000031import android.content.pm.ResolveInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070032import android.content.res.TypedArray;
33import android.graphics.Canvas;
34import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090035import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070036import android.graphics.Paint;
37import android.graphics.Path;
38import android.graphics.Rect;
39import android.graphics.RectF;
40import android.graphics.drawable.Drawable;
41import android.inputmethodservice.ExtractEditText;
42import android.os.Bundle;
43import android.os.Handler;
Raph Levien26d443a2015-03-30 14:18:32 -070044import android.os.Parcel;
45import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080046import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070047import android.os.SystemClock;
48import android.provider.Settings;
49import android.text.DynamicLayout;
50import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070051import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070052import android.text.InputType;
53import android.text.Layout;
54import android.text.ParcelableSpan;
55import android.text.Selection;
56import android.text.SpanWatcher;
57import android.text.Spannable;
58import android.text.SpannableStringBuilder;
59import android.text.Spanned;
60import android.text.StaticLayout;
61import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070062import android.text.method.KeyListener;
63import android.text.method.MetaKeyKeyListener;
64import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070065import android.text.method.WordIterator;
66import android.text.style.EasyEditSpan;
67import android.text.style.SuggestionRangeSpan;
68import android.text.style.SuggestionSpan;
69import android.text.style.TextAppearanceSpan;
70import android.text.style.URLSpan;
71import android.util.DisplayMetrics;
72import android.util.Log;
73import android.view.ActionMode;
74import android.view.ActionMode.Callback;
Chris Craikf6829a02015-03-10 10:28:59 -070075import android.view.DisplayListCanvas;
Gilles Debunned88876a2012-03-16 17:34:04 -070076import android.view.DragEvent;
77import android.view.Gravity;
Gilles Debunned88876a2012-03-16 17:34:04 -070078import android.view.LayoutInflater;
79import android.view.Menu;
80import android.view.MenuItem;
81import android.view.MotionEvent;
Chris Craikf6829a02015-03-10 10:28:59 -070082import android.view.RenderNode;
Gilles Debunned88876a2012-03-16 17:34:04 -070083import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -070084import android.view.View.DragShadowBuilder;
85import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -070086import android.view.ViewConfiguration;
87import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -070088import android.view.ViewGroup.LayoutParams;
89import android.view.ViewParent;
90import android.view.ViewTreeObserver;
91import android.view.WindowManager;
92import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090093import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070094import android.view.inputmethod.EditorInfo;
95import android.view.inputmethod.ExtractedText;
96import android.view.inputmethod.ExtractedTextRequest;
97import android.view.inputmethod.InputConnection;
98import android.view.inputmethod.InputMethodManager;
99import android.widget.AdapterView.OnItemClickListener;
100import android.widget.TextView.Drawables;
101import android.widget.TextView.OnEditorActionListener;
102
Raph Levien26d443a2015-03-30 14:18:32 -0700103import com.android.internal.util.ArrayUtils;
104import com.android.internal.util.GrowingArrayUtils;
Raph Levien26d443a2015-03-30 14:18:32 -0700105import com.android.internal.widget.EditableInputConnection;
106
Gilles Debunned88876a2012-03-16 17:34:04 -0700107import java.text.BreakIterator;
108import java.util.Arrays;
109import java.util.Comparator;
110import java.util.HashMap;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +0000111import java.util.List;
Gilles Debunned88876a2012-03-16 17:34:04 -0700112
113/**
114 * Helper class used by TextView to handle editable text views.
115 *
116 * @hide
117 */
118public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700119 private static final String TAG = "Editor";
James Cookf59152c2015-02-26 18:03:58 -0800120 private static final boolean DEBUG_UNDO = false;
Adam Powell057a5852012-05-11 10:28:38 -0700121
Gilles Debunned88876a2012-03-16 17:34:04 -0700122 static final int BLINK = 500;
123 private static final float[] TEMP_POSITION = new float[2];
124 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
James Cookf59152c2015-02-26 18:03:58 -0800125 // Tag used when the Editor maintains its own separate UndoManager.
126 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700127
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100128 // Ordering constants used to place the Action Mode items in their menu.
129 private static final int MENU_ITEM_ORDER_CUT = 1;
130 private static final int MENU_ITEM_ORDER_COPY = 2;
131 private static final int MENU_ITEM_ORDER_PASTE = 3;
132 private static final int MENU_ITEM_ORDER_SHARE = 4;
133 private static final int MENU_ITEM_ORDER_SELECT_ALL = 5;
134 private static final int MENU_ITEM_ORDER_REPLACE = 6;
135 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 10;
136
James Cookf59152c2015-02-26 18:03:58 -0800137 // Each Editor manages its own undo stack.
138 private final UndoManager mUndoManager = new UndoManager();
139 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800140 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800141 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700142
Gilles Debunned88876a2012-03-16 17:34:04 -0700143 // Cursor Controllers.
144 InsertionPointCursorController mInsertionPointCursorController;
145 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100146 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
147 ActionMode mTextActionMode;
Gilles Debunned88876a2012-03-16 17:34:04 -0700148 boolean mInsertionControllerEnabled;
149 boolean mSelectionControllerEnabled;
150
151 // Used to highlight a word when it is corrected by the IME
152 CorrectionHighlighter mCorrectionHighlighter;
153
154 InputContentType mInputContentType;
155 InputMethodState mInputMethodState;
156
Chris Craik956f3402015-04-27 16:41:00 -0700157 private static class TextRenderNode {
158 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800159 boolean isDirty;
Chris Craik956f3402015-04-27 16:41:00 -0700160 public TextRenderNode(String name) {
John Reck7558aa72014-03-05 14:59:59 -0800161 isDirty = true;
Chris Craik956f3402015-04-27 16:41:00 -0700162 renderNode = RenderNode.create(name, null);
John Reck7558aa72014-03-05 14:59:59 -0800163 }
Chris Craik956f3402015-04-27 16:41:00 -0700164 boolean needsRecord() { return isDirty || !renderNode.isValid(); }
John Reck7558aa72014-03-05 14:59:59 -0800165 }
Chris Craik956f3402015-04-27 16:41:00 -0700166 TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700167
168 boolean mFrozenWithFocus;
169 boolean mSelectionMoved;
170 boolean mTouchFocusSelected;
171
172 KeyListener mKeyListener;
173 int mInputType = EditorInfo.TYPE_NULL;
174
175 boolean mDiscardNextActionUp;
176 boolean mIgnoreActionUpEvent;
177
178 long mShowCursor;
179 Blink mBlink;
180
181 boolean mCursorVisible = true;
182 boolean mSelectAllOnFocus;
183 boolean mTextIsSelectable;
184
185 CharSequence mError;
186 boolean mErrorWasChanged;
187 ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700188
Gilles Debunned88876a2012-03-16 17:34:04 -0700189 /**
190 * This flag is set if the TextView tries to display an error before it
191 * is attached to the window (so its position is still unknown).
192 * It causes the error to be shown later, when onAttachedToWindow()
193 * is called.
194 */
195 boolean mShowErrorAfterAttach;
196
197 boolean mInBatchEditControllers;
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700198 boolean mShowSoftInputOnFocus = true;
Adam Powell057a5852012-05-11 10:28:38 -0700199 boolean mPreserveDetachedSelection;
200 boolean mTemporaryDetach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700201
202 SuggestionsPopupWindow mSuggestionsPopupWindow;
203 SuggestionRangeSpan mSuggestionRangeSpan;
204 Runnable mShowSuggestionRunnable;
205
206 final Drawable[] mCursorDrawable = new Drawable[2];
207 int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
208
209 private Drawable mSelectHandleLeft;
210 private Drawable mSelectHandleRight;
211 private Drawable mSelectHandleCenter;
212
213 // Global listener that detects changes in the global position of the TextView
214 private PositionListener mPositionListener;
215
216 float mLastDownPositionX, mLastDownPositionY;
217 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100218 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700219
220 // Set when this TextView gained focus with some text selected. Will start selection mode.
221 boolean mCreatedWithASelection;
222
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100223 boolean mDoubleTap = false;
224
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100225 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100226
Jean Chalardbaf30942013-02-28 16:01:51 -0800227 // The span controller helps monitoring the changes to which the Editor needs to react:
228 // - EasyEditSpans, for which we have some UI to display on attach and on hide
229 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
230 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700231
232 WordIterator mWordIterator;
233 SpellChecker mSpellChecker;
234
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800235 // This word iterator is set with text and used to determine word boundaries
236 // when a user is selecting text.
237 private WordIterator mWordIteratorWithText;
238 // Indicate that the text in the word iterator needs to be updated.
239 private boolean mUpdateWordIteratorText;
240
Gilles Debunned88876a2012-03-16 17:34:04 -0700241 private Rect mTempRect;
242
243 private TextView mTextView;
244
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900245 final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier();
246
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100247 private final Runnable mHideFloatingToolbar = new Runnable() {
248 @Override
249 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100250 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100251 mTextActionMode.hide(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100252 }
253 }
254 };
255
256 private final Runnable mShowFloatingToolbar = new Runnable() {
257 @Override
258 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100259 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100260 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100261 }
262 }
263 };
264
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100265 boolean mIsInsertionActionModeStartPending = false;
266
Gilles Debunned88876a2012-03-16 17:34:04 -0700267 Editor(TextView textView) {
268 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800269 // Synchronize the filter list, which places the undo input filter at the end.
270 mTextView.setFilters(mTextView.getFilters());
271 }
272
273 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800274 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
275 Parcel parcel = state.getParcel();
276 mUndoManager.saveInstanceState(parcel);
277 mUndoInputFilter.saveInstanceState(parcel);
278 return state;
James Cookf59152c2015-02-26 18:03:58 -0800279 }
280
281 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800282 Parcel parcel = state.getParcel();
283 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
284 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800285 // Re-associate this object as the owner of undo state.
286 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
287 }
288
James Cook48e0fac2015-02-25 15:44:51 -0800289 /**
290 * Forgets all undo and redo operations for this Editor.
291 */
292 void forgetUndoRedo() {
293 UndoOwner[] owners = { mUndoOwner };
294 mUndoManager.forgetUndos(owners, -1 /* all */);
295 mUndoManager.forgetRedos(owners, -1 /* all */);
296 }
297
James Cookf59152c2015-02-26 18:03:58 -0800298 boolean canUndo() {
299 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800300 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800301 }
302
303 boolean canRedo() {
304 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800305 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800306 }
307
308 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800309 if (!mAllowUndo) {
310 return;
311 }
James Cookf59152c2015-02-26 18:03:58 -0800312 UndoOwner[] owners = { mUndoOwner };
313 mUndoManager.undo(owners, 1); // Undo 1 action.
314 }
315
316 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800317 if (!mAllowUndo) {
318 return;
319 }
James Cookf59152c2015-02-26 18:03:58 -0800320 UndoOwner[] owners = { mUndoOwner };
321 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700322 }
323
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100324 void replace() {
325 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100326 stopTextActionMode();
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100327 Selection.setSelection((Spannable) mTextView.getText(), middle);
328 showSuggestions();
329 }
330
Gilles Debunned88876a2012-03-16 17:34:04 -0700331 void onAttachedToWindow() {
332 if (mShowErrorAfterAttach) {
333 showError();
334 mShowErrorAfterAttach = false;
335 }
Adam Powell057a5852012-05-11 10:28:38 -0700336 mTemporaryDetach = false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700337
338 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
339 // No need to create the controller.
340 // The get method will add the listener on controller creation.
341 if (mInsertionPointCursorController != null) {
342 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
343 }
344 if (mSelectionModifierCursorController != null) {
Adam Powell057a5852012-05-11 10:28:38 -0700345 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunned88876a2012-03-16 17:34:04 -0700346 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
347 }
348 updateSpellCheckSpans(0, mTextView.getText().length(),
349 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700350
351 if (mTextView.hasTransientState() &&
352 mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
353 // Since transient state is reference counted make sure it stays matched
354 // with our own calls to it for managing selection.
355 // The action mode callback will set this back again when/if the action mode starts.
356 mTextView.setHasTransientState(false);
357
358 // We had an active selection from before, start the selection mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100359 startSelectionActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700360 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900361
362 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200363 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700364 }
365
366 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900367 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
368
Gilles Debunned88876a2012-03-16 17:34:04 -0700369 if (mError != null) {
370 hideError();
371 }
372
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200373 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700374
375 if (mInsertionPointCursorController != null) {
376 mInsertionPointCursorController.onDetached();
377 }
378
379 if (mSelectionModifierCursorController != null) {
380 mSelectionModifierCursorController.onDetached();
381 }
382
383 if (mShowSuggestionRunnable != null) {
384 mTextView.removeCallbacks(mShowSuggestionRunnable);
385 }
386
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100387 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100388 if (mInsertionActionModeRunnable != null) {
389 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100390 }
391
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100392 mTextView.removeCallbacks(mHideFloatingToolbar);
393 mTextView.removeCallbacks(mShowFloatingToolbar);
394
John Reck7558aa72014-03-05 14:59:59 -0800395 destroyDisplayListsData();
Gilles Debunned88876a2012-03-16 17:34:04 -0700396
397 if (mSpellChecker != null) {
398 mSpellChecker.closeSession();
399 // Forces the creation of a new SpellChecker next time this window is created.
400 // Will handle the cases where the settings has been changed in the meantime.
401 mSpellChecker = null;
402 }
403
Adam Powell057a5852012-05-11 10:28:38 -0700404 mPreserveDetachedSelection = true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700405 hideControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100406 stopTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700407 mPreserveDetachedSelection = false;
408 mTemporaryDetach = false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700409 }
410
John Reck7558aa72014-03-05 14:59:59 -0800411 private void destroyDisplayListsData() {
Chris Craik956f3402015-04-27 16:41:00 -0700412 if (mTextRenderNodes != null) {
413 for (int i = 0; i < mTextRenderNodes.length; i++) {
414 RenderNode displayList = mTextRenderNodes[i] != null
415 ? mTextRenderNodes[i].renderNode : null;
John Reck7558aa72014-03-05 14:59:59 -0800416 if (displayList != null && displayList.isValid()) {
John Reckbe34f2f2014-03-10 08:58:44 -0700417 displayList.destroyDisplayListData();
John Reck7558aa72014-03-05 14:59:59 -0800418 }
419 }
420 }
421 }
422
Gilles Debunned88876a2012-03-16 17:34:04 -0700423 private void showError() {
424 if (mTextView.getWindowToken() == null) {
425 mShowErrorAfterAttach = true;
426 return;
427 }
428
429 if (mErrorPopup == null) {
430 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
431 final TextView err = (TextView) inflater.inflate(
432 com.android.internal.R.layout.textview_hint, null);
433
434 final float scale = mTextView.getResources().getDisplayMetrics().density;
435 mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
436 mErrorPopup.setFocusable(false);
437 // The user is entering text, so the input method is needed. We
438 // don't want the popup to be displayed on top of it.
439 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
440 }
441
442 TextView tv = (TextView) mErrorPopup.getContentView();
443 chooseSize(mErrorPopup, mError, tv);
444 tv.setText(mError);
445
446 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
447 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
448 }
449
450 public void setError(CharSequence error, Drawable icon) {
451 mError = TextUtils.stringOrSpannedString(error);
452 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800453
Gilles Debunned88876a2012-03-16 17:34:04 -0700454 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800455 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700456 if (mErrorPopup != null) {
457 if (mErrorPopup.isShowing()) {
458 mErrorPopup.dismiss();
459 }
460
461 mErrorPopup = null;
462 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200463 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800464 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800465 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800466 if (mTextView.isFocused()) {
467 showError();
468 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800469 }
470 }
471
472 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800473 Drawables dr = mTextView.mDrawables;
474 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700475 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700476 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800477 dr.setErrorDrawable(icon, mTextView);
478
479 mTextView.resetResolvedDrawables();
480 mTextView.invalidate();
481 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700482 }
483
484 private void hideError() {
485 if (mErrorPopup != null) {
486 if (mErrorPopup.isShowing()) {
487 mErrorPopup.dismiss();
488 }
489 }
490
491 mShowErrorAfterAttach = false;
492 }
493
494 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800495 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700496 * at the middle of the error icon.
497 */
498 private int getErrorX() {
499 /*
500 * The "25" is the distance between the point and the right edge
501 * of the background
502 */
503 final float scale = mTextView.getResources().getDisplayMetrics().density;
504
505 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800506
507 final int layoutDirection = mTextView.getLayoutDirection();
508 int errorX;
509 int offset;
510 switch (layoutDirection) {
511 default:
512 case View.LAYOUT_DIRECTION_LTR:
513 offset = - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
514 errorX = mTextView.getWidth() - mErrorPopup.getWidth() -
515 mTextView.getPaddingRight() + offset;
516 break;
517 case View.LAYOUT_DIRECTION_RTL:
518 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
519 errorX = mTextView.getPaddingLeft() + offset;
520 break;
521 }
522 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700523 }
524
525 /**
526 * Returns the Y offset to make the pointy top of the error point
527 * at the bottom of the error icon.
528 */
529 private int getErrorY() {
530 /*
531 * Compound, not extended, because the icon is not clipped
532 * if the text height is smaller.
533 */
534 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
535 int vspace = mTextView.getBottom() - mTextView.getTop() -
536 mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
537
538 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800539
540 final int layoutDirection = mTextView.getLayoutDirection();
541 int height;
542 switch (layoutDirection) {
543 default:
544 case View.LAYOUT_DIRECTION_LTR:
545 height = (dr != null ? dr.mDrawableHeightRight : 0);
546 break;
547 case View.LAYOUT_DIRECTION_RTL:
548 height = (dr != null ? dr.mDrawableHeightLeft : 0);
549 break;
550 }
551
552 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700553
554 /*
555 * The "2" is the distance between the point and the top edge
556 * of the background.
557 */
558 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800559 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700560 }
561
562 void createInputContentTypeIfNeeded() {
563 if (mInputContentType == null) {
564 mInputContentType = new InputContentType();
565 }
566 }
567
568 void createInputMethodStateIfNeeded() {
569 if (mInputMethodState == null) {
570 mInputMethodState = new InputMethodState();
571 }
572 }
573
574 boolean isCursorVisible() {
575 // The default value is true, even when there is no associated Editor
576 return mCursorVisible && mTextView.isTextEditable();
577 }
578
579 void prepareCursorControllers() {
580 boolean windowSupportsHandles = false;
581
582 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
583 if (params instanceof WindowManager.LayoutParams) {
584 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
585 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
586 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
587 }
588
589 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
590 mInsertionControllerEnabled = enabled && isCursorVisible();
591 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
592
593 if (!mInsertionControllerEnabled) {
594 hideInsertionPointCursorController();
595 if (mInsertionPointCursorController != null) {
596 mInsertionPointCursorController.onDetached();
597 mInsertionPointCursorController = null;
598 }
599 }
600
601 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100602 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700603 if (mSelectionModifierCursorController != null) {
604 mSelectionModifierCursorController.onDetached();
605 mSelectionModifierCursorController = null;
606 }
607 }
608 }
609
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900610 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700611 if (mInsertionPointCursorController != null) {
612 mInsertionPointCursorController.hide();
613 }
614 }
615
616 /**
617 * Hides the insertion controller and stops text selection mode, hiding the selection controller
618 */
619 void hideControllers() {
620 hideCursorControllers();
621 hideSpanControllers();
622 }
623
624 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800625 if (mSpanController != null) {
626 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700627 }
628 }
629
630 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700631 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
632 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
633 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
634 // to distinguish one from the other.
635 if (mSuggestionsPopupWindow != null && ((mTextView instanceof ExtractEditText) ||
636 !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700637 // Should be done before hide insertion point controller since it triggers a show of it
638 mSuggestionsPopupWindow.hide();
639 }
640 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700641 }
642
643 /**
644 * Create new SpellCheckSpans on the modified region.
645 */
646 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900647 // Remove spans whose adjacent characters are text not punctuation
648 mTextView.removeAdjacentSuggestionSpans(start);
649 mTextView.removeAdjacentSuggestionSpans(end);
650
Gilles Debunned88876a2012-03-16 17:34:04 -0700651 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
652 !(mTextView instanceof ExtractEditText)) {
653 if (mSpellChecker == null && createSpellChecker) {
654 mSpellChecker = new SpellChecker(mTextView);
655 }
656 if (mSpellChecker != null) {
657 mSpellChecker.spellCheck(start, end);
658 }
659 }
660 }
661
662 void onScreenStateChanged(int screenState) {
663 switch (screenState) {
664 case View.SCREEN_STATE_ON:
665 resumeBlink();
666 break;
667 case View.SCREEN_STATE_OFF:
668 suspendBlink();
669 break;
670 }
671 }
672
673 private void suspendBlink() {
674 if (mBlink != null) {
675 mBlink.cancel();
676 }
677 }
678
679 private void resumeBlink() {
680 if (mBlink != null) {
681 mBlink.uncancel();
682 makeBlink();
683 }
684 }
685
686 void adjustInputType(boolean password, boolean passwordInputType,
687 boolean webPasswordInputType, boolean numberPasswordInputType) {
688 // mInputType has been set from inputType, possibly modified by mInputMethod.
689 // Specialize mInputType to [web]password if we have a text class and the original input
690 // type was a password.
691 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
692 if (password || passwordInputType) {
693 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
694 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
695 }
696 if (webPasswordInputType) {
697 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
698 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
699 }
700 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
701 if (numberPasswordInputType) {
702 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
703 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
704 }
705 }
706 }
707
708 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
709 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
710 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
711
712 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
713 com.android.internal.R.dimen.textview_error_popup_default_width);
714 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
715 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
716 float max = 0;
717 for (int i = 0; i < l.getLineCount(); i++) {
718 max = Math.max(max, l.getLineWidth(i));
719 }
720
721 /*
722 * Now set the popup size to be big enough for the text plus the border capped
723 * to DEFAULT_MAX_POPUP_WIDTH
724 */
725 pop.setWidth(wid + (int) Math.ceil(max));
726 pop.setHeight(ht + l.getHeight());
727 }
728
729 void setFrame() {
730 if (mErrorPopup != null) {
731 TextView tv = (TextView) mErrorPopup.getContentView();
732 chooseSize(mErrorPopup, mError, tv);
733 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
734 mErrorPopup.getWidth(), mErrorPopup.getHeight());
735 }
736 }
737
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800738 private int getWordStart(int offset) {
739 // FIXME - For this and similar methods we're not doing anything to check if there's
740 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700741 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700742 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
743 // On punctuation boundary or within group of punctuation, find punctuation start.
744 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
745 } else {
746 // Not on a punctuation boundary, find the word start.
747 retOffset = getWordIteratorWithText().getBeginning(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800748 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700749 if (retOffset == BreakIterator.DONE) {
750 return offset;
751 }
752 return retOffset;
753 }
754
755 private int getWordEnd(int offset) {
756 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700757 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
758 // On punctuation boundary or within group of punctuation, find punctuation end.
759 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
760 } else {
761 // Not on a punctuation boundary, find the word end.
762 retOffset = getWordIteratorWithText().getEnd(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700763 }
764 if (retOffset == BreakIterator.DONE) {
765 return offset;
766 }
767 return retOffset;
768 }
769
770 /**
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800771 * Adjusts selection to the word under last touch offset. Return true if the operation was
772 * successfully performed.
Gilles Debunned88876a2012-03-16 17:34:04 -0700773 */
774 private boolean selectCurrentWord() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100775 if (!mTextView.canSelectText()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700776 return false;
777 }
778
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100779 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700780 // Always select all on a password field.
781 // Cut/copy menu entries are not available for passwords, but being able to select all
782 // is however useful to delete or paste to replace the entire content.
783 return mTextView.selectAllText();
784 }
785
786 int inputType = mTextView.getInputType();
787 int klass = inputType & InputType.TYPE_MASK_CLASS;
788 int variation = inputType & InputType.TYPE_MASK_VARIATION;
789
790 // Specific text field types: select the entire text for these
791 if (klass == InputType.TYPE_CLASS_NUMBER ||
792 klass == InputType.TYPE_CLASS_PHONE ||
793 klass == InputType.TYPE_CLASS_DATETIME ||
794 variation == InputType.TYPE_TEXT_VARIATION_URI ||
795 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
796 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
797 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
798 return mTextView.selectAllText();
799 }
800
801 long lastTouchOffsets = getLastTouchOffsets();
802 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
803 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
804
805 // Safety check in case standard touch event handling has been bypassed
806 if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
807 if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
808
809 int selectionStart, selectionEnd;
810
811 // If a URLSpan (web address, email, phone...) is found at that position, select it.
812 URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
813 getSpans(minOffset, maxOffset, URLSpan.class);
814 if (urlSpans.length >= 1) {
815 URLSpan urlSpan = urlSpans[0];
816 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
817 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
818 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800819 // FIXME - We should check if there's a LocaleSpan in the text, this may be
820 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700821 final WordIterator wordIterator = getWordIterator();
822 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
823
824 selectionStart = wordIterator.getBeginning(minOffset);
825 selectionEnd = wordIterator.getEnd(maxOffset);
826
827 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
828 selectionStart == selectionEnd) {
829 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900830 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700831 selectionStart = TextUtils.unpackRangeStartFromLong(range);
832 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
833 }
834 }
835
836 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
837 return selectionEnd > selectionStart;
838 }
839
840 void onLocaleChanged() {
841 // Will be re-created on demand in getWordIterator with the proper new locale
842 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800843 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700844 }
845
846 /**
847 * @hide
848 */
849 public WordIterator getWordIterator() {
850 if (mWordIterator == null) {
851 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
852 }
853 return mWordIterator;
854 }
855
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800856 private WordIterator getWordIteratorWithText() {
857 if (mWordIteratorWithText == null) {
858 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
859 mUpdateWordIteratorText = true;
860 }
861 if (mUpdateWordIteratorText) {
862 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
863 // to the user's selection is needed. A possible solution would be to
864 // copy some number N of characters near the selection and then when the
865 // user approaches N then we'd do another copy of the next N characters.
866 CharSequence text = mTextView.getText();
867 mWordIteratorWithText.setCharSequence(text, 0, text.length());
868 mUpdateWordIteratorText = false;
869 }
870 return mWordIteratorWithText;
871 }
872
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900873 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
874 final Layout layout = mTextView.getLayout();
875 if (layout == null) return offset;
876 final CharSequence text = mTextView.getText();
877 final int nextOffset = layout.getPaint().getTextRunCursor(text, 0, text.length(),
878 layout.isRtlCharAt(offset) ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR,
879 offset, findAfterGivenOffset ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE);
880 return nextOffset == -1 ? offset : nextOffset;
881 }
882
883 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700884 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -0700885 if (offset < textLength) {
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900886 return TextUtils.packRangeInLong(offset, getNextCursorOffset(offset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -0700887 }
888 if (offset - 1 >= 0) {
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900889 return TextUtils.packRangeInLong(getNextCursorOffset(offset, false), offset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700890 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900891 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700892 }
893
894 private boolean touchPositionIsInSelection() {
895 int selectionStart = mTextView.getSelectionStart();
896 int selectionEnd = mTextView.getSelectionEnd();
897
898 if (selectionStart == selectionEnd) {
899 return false;
900 }
901
902 if (selectionStart > selectionEnd) {
903 int tmp = selectionStart;
904 selectionStart = selectionEnd;
905 selectionEnd = tmp;
906 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
907 }
908
909 SelectionModifierCursorController selectionController = getSelectionController();
910 int minOffset = selectionController.getMinTouchOffset();
911 int maxOffset = selectionController.getMaxTouchOffset();
912
913 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
914 }
915
916 private PositionListener getPositionListener() {
917 if (mPositionListener == null) {
918 mPositionListener = new PositionListener();
919 }
920 return mPositionListener;
921 }
922
923 private interface TextViewPositionListener {
924 public void updatePosition(int parentPositionX, int parentPositionY,
925 boolean parentPositionChanged, boolean parentScrolled);
926 }
927
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900928 private boolean isPositionVisible(final float positionX, final float positionY) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700929 synchronized (TEMP_POSITION) {
930 final float[] position = TEMP_POSITION;
931 position[0] = positionX;
932 position[1] = positionY;
933 View view = mTextView;
934
935 while (view != null) {
936 if (view != mTextView) {
937 // Local scroll is already taken into account in positionX/Y
938 position[0] -= view.getScrollX();
939 position[1] -= view.getScrollY();
940 }
941
942 if (position[0] < 0 || position[1] < 0 ||
943 position[0] > view.getWidth() || position[1] > view.getHeight()) {
944 return false;
945 }
946
947 if (!view.getMatrix().isIdentity()) {
948 view.getMatrix().mapPoints(position);
949 }
950
951 position[0] += view.getLeft();
952 position[1] += view.getTop();
953
954 final ViewParent parent = view.getParent();
955 if (parent instanceof View) {
956 view = (View) parent;
957 } else {
958 // We've reached the ViewRoot, stop iterating
959 view = null;
960 }
961 }
962 }
963
964 // We've been able to walk up the view hierarchy and the position was never clipped
965 return true;
966 }
967
968 private boolean isOffsetVisible(int offset) {
969 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -0700970 if (layout == null) return false;
971
Gilles Debunned88876a2012-03-16 17:34:04 -0700972 final int line = layout.getLineForOffset(offset);
973 final int lineBottom = layout.getLineBottom(line);
974 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
975 return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
976 lineBottom + mTextView.viewportToContentVerticalOffset());
977 }
978
979 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
980 * in the view. Returns false when the position is in the empty space of left/right of text.
981 */
982 private boolean isPositionOnText(float x, float y) {
983 Layout layout = mTextView.getLayout();
984 if (layout == null) return false;
985
986 final int line = mTextView.getLineAtCoordinate(y);
987 x = mTextView.convertToLocalHorizontalCoordinate(x);
988
989 if (x < layout.getLineLeft(line)) return false;
990 if (x > layout.getLineRight(line)) return false;
991 return true;
992 }
993
994 public boolean performLongClick(boolean handled) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100995 // Long press in empty space moves cursor and starts the insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -0700996 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
997 mInsertionControllerEnabled) {
998 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
999 mLastDownPositionY);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001000 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001001 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001002 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001003 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001004 handled = true;
1005 }
1006
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001007 if (!handled && mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001008 if (touchPositionIsInSelection()) {
1009 // Start a drag
1010 final int start = mTextView.getSelectionStart();
1011 final int end = mTextView.getSelectionEnd();
1012 CharSequence selectedText = mTextView.getTransformedText(start, end);
1013 ClipData data = ClipData.newPlainText(null, selectedText);
1014 DragLocalState localState = new DragLocalState(mTextView, start, end);
Vladislav Kaznacheevc3debf22015-03-18 16:28:06 -07001015 mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState,
1016 View.DRAG_FLAG_GLOBAL);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001017 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001018 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001019 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001020 selectCurrentWordAndStartDrag();
Gilles Debunned88876a2012-03-16 17:34:04 -07001021 }
1022 handled = true;
1023 }
1024
1025 // Start a new selection
1026 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001027 handled = selectCurrentWordAndStartDrag();
Gilles Debunned88876a2012-03-16 17:34:04 -07001028 }
1029
1030 return handled;
1031 }
1032
1033 private long getLastTouchOffsets() {
1034 SelectionModifierCursorController selectionController = getSelectionController();
1035 final int minOffset = selectionController.getMinTouchOffset();
1036 final int maxOffset = selectionController.getMaxTouchOffset();
1037 return TextUtils.packRangeInLong(minOffset, maxOffset);
1038 }
1039
1040 void onFocusChanged(boolean focused, int direction) {
1041 mShowCursor = SystemClock.uptimeMillis();
1042 ensureEndedBatchEdit();
1043
1044 if (focused) {
1045 int selStart = mTextView.getSelectionStart();
1046 int selEnd = mTextView.getSelectionEnd();
1047
1048 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1049 // mode for these, unless there was a specific selection already started.
1050 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
1051 selEnd == mTextView.getText().length();
1052
1053 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
1054 !isFocusHighlighted;
1055
1056 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1057 // If a tap was used to give focus to that view, move cursor at tap position.
1058 // Has to be done before onTakeFocus, which can be overloaded.
1059 final int lastTapPosition = getLastTapPosition();
1060 if (lastTapPosition >= 0) {
1061 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1062 }
1063
1064 // Note this may have to be moved out of the Editor class
1065 MovementMethod mMovement = mTextView.getMovementMethod();
1066 if (mMovement != null) {
1067 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1068 }
1069
1070 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1071 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1072 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1073 // This special case ensure that we keep current selection in that case.
1074 // It would be better to know why the DecorView does not have focus at that time.
1075 if (((mTextView instanceof ExtractEditText) || mSelectionMoved) &&
1076 selStart >= 0 && selEnd >= 0) {
1077 /*
1078 * Someone intentionally set the selection, so let them
1079 * do whatever it is that they wanted to do instead of
1080 * the default on-focus behavior. We reset the selection
1081 * here instead of just skipping the onTakeFocus() call
1082 * because some movement methods do something other than
1083 * just setting the selection in theirs and we still
1084 * need to go through that path.
1085 */
1086 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1087 }
1088
1089 if (mSelectAllOnFocus) {
1090 mTextView.selectAllText();
1091 }
1092
1093 mTouchFocusSelected = true;
1094 }
1095
1096 mFrozenWithFocus = false;
1097 mSelectionMoved = false;
1098
1099 if (mError != null) {
1100 showError();
1101 }
1102
1103 makeBlink();
1104 } else {
1105 if (mError != null) {
1106 hideError();
1107 }
1108 // Don't leave us in the middle of a batch edit.
1109 mTextView.onEndBatchEdit();
1110
1111 if (mTextView instanceof ExtractEditText) {
1112 // terminateTextSelectionMode removes selection, which we want to keep when
1113 // ExtractEditText goes out of focus.
1114 final int selStart = mTextView.getSelectionStart();
1115 final int selEnd = mTextView.getSelectionEnd();
1116 hideControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001117 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001118 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1119 } else {
Adam Powell057a5852012-05-11 10:28:38 -07001120 if (mTemporaryDetach) mPreserveDetachedSelection = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001121 hideControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001122 stopTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -07001123 if (mTemporaryDetach) mPreserveDetachedSelection = false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001124 downgradeEasyCorrectionSpans();
1125 }
1126
1127 // No need to create the controller
1128 if (mSelectionModifierCursorController != null) {
1129 mSelectionModifierCursorController.resetTouchOffsets();
1130 }
1131 }
1132 }
1133
1134 /**
1135 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1136 * span.
1137 */
1138 private void downgradeEasyCorrectionSpans() {
1139 CharSequence text = mTextView.getText();
1140 if (text instanceof Spannable) {
1141 Spannable spannable = (Spannable) text;
1142 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1143 spannable.length(), SuggestionSpan.class);
1144 for (int i = 0; i < suggestionSpans.length; i++) {
1145 int flags = suggestionSpans[i].getFlags();
1146 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1147 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1148 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1149 suggestionSpans[i].setFlags(flags);
1150 }
1151 }
1152 }
1153 }
1154
1155 void sendOnTextChanged(int start, int after) {
1156 updateSpellCheckSpans(start, start + after, false);
1157
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001158 // Flip flag to indicate the word iterator needs to have the text reset.
1159 mUpdateWordIteratorText = true;
1160
Gilles Debunned88876a2012-03-16 17:34:04 -07001161 // Hide the controllers as soon as text is modified (typing, procedural...)
1162 // We do not hide the span controllers, since they can be added when a new text is
1163 // inserted into the text view (voice IME).
1164 hideCursorControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001165 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001166 }
1167
1168 private int getLastTapPosition() {
1169 // No need to create the controller at that point, no last tap position saved
1170 if (mSelectionModifierCursorController != null) {
1171 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1172 if (lastTapPosition >= 0) {
1173 // Safety check, should not be possible.
1174 if (lastTapPosition > mTextView.getText().length()) {
1175 lastTapPosition = mTextView.getText().length();
1176 }
1177 return lastTapPosition;
1178 }
1179 }
1180
1181 return -1;
1182 }
1183
1184 void onWindowFocusChanged(boolean hasWindowFocus) {
1185 if (hasWindowFocus) {
1186 if (mBlink != null) {
1187 mBlink.uncancel();
1188 makeBlink();
1189 }
1190 } else {
1191 if (mBlink != null) {
1192 mBlink.cancel();
1193 }
1194 if (mInputContentType != null) {
1195 mInputContentType.enterDown = false;
1196 }
1197 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
1198 hideControllers();
1199 if (mSuggestionsPopupWindow != null) {
1200 mSuggestionsPopupWindow.onParentLostFocus();
1201 }
1202
Gilles Debunnec72fba82012-06-26 14:47:07 -07001203 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1204 ensureEndedBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001205 }
1206 }
1207
1208 void onTouchEvent(MotionEvent event) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001209 updateFloatingToolbarVisibility(event);
1210
Gilles Debunned88876a2012-03-16 17:34:04 -07001211 if (hasSelectionController()) {
1212 getSelectionController().onTouchEvent(event);
1213 }
1214
1215 if (mShowSuggestionRunnable != null) {
1216 mTextView.removeCallbacks(mShowSuggestionRunnable);
1217 mShowSuggestionRunnable = null;
1218 }
1219
1220 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1221 mLastDownPositionX = event.getX();
1222 mLastDownPositionY = event.getY();
1223
1224 // Reset this state; it will be re-set if super.onTouchEvent
1225 // causes focus to move to the view.
1226 mTouchFocusSelected = false;
1227 mIgnoreActionUpEvent = false;
1228 }
1229 }
1230
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001231 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001232 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001233 switch (event.getActionMasked()) {
1234 case MotionEvent.ACTION_MOVE:
1235 hideFloatingToolbar();
1236 break;
1237 case MotionEvent.ACTION_UP: // fall through
1238 case MotionEvent.ACTION_CANCEL:
1239 showFloatingToolbar();
1240 }
1241 }
1242 }
1243
1244 private void hideFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001245 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001246 mTextView.removeCallbacks(mShowFloatingToolbar);
1247 // Delay the "hide" a little bit just in case a "show" will happen almost immediately.
1248 mTextView.postDelayed(mHideFloatingToolbar, 100);
1249 }
1250 }
1251
1252 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001253 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001254 mTextView.removeCallbacks(mHideFloatingToolbar);
1255 // Delay "show" so it doesn't interfere with click confirmations
1256 // or double-clicks that could "dismiss" the floating toolbar.
1257 int delay = ViewConfiguration.getDoubleTapTimeout();
1258 mTextView.postDelayed(mShowFloatingToolbar, delay);
1259 }
1260 }
1261
Gilles Debunned88876a2012-03-16 17:34:04 -07001262 public void beginBatchEdit() {
1263 mInBatchEditControllers = true;
1264 final InputMethodState ims = mInputMethodState;
1265 if (ims != null) {
1266 int nesting = ++ims.mBatchEditNesting;
1267 if (nesting == 1) {
1268 ims.mCursorChanged = false;
1269 ims.mChangedDelta = 0;
1270 if (ims.mContentChanged) {
1271 // We already have a pending change from somewhere else,
1272 // so turn this into a full update.
1273 ims.mChangedStart = 0;
1274 ims.mChangedEnd = mTextView.getText().length();
1275 } else {
1276 ims.mChangedStart = EXTRACT_UNKNOWN;
1277 ims.mChangedEnd = EXTRACT_UNKNOWN;
1278 ims.mContentChanged = false;
1279 }
James Cook48e0fac2015-02-25 15:44:51 -08001280 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001281 mTextView.onBeginBatchEdit();
1282 }
1283 }
1284 }
1285
1286 public void endBatchEdit() {
1287 mInBatchEditControllers = false;
1288 final InputMethodState ims = mInputMethodState;
1289 if (ims != null) {
1290 int nesting = --ims.mBatchEditNesting;
1291 if (nesting == 0) {
1292 finishBatchEdit(ims);
1293 }
1294 }
1295 }
1296
1297 void ensureEndedBatchEdit() {
1298 final InputMethodState ims = mInputMethodState;
1299 if (ims != null && ims.mBatchEditNesting != 0) {
1300 ims.mBatchEditNesting = 0;
1301 finishBatchEdit(ims);
1302 }
1303 }
1304
1305 void finishBatchEdit(final InputMethodState ims) {
1306 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001307 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001308
1309 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1310 mTextView.updateAfterEdit();
1311 reportExtractedText();
1312 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001313 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001314 mTextView.invalidateCursor();
1315 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001316 // sendUpdateSelection knows to avoid sending if the selection did
1317 // not actually change.
1318 sendUpdateSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001319 }
1320
1321 static final int EXTRACT_NOTHING = -2;
1322 static final int EXTRACT_UNKNOWN = -1;
1323
1324 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1325 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1326 EXTRACT_UNKNOWN, outText);
1327 }
1328
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001329 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001330 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001331 @Nullable ExtractedText outText) {
1332 if (request == null || outText == null) {
1333 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001334 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001335
1336 final CharSequence content = mTextView.getText();
1337 if (content == null) {
1338 return false;
1339 }
1340
1341 if (partialStartOffset != EXTRACT_NOTHING) {
1342 final int N = content.length();
1343 if (partialStartOffset < 0) {
1344 outText.partialStartOffset = outText.partialEndOffset = -1;
1345 partialStartOffset = 0;
1346 partialEndOffset = N;
1347 } else {
1348 // Now use the delta to determine the actual amount of text
1349 // we need.
1350 partialEndOffset += delta;
1351 // Adjust offsets to ensure we contain full spans.
1352 if (content instanceof Spanned) {
1353 Spanned spanned = (Spanned)content;
1354 Object[] spans = spanned.getSpans(partialStartOffset,
1355 partialEndOffset, ParcelableSpan.class);
1356 int i = spans.length;
1357 while (i > 0) {
1358 i--;
1359 int j = spanned.getSpanStart(spans[i]);
1360 if (j < partialStartOffset) partialStartOffset = j;
1361 j = spanned.getSpanEnd(spans[i]);
1362 if (j > partialEndOffset) partialEndOffset = j;
1363 }
1364 }
1365 outText.partialStartOffset = partialStartOffset;
1366 outText.partialEndOffset = partialEndOffset - delta;
1367
1368 if (partialStartOffset > N) {
1369 partialStartOffset = N;
1370 } else if (partialStartOffset < 0) {
1371 partialStartOffset = 0;
1372 }
1373 if (partialEndOffset > N) {
1374 partialEndOffset = N;
1375 } else if (partialEndOffset < 0) {
1376 partialEndOffset = 0;
1377 }
1378 }
1379 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
1380 outText.text = content.subSequence(partialStartOffset,
1381 partialEndOffset);
1382 } else {
1383 outText.text = TextUtils.substring(content, partialStartOffset,
1384 partialEndOffset);
1385 }
1386 } else {
1387 outText.partialStartOffset = 0;
1388 outText.partialEndOffset = 0;
1389 outText.text = "";
1390 }
1391 outText.flags = 0;
1392 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1393 outText.flags |= ExtractedText.FLAG_SELECTING;
1394 }
1395 if (mTextView.isSingleLine()) {
1396 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1397 }
1398 outText.startOffset = 0;
1399 outText.selectionStart = mTextView.getSelectionStart();
1400 outText.selectionEnd = mTextView.getSelectionEnd();
1401 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001402 }
1403
1404 boolean reportExtractedText() {
1405 final Editor.InputMethodState ims = mInputMethodState;
1406 if (ims != null) {
1407 final boolean contentChanged = ims.mContentChanged;
1408 if (contentChanged || ims.mSelectionModeChanged) {
1409 ims.mContentChanged = false;
1410 ims.mSelectionModeChanged = false;
Gilles Debunnec62589c2012-04-12 14:50:23 -07001411 final ExtractedTextRequest req = ims.mExtractedTextRequest;
Gilles Debunned88876a2012-03-16 17:34:04 -07001412 if (req != null) {
1413 InputMethodManager imm = InputMethodManager.peekInstance();
1414 if (imm != null) {
1415 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1416 "Retrieving extracted start=" + ims.mChangedStart +
1417 " end=" + ims.mChangedEnd +
1418 " delta=" + ims.mChangedDelta);
1419 if (ims.mChangedStart < 0 && !contentChanged) {
1420 ims.mChangedStart = EXTRACT_NOTHING;
1421 }
1422 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
Gilles Debunnec62589c2012-04-12 14:50:23 -07001423 ims.mChangedDelta, ims.mExtractedText)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001424 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1425 "Reporting extracted start=" +
Gilles Debunnec62589c2012-04-12 14:50:23 -07001426 ims.mExtractedText.partialStartOffset +
1427 " end=" + ims.mExtractedText.partialEndOffset +
1428 ": " + ims.mExtractedText.text);
1429
1430 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
Gilles Debunned88876a2012-03-16 17:34:04 -07001431 ims.mChangedStart = EXTRACT_UNKNOWN;
1432 ims.mChangedEnd = EXTRACT_UNKNOWN;
1433 ims.mChangedDelta = 0;
1434 ims.mContentChanged = false;
1435 return true;
1436 }
1437 }
1438 }
1439 }
1440 }
1441 return false;
1442 }
1443
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001444 private void sendUpdateSelection() {
1445 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1446 final InputMethodManager imm = InputMethodManager.peekInstance();
1447 if (null != imm) {
1448 final int selectionStart = mTextView.getSelectionStart();
1449 final int selectionEnd = mTextView.getSelectionEnd();
1450 int candStart = -1;
1451 int candEnd = -1;
1452 if (mTextView.getText() instanceof Spannable) {
1453 final Spannable sp = (Spannable) mTextView.getText();
1454 candStart = EditableInputConnection.getComposingSpanStart(sp);
1455 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1456 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001457 // InputMethodManager#updateSelection skips sending the message if
1458 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001459 imm.updateSelection(mTextView,
1460 selectionStart, selectionEnd, candStart, candEnd);
1461 }
1462 }
1463 }
1464
Gilles Debunned88876a2012-03-16 17:34:04 -07001465 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1466 int cursorOffsetVertical) {
1467 final int selectionStart = mTextView.getSelectionStart();
1468 final int selectionEnd = mTextView.getSelectionEnd();
1469
1470 final InputMethodState ims = mInputMethodState;
1471 if (ims != null && ims.mBatchEditNesting == 0) {
1472 InputMethodManager imm = InputMethodManager.peekInstance();
1473 if (imm != null) {
1474 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001475 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1476 // We are in extract mode and the content has changed
1477 // in some way... just report complete new text to the
1478 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001479 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001480 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001481 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001482 }
1483 }
1484
1485 if (mCorrectionHighlighter != null) {
1486 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1487 }
1488
1489 if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
1490 drawCursor(canvas, cursorOffsetVertical);
1491 // Rely on the drawable entirely, do not draw the cursor line.
1492 // Has to be done after the IMM related code above which relies on the highlight.
1493 highlight = null;
1494 }
1495
1496 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1497 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1498 cursorOffsetVertical);
1499 } else {
1500 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1501 }
1502 }
1503
1504 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1505 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001506 final long lineRange = layout.getLineRangeForDraw(canvas);
1507 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1508 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1509 if (lastLine < 0) return;
1510
1511 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1512 firstLine, lastLine);
1513
1514 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001515 if (mTextRenderNodes == null) {
1516 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001517 }
1518
1519 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001520 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001521 int[] blockIndices = dynamicLayout.getBlockIndices();
1522 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001523 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001524
Gilles Debunned88876a2012-03-16 17:34:04 -07001525 int endOfPreviousBlock = -1;
1526 int searchStartIndex = 0;
1527 for (int i = 0; i < numberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001528 int blockEndLine = blockEndLines[i];
Gilles Debunned88876a2012-03-16 17:34:04 -07001529 int blockIndex = blockIndices[i];
1530
1531 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1532 if (blockIsInvalid) {
1533 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1534 searchStartIndex);
Gilles Debunne157aafc2012-04-19 17:21:57 -07001535 // Note how dynamic layout's internal block indices get updated from Editor
Gilles Debunned88876a2012-03-16 17:34:04 -07001536 blockIndices[i] = blockIndex;
Chris Craik956f3402015-04-27 16:41:00 -07001537 if (mTextRenderNodes[blockIndex] != null) {
1538 mTextRenderNodes[blockIndex].isDirty = true;
Raph Levienbb2397c2015-02-12 16:16:49 -08001539 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001540 searchStartIndex = blockIndex + 1;
1541 }
1542
Chris Craik956f3402015-04-27 16:41:00 -07001543 if (mTextRenderNodes[blockIndex] == null) {
1544 mTextRenderNodes[blockIndex] =
1545 new TextRenderNode("Text " + blockIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001546 }
1547
Chris Craik956f3402015-04-27 16:41:00 -07001548 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1549 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
Sangkyu Lee955beb22012-12-10 15:47:00 +09001550 if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) {
Gilles Debunne157aafc2012-04-19 17:21:57 -07001551 final int blockBeginLine = endOfPreviousBlock + 1;
1552 final int top = layout.getLineTop(blockBeginLine);
1553 final int bottom = layout.getLineBottom(blockEndLine);
Gilles Debunnefd5bc012012-04-23 16:21:35 -07001554 int left = 0;
1555 int right = mTextView.getWidth();
1556 if (mTextView.getHorizontallyScrolling()) {
1557 float min = Float.MAX_VALUE;
1558 float max = Float.MIN_VALUE;
1559 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1560 min = Math.min(min, layout.getLineLeft(line));
1561 max = Math.max(max, layout.getLineRight(line));
1562 }
1563 left = (int) min;
1564 right = (int) (max + 0.5f);
1565 }
Gilles Debunne157aafc2012-04-19 17:21:57 -07001566
Sangkyu Lee955beb22012-12-10 15:47:00 +09001567 // Rebuild display list if it is invalid
1568 if (blockDisplayListIsInvalid) {
Chris Craikf6829a02015-03-10 10:28:59 -07001569 final DisplayListCanvas displayListCanvas = blockDisplayList.start(
Romain Guy52036b12013-02-14 18:03:37 -08001570 right - left, bottom - top);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001571 try {
Romain Guy52036b12013-02-14 18:03:37 -08001572 // drawText is always relative to TextView's origin, this translation
1573 // brings this range of text back to the top left corner of the viewport
Chris Craikf6829a02015-03-10 10:28:59 -07001574 displayListCanvas.translate(-left, -top);
1575 layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
Chris Craik956f3402015-04-27 16:41:00 -07001576 mTextRenderNodes[blockIndex].isDirty = false;
Romain Guy52036b12013-02-14 18:03:37 -08001577 // No need to untranslate, previous context is popped after
1578 // drawDisplayList
Sangkyu Lee955beb22012-12-10 15:47:00 +09001579 } finally {
Chris Craikf6829a02015-03-10 10:28:59 -07001580 blockDisplayList.end(displayListCanvas);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001581 // Same as drawDisplayList below, handled by our TextView's parent
Chet Haasedd671592013-04-19 14:54:34 -07001582 blockDisplayList.setClipToBounds(false);
Sangkyu Lee955beb22012-12-10 15:47:00 +09001583 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001584 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001585
1586 // Valid disply list whose index is >= indexFirstChangedBlock
1587 // only needs to update its drawing location.
1588 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
Gilles Debunned88876a2012-03-16 17:34:04 -07001589 }
1590
Chris Craik956f3402015-04-27 16:41:00 -07001591 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
Gilles Debunnefd5bc012012-04-23 16:21:35 -07001592
Gilles Debunne157aafc2012-04-19 17:21:57 -07001593 endOfPreviousBlock = blockEndLine;
Gilles Debunned88876a2012-03-16 17:34:04 -07001594 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001595
1596 dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks);
Gilles Debunned88876a2012-03-16 17:34:04 -07001597 } else {
1598 // Boring layout is used for empty and hint text
1599 layout.drawText(canvas, firstLine, lastLine);
1600 }
1601 }
1602
1603 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1604 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001605 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001606 for (int i = searchStartIndex; i < length; i++) {
1607 boolean blockIndexFound = false;
1608 for (int j = 0; j < numberOfBlocks; j++) {
1609 if (blockIndices[j] == i) {
1610 blockIndexFound = true;
1611 break;
1612 }
1613 }
1614 if (blockIndexFound) continue;
1615 return i;
1616 }
1617
1618 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001619 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001620 return length;
1621 }
1622
1623 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1624 final boolean translate = cursorOffsetVertical != 0;
1625 if (translate) canvas.translate(0, cursorOffsetVertical);
1626 for (int i = 0; i < mCursorCount; i++) {
1627 mCursorDrawable[i].draw(canvas);
1628 }
1629 if (translate) canvas.translate(0, -cursorOffsetVertical);
1630 }
1631
Gilles Debunneebc86af2012-04-20 15:10:47 -07001632 /**
1633 * Invalidates all the sub-display lists that overlap the specified character range
1634 */
1635 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07001636 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07001637 final int firstLine = layout.getLineForOffset(start);
1638 final int lastLine = layout.getLineForOffset(end);
1639
1640 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1641 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1642 int[] blockIndices = dynamicLayout.getBlockIndices();
1643 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1644
1645 int i = 0;
1646 // Skip the blocks before firstLine
1647 while (i < numberOfBlocks) {
1648 if (blockEndLines[i] >= firstLine) break;
1649 i++;
1650 }
1651
1652 // Invalidate all subsequent blocks until lastLine is passed
1653 while (i < numberOfBlocks) {
1654 final int blockIndex = blockIndices[i];
1655 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07001656 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07001657 }
1658 if (blockEndLines[i] >= lastLine) break;
1659 i++;
1660 }
1661 }
1662 }
1663
Gilles Debunned88876a2012-03-16 17:34:04 -07001664 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07001665 if (mTextRenderNodes != null) {
1666 for (int i = 0; i < mTextRenderNodes.length; i++) {
1667 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001668 }
1669 }
1670 }
1671
1672 void updateCursorsPositions() {
1673 if (mTextView.mCursorDrawableRes == 0) {
1674 mCursorCount = 0;
1675 return;
1676 }
1677
1678 Layout layout = mTextView.getLayout();
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001679 Layout hintLayout = mTextView.getHintLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07001680 final int offset = mTextView.getSelectionStart();
1681 final int line = layout.getLineForOffset(offset);
1682 final int top = layout.getLineTop(line);
1683 final int bottom = layout.getLineTop(line + 1);
1684
1685 mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
1686
1687 int middle = bottom;
1688 if (mCursorCount == 2) {
1689 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
1690 middle = (top + bottom) >> 1;
1691 }
1692
Raph Levienafe8e9b2012-12-19 16:09:32 -08001693 boolean clamped = layout.shouldClampCursor(line);
1694 updateCursorPosition(0, top, middle,
1695 getPrimaryHorizontal(layout, hintLayout, offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001696
1697 if (mCursorCount == 2) {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001698 updateCursorPosition(1, middle, bottom,
1699 layout.getSecondaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001700 }
1701 }
1702
Raph Levienafe8e9b2012-12-19 16:09:32 -08001703 private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset,
1704 boolean clamped) {
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001705 if (TextUtils.isEmpty(layout.getText()) &&
1706 hintLayout != null &&
1707 !TextUtils.isEmpty(hintLayout.getText())) {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001708 return hintLayout.getPrimaryHorizontal(offset, clamped);
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001709 } else {
Raph Levienafe8e9b2012-12-19 16:09:32 -08001710 return layout.getPrimaryHorizontal(offset, clamped);
Fabrice Di Meglio0ed59fa2012-05-29 20:32:51 -07001711 }
1712 }
1713
Gilles Debunned88876a2012-03-16 17:34:04 -07001714 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001715 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07001716 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001717 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001718 if (mInsertionActionModeRunnable != null) {
1719 mTextView.removeCallbacks(mInsertionActionModeRunnable);
1720 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001721 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001722 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001723 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001724 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001725
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001726 ActionMode.Callback actionModeCallback =
1727 new TextActionModeCallback(false /* hasSelection */);
1728 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01001729 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001730 if (mTextActionMode != null && getInsertionController() != null) {
1731 getInsertionController().show();
1732 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001733 }
1734
1735 /**
Clara Bayarri578286f2015-04-10 15:35:31 +01001736 * Starts a Selection Action Mode with the current selection and ensures the selection handles
Clara Bayarridfac4432015-05-15 12:18:24 +01001737 * are shown if there is a selection, otherwise the insertion handle is shown. This should be
1738 * used when the mode is started from a non-touch event.
Clara Bayarri578286f2015-04-10 15:35:31 +01001739 *
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001740 * @return true if the selection mode was actually started.
1741 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001742 boolean startSelectionActionMode() {
1743 boolean selectionStarted = startSelectionActionModeInternal();
Clara Bayarri578286f2015-04-10 15:35:31 +01001744 if (selectionStarted) {
1745 getSelectionController().show();
Clara Bayarridfac4432015-05-15 12:18:24 +01001746 } else if (getInsertionController() != null) {
1747 getInsertionController().show();
Clara Bayarri578286f2015-04-10 15:35:31 +01001748 }
1749 return selectionStarted;
1750 }
1751
Clara Bayarridfac4432015-05-15 12:18:24 +01001752 /**
1753 * If the TextView allows text selection, selects the current word when no existing selection
1754 * was available and starts a drag.
1755 *
1756 * @return true if the drag was started.
1757 */
1758 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01001759 if (mInsertionActionModeRunnable != null) {
1760 mTextView.removeCallbacks(mInsertionActionModeRunnable);
1761 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001762 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001763 return false;
1764 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001765 if (mTextActionMode != null) {
1766 mTextActionMode.finish();
Clara Bayarridfac4432015-05-15 12:18:24 +01001767 }
1768 if (!checkFieldAndSelectCurrentWord()) {
1769 return false;
1770 }
Clara Bayarri01243ac2015-06-03 00:46:29 +01001771
1772 // Avoid dismissing the selection if it exists.
1773 mPreserveDetachedSelection = true;
1774 stopTextActionMode();
1775 mPreserveDetachedSelection = false;
1776
Clara Bayarridfac4432015-05-15 12:18:24 +01001777 getSelectionController().enterDrag();
1778 return true;
1779 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001780
Clara Bayarridfac4432015-05-15 12:18:24 +01001781 /**
1782 * Checks whether a selection can be performed on the current TextView and if so selects
1783 * the current word.
1784 *
1785 * @return true if there already was a selection or if the current word was selected.
1786 */
1787 private boolean checkFieldAndSelectCurrentWord() {
1788 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
1789 Log.w(TextView.LOG_TAG,
1790 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001791 return false;
1792 }
1793
Clara Bayarridfac4432015-05-15 12:18:24 +01001794 if (!mTextView.hasSelection()) {
1795 // There may already be a selection on device rotation
1796 return selectCurrentWord();
1797 }
1798 return true;
1799 }
1800
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001801 private boolean startSelectionActionModeInternal() {
1802 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001803 // Text action mode is already started
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001804 mTextActionMode.invalidate();
Gilles Debunned88876a2012-03-16 17:34:04 -07001805 return false;
1806 }
1807
Clara Bayarridfac4432015-05-15 12:18:24 +01001808 if (!checkFieldAndSelectCurrentWord()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001809 return false;
1810 }
1811
Gilles Debunned88876a2012-03-16 17:34:04 -07001812 boolean willExtract = extractedTextModeWillBeStarted();
1813
1814 // Do not start the action mode when extracted text will show up full screen, which would
1815 // immediately hide the newly created action bar and would be visually distracting.
1816 if (!willExtract) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001817 ActionMode.Callback actionModeCallback =
1818 new TextActionModeCallback(true /* hasSelection */);
1819 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01001820 actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07001821 }
1822
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001823 final boolean selectionStarted = mTextActionMode != null || willExtract;
Gilles Debunne3473b2b2012-04-20 16:21:10 -07001824 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001825 // Show the IME to be able to replace text, except when selecting non editable text.
1826 final InputMethodManager imm = InputMethodManager.peekInstance();
1827 if (imm != null) {
1828 imm.showSoftInput(mTextView, 0, null);
1829 }
1830 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001831 return selectionStarted;
1832 }
1833
1834 private boolean extractedTextModeWillBeStarted() {
1835 if (!(mTextView instanceof ExtractEditText)) {
1836 final InputMethodManager imm = InputMethodManager.peekInstance();
1837 return imm != null && imm.isFullscreenMode();
1838 }
1839 return false;
1840 }
1841
1842 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09001843 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
1844 * the current cursor position or selection range. This method is consistent with the
1845 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07001846 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09001847 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07001848 CharSequence text = mTextView.getText();
1849 if (!(text instanceof Spannable)) return false;
1850
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09001851 final Spannable spannable = (Spannable) text;
1852 final int selectionStart = mTextView.getSelectionStart();
1853 final int selectionEnd = mTextView.getSelectionEnd();
1854 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
1855 SuggestionSpan.class);
1856 if (suggestionSpans.length == 0) {
1857 return false;
1858 }
1859 if (selectionStart == selectionEnd) {
1860 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09001861 for (int i = 0; i < suggestionSpans.length; i++) {
1862 if (suggestionSpans[i].getSuggestions().length > 0) {
1863 return true;
1864 }
1865 }
1866 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09001867 }
1868 int minSpanStart = mTextView.getText().length();
1869 int maxSpanEnd = 0;
1870 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
1871 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09001872 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09001873 for (int i = 0; i < suggestionSpans.length; i++) {
1874 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
1875 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
1876 minSpanStart = Math.min(minSpanStart, spanStart);
1877 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
1878 if (selectionStart < spanStart || selectionStart > spanEnd) {
1879 // The span doesn't cover the current selection start point.
1880 continue;
1881 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09001882 hasValidSuggestions =
1883 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09001884 unionOfSpansCoveringSelectionStartStart =
1885 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
1886 unionOfSpansCoveringSelectionStartEnd =
1887 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
1888 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09001889 if (!hasValidSuggestions) {
1890 return false;
1891 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09001892 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
1893 // No spans cover the selection start point.
1894 return false;
1895 }
1896 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
1897 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
1898 // There is a span that is not covered by the union. In this case, we soouldn't offer
1899 // to show suggestions as it's confusing.
1900 return false;
1901 }
1902 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001903 }
1904
1905 /**
1906 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
1907 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
1908 */
1909 private boolean isCursorInsideEasyCorrectionSpan() {
1910 Spannable spannable = (Spannable) mTextView.getText();
1911 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
1912 mTextView.getSelectionEnd(), SuggestionSpan.class);
1913 for (int i = 0; i < suggestionSpans.length; i++) {
1914 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
1915 return true;
1916 }
1917 }
1918 return false;
1919 }
1920
1921 void onTouchUpEvent(MotionEvent event) {
1922 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
1923 hideControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001924 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001925 CharSequence text = mTextView.getText();
1926 if (!selectAllGotFocus && text.length() > 0) {
1927 // Move cursor
1928 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1929 Selection.setSelection((Spannable) text, offset);
1930 if (mSpellChecker != null) {
1931 // When the cursor moves, the word that was typed may need spell check
1932 mSpellChecker.onSelectionChanged();
1933 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01001934
Gilles Debunned88876a2012-03-16 17:34:04 -07001935 if (!extractedTextModeWillBeStarted()) {
1936 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01001937 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001938 if (mInsertionActionModeRunnable != null) {
1939 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01001940 }
1941
Gilles Debunned88876a2012-03-16 17:34:04 -07001942 mShowSuggestionRunnable = new Runnable() {
1943 public void run() {
1944 showSuggestions();
1945 }
1946 };
1947 // removeCallbacks is performed on every touch
1948 mTextView.postDelayed(mShowSuggestionRunnable,
1949 ViewConfiguration.getDoubleTapTimeout());
1950 } else if (hasInsertionController()) {
1951 getInsertionController().show();
1952 }
1953 }
1954 }
1955 }
1956
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001957 protected void stopTextActionMode() {
1958 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001959 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001960 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07001961 }
1962 }
1963
1964 /**
1965 * @return True if this view supports insertion handles.
1966 */
1967 boolean hasInsertionController() {
1968 return mInsertionControllerEnabled;
1969 }
1970
1971 /**
1972 * @return True if this view supports selection handles.
1973 */
1974 boolean hasSelectionController() {
1975 return mSelectionControllerEnabled;
1976 }
1977
1978 InsertionPointCursorController getInsertionController() {
1979 if (!mInsertionControllerEnabled) {
1980 return null;
1981 }
1982
1983 if (mInsertionPointCursorController == null) {
1984 mInsertionPointCursorController = new InsertionPointCursorController();
1985
1986 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1987 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
1988 }
1989
1990 return mInsertionPointCursorController;
1991 }
1992
1993 SelectionModifierCursorController getSelectionController() {
1994 if (!mSelectionControllerEnabled) {
1995 return null;
1996 }
1997
1998 if (mSelectionModifierCursorController == null) {
1999 mSelectionModifierCursorController = new SelectionModifierCursorController();
2000
2001 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2002 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2003 }
2004
2005 return mSelectionModifierCursorController;
2006 }
2007
2008 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
2009 if (mCursorDrawable[cursorIndex] == null)
Alan Viverette8eea3ea2014-02-03 18:40:20 -08002010 mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07002011 mTextView.mCursorDrawableRes);
2012
2013 if (mTempRect == null) mTempRect = new Rect();
2014 mCursorDrawable[cursorIndex].getPadding(mTempRect);
2015 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
2016 horizontal = Math.max(0.5f, horizontal - 0.5f);
2017 final int left = (int) (horizontal) - mTempRect.left;
2018 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
2019 bottom + mTempRect.bottom);
2020 }
2021
2022 /**
2023 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
James Cookf59152c2015-02-26 18:03:58 -08002024 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002025 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2026 * implementation flashes the background of the corrected word to provide feedback to the user.
2027 *
2028 * @param info The auto correct info about the text that was corrected.
2029 */
2030 public void onCommitCorrection(CorrectionInfo info) {
2031 if (mCorrectionHighlighter == null) {
2032 mCorrectionHighlighter = new CorrectionHighlighter();
2033 } else {
2034 mCorrectionHighlighter.invalidate(false);
2035 }
2036
2037 mCorrectionHighlighter.highlight(info);
2038 }
2039
2040 void showSuggestions() {
2041 if (mSuggestionsPopupWindow == null) {
2042 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
2043 }
2044 hideControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002045 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002046 mSuggestionsPopupWindow.show();
2047 }
2048
Gilles Debunned88876a2012-03-16 17:34:04 -07002049 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002050 if (mPositionListener != null) {
2051 mPositionListener.onScrollChanged();
2052 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002053 if (mTextActionMode != null) {
2054 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002055 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002056 }
2057
2058 /**
2059 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2060 */
2061 private boolean shouldBlink() {
2062 if (!isCursorVisible() || !mTextView.isFocused()) return false;
2063
2064 final int start = mTextView.getSelectionStart();
2065 if (start < 0) return false;
2066
2067 final int end = mTextView.getSelectionEnd();
2068 if (end < 0) return false;
2069
2070 return start == end;
2071 }
2072
2073 void makeBlink() {
2074 if (shouldBlink()) {
2075 mShowCursor = SystemClock.uptimeMillis();
2076 if (mBlink == null) mBlink = new Blink();
2077 mBlink.removeCallbacks(mBlink);
2078 mBlink.postAtTime(mBlink, mShowCursor + BLINK);
2079 } else {
2080 if (mBlink != null) mBlink.removeCallbacks(mBlink);
2081 }
2082 }
2083
2084 private class Blink extends Handler implements Runnable {
2085 private boolean mCancelled;
2086
2087 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002088 if (mCancelled) {
2089 return;
2090 }
2091
2092 removeCallbacks(Blink.this);
2093
2094 if (shouldBlink()) {
2095 if (mTextView.getLayout() != null) {
2096 mTextView.invalidateCursorPath();
2097 }
2098
2099 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
2100 }
2101 }
2102
2103 void cancel() {
2104 if (!mCancelled) {
2105 removeCallbacks(Blink.this);
2106 mCancelled = true;
2107 }
2108 }
2109
2110 void uncancel() {
2111 mCancelled = false;
2112 }
2113 }
2114
2115 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
2116 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2117 com.android.internal.R.layout.text_drag_thumbnail, null);
2118
2119 if (shadowView == null) {
2120 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2121 }
2122
2123 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2124 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
2125 }
2126 shadowView.setText(text);
2127 shadowView.setTextColor(mTextView.getTextColors());
2128
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002129 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002130 shadowView.setGravity(Gravity.CENTER);
2131
2132 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2133 ViewGroup.LayoutParams.WRAP_CONTENT));
2134
2135 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2136 shadowView.measure(size, size);
2137
2138 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2139 shadowView.invalidate();
2140 return new DragShadowBuilder(shadowView);
2141 }
2142
2143 private static class DragLocalState {
2144 public TextView sourceTextView;
2145 public int start, end;
2146
2147 public DragLocalState(TextView sourceTextView, int start, int end) {
2148 this.sourceTextView = sourceTextView;
2149 this.start = start;
2150 this.end = end;
2151 }
2152 }
2153
2154 void onDrop(DragEvent event) {
2155 StringBuilder content = new StringBuilder("");
2156 ClipData clipData = event.getClipData();
2157 final int itemCount = clipData.getItemCount();
2158 for (int i=0; i < itemCount; i++) {
2159 Item item = clipData.getItemAt(i);
Dianne Hackbornacb69bb2012-04-13 15:36:06 -07002160 content.append(item.coerceToStyledText(mTextView.getContext()));
Gilles Debunned88876a2012-03-16 17:34:04 -07002161 }
2162
2163 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2164
2165 Object localState = event.getLocalState();
2166 DragLocalState dragLocalState = null;
2167 if (localState instanceof DragLocalState) {
2168 dragLocalState = (DragLocalState) localState;
2169 }
2170 boolean dragDropIntoItself = dragLocalState != null &&
2171 dragLocalState.sourceTextView == mTextView;
2172
2173 if (dragDropIntoItself) {
2174 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2175 // A drop inside the original selection discards the drop.
2176 return;
2177 }
2178 }
2179
2180 final int originalLength = mTextView.getText().length();
Raph Levien5a689ce2014-09-10 11:03:18 -07002181 int min = offset;
2182 int max = offset;
Gilles Debunned88876a2012-03-16 17:34:04 -07002183
2184 Selection.setSelection((Spannable) mTextView.getText(), max);
2185 mTextView.replaceText_internal(min, max, content);
2186
2187 if (dragDropIntoItself) {
2188 int dragSourceStart = dragLocalState.start;
2189 int dragSourceEnd = dragLocalState.end;
2190 if (max <= dragSourceStart) {
2191 // Inserting text before selection has shifted positions
2192 final int shift = mTextView.getText().length() - originalLength;
2193 dragSourceStart += shift;
2194 dragSourceEnd += shift;
2195 }
2196
2197 // Delete original selection
2198 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
2199
2200 // Make sure we do not leave two adjacent spaces.
Victoria Lease91373202012-09-07 16:41:59 -07002201 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2202 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2203 if (nextCharIdx > prevCharIdx + 1) {
2204 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2205 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2206 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2207 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002208 }
2209 }
2210 }
2211
Gilles Debunnec62589c2012-04-12 14:50:23 -07002212 public void addSpanWatchers(Spannable text) {
2213 final int textLength = text.length();
2214
2215 if (mKeyListener != null) {
2216 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2217 }
2218
Jean Chalardbaf30942013-02-28 16:01:51 -08002219 if (mSpanController == null) {
2220 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002221 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002222 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002223 }
2224
Gilles Debunned88876a2012-03-16 17:34:04 -07002225 /**
2226 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2227 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002228 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002229 */
Jean Chalardbaf30942013-02-28 16:01:51 -08002230 class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002231
2232 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2233
2234 private EasyEditPopupWindow mPopupWindow;
2235
Gilles Debunned88876a2012-03-16 17:34:04 -07002236 private Runnable mHidePopup;
2237
Jean Chalardbaf30942013-02-28 16:01:51 -08002238 // This function is pure but inner classes can't have static functions
2239 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2240 final Object span) {
2241 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2242 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2243 }
2244
Gilles Debunnec62589c2012-04-12 14:50:23 -07002245 @Override
2246 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002247 if (isNonIntermediateSelectionSpan(text, span)) {
2248 sendUpdateSelection();
2249 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002250 if (mPopupWindow == null) {
2251 mPopupWindow = new EasyEditPopupWindow();
2252 mHidePopup = new Runnable() {
2253 @Override
2254 public void run() {
2255 hide();
2256 }
2257 };
2258 }
2259
2260 // Make sure there is only at most one EasyEditSpan in the text
2261 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002262 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002263 }
2264
2265 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002266 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2267 @Override
2268 public void onDeleteClick(EasyEditSpan span) {
2269 Editable editable = (Editable) mTextView.getText();
2270 int start = editable.getSpanStart(span);
2271 int end = editable.getSpanEnd(span);
2272 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002273 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002274 mTextView.deleteText_internal(start, end);
2275 }
2276 editable.removeSpan(span);
2277 }
2278 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002279
2280 if (mTextView.getWindowVisibility() != View.VISIBLE) {
2281 // The window is not visible yet, ignore the text change.
2282 return;
2283 }
2284
2285 if (mTextView.getLayout() == null) {
2286 // The view has not been laid out yet, ignore the text change
2287 return;
2288 }
2289
2290 if (extractedTextModeWillBeStarted()) {
2291 // The input is in extract mode. Do not handle the easy edit in
2292 // the original TextView, as the ExtractEditText will do
2293 return;
2294 }
2295
2296 mPopupWindow.show();
2297 mTextView.removeCallbacks(mHidePopup);
2298 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2299 }
2300 }
2301
2302 @Override
2303 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002304 if (isNonIntermediateSelectionSpan(text, span)) {
2305 sendUpdateSelection();
2306 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002307 hide();
2308 }
2309 }
2310
2311 @Override
2312 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2313 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002314 if (isNonIntermediateSelectionSpan(text, span)) {
2315 sendUpdateSelection();
2316 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002317 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08002318 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002319 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002320 }
2321 }
2322
Gilles Debunned88876a2012-03-16 17:34:04 -07002323 public void hide() {
2324 if (mPopupWindow != null) {
2325 mPopupWindow.hide();
2326 mTextView.removeCallbacks(mHidePopup);
2327 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002328 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002329
Jean Chalardbaf30942013-02-28 16:01:51 -08002330 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002331 try {
2332 PendingIntent pendingIntent = span.getPendingIntent();
2333 if (pendingIntent != null) {
2334 Intent intent = new Intent();
2335 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2336 pendingIntent.send(mTextView.getContext(), 0, intent);
2337 }
2338 } catch (CanceledException e) {
2339 // This should not happen, as we should try to send the intent only once.
2340 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2341 }
2342 }
2343 }
2344
2345 /**
2346 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2347 */
2348 private interface EasyEditDeleteListener {
2349
2350 /**
2351 * Clicks the delete pop-up.
2352 */
2353 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07002354 }
2355
2356 /**
2357 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002358 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002359 */
2360 private class EasyEditPopupWindow extends PinnedPopupWindow
2361 implements OnClickListener {
2362 private static final int POPUP_TEXT_LAYOUT =
2363 com.android.internal.R.layout.text_edit_action_popup_text;
2364 private TextView mDeleteTextView;
2365 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002366 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07002367
2368 @Override
2369 protected void createPopupWindow() {
2370 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2371 com.android.internal.R.attr.textSelectHandleWindowStyle);
2372 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2373 mPopupWindow.setClippingEnabled(true);
2374 }
2375
2376 @Override
2377 protected void initContentView() {
2378 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2379 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2380 mContentView = linearLayout;
2381 mContentView.setBackgroundResource(
2382 com.android.internal.R.drawable.text_edit_side_paste_window);
2383
2384 LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
2385 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2386
2387 LayoutParams wrapContent = new LayoutParams(
2388 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2389
2390 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2391 mDeleteTextView.setLayoutParams(wrapContent);
2392 mDeleteTextView.setText(com.android.internal.R.string.delete);
2393 mDeleteTextView.setOnClickListener(this);
2394 mContentView.addView(mDeleteTextView);
2395 }
2396
Gilles Debunnec62589c2012-04-12 14:50:23 -07002397 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002398 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07002399 }
2400
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002401 private void setOnDeleteListener(EasyEditDeleteListener listener) {
2402 mOnDeleteListener = listener;
2403 }
2404
Gilles Debunned88876a2012-03-16 17:34:04 -07002405 @Override
2406 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002407 if (view == mDeleteTextView
2408 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2409 && mOnDeleteListener != null) {
2410 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07002411 }
2412 }
2413
2414 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002415 public void hide() {
2416 if (mEasyEditSpan != null) {
2417 mEasyEditSpan.setDeleteEnabled(false);
2418 }
2419 mOnDeleteListener = null;
2420 super.hide();
2421 }
2422
2423 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07002424 protected int getTextOffset() {
2425 // Place the pop-up at the end of the span
2426 Editable editable = (Editable) mTextView.getText();
2427 return editable.getSpanEnd(mEasyEditSpan);
2428 }
2429
2430 @Override
2431 protected int getVerticalLocalPosition(int line) {
2432 return mTextView.getLayout().getLineBottom(line);
2433 }
2434
2435 @Override
2436 protected int clipVertically(int positionY) {
2437 // As we display the pop-up below the span, no vertical clipping is required.
2438 return positionY;
2439 }
2440 }
2441
2442 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
2443 // 3 handles
2444 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09002445 // 1 CursorAnchorInfoNotifier
2446 private final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07002447 private TextViewPositionListener[] mPositionListeners =
2448 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
2449 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
2450 private boolean mPositionHasChanged = true;
2451 // Absolute position of the TextView with respect to its parent window
2452 private int mPositionX, mPositionY;
2453 private int mNumberOfListeners;
2454 private boolean mScrollHasChanged;
2455 final int[] mTempCoords = new int[2];
2456
2457 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
2458 if (mNumberOfListeners == 0) {
2459 updatePosition();
2460 ViewTreeObserver vto = mTextView.getViewTreeObserver();
2461 vto.addOnPreDrawListener(this);
2462 }
2463
2464 int emptySlotIndex = -1;
2465 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2466 TextViewPositionListener listener = mPositionListeners[i];
2467 if (listener == positionListener) {
2468 return;
2469 } else if (emptySlotIndex < 0 && listener == null) {
2470 emptySlotIndex = i;
2471 }
2472 }
2473
2474 mPositionListeners[emptySlotIndex] = positionListener;
2475 mCanMove[emptySlotIndex] = canMove;
2476 mNumberOfListeners++;
2477 }
2478
2479 public void removeSubscriber(TextViewPositionListener positionListener) {
2480 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2481 if (mPositionListeners[i] == positionListener) {
2482 mPositionListeners[i] = null;
2483 mNumberOfListeners--;
2484 break;
2485 }
2486 }
2487
2488 if (mNumberOfListeners == 0) {
2489 ViewTreeObserver vto = mTextView.getViewTreeObserver();
2490 vto.removeOnPreDrawListener(this);
2491 }
2492 }
2493
2494 public int getPositionX() {
2495 return mPositionX;
2496 }
2497
2498 public int getPositionY() {
2499 return mPositionY;
2500 }
2501
2502 @Override
2503 public boolean onPreDraw() {
2504 updatePosition();
2505
2506 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2507 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
2508 TextViewPositionListener positionListener = mPositionListeners[i];
2509 if (positionListener != null) {
2510 positionListener.updatePosition(mPositionX, mPositionY,
2511 mPositionHasChanged, mScrollHasChanged);
2512 }
2513 }
2514 }
2515
2516 mScrollHasChanged = false;
2517 return true;
2518 }
2519
2520 private void updatePosition() {
2521 mTextView.getLocationInWindow(mTempCoords);
2522
2523 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
2524
2525 mPositionX = mTempCoords[0];
2526 mPositionY = mTempCoords[1];
2527 }
2528
2529 public void onScrollChanged() {
2530 mScrollHasChanged = true;
2531 }
2532 }
2533
2534 private abstract class PinnedPopupWindow implements TextViewPositionListener {
2535 protected PopupWindow mPopupWindow;
2536 protected ViewGroup mContentView;
2537 int mPositionX, mPositionY;
2538
2539 protected abstract void createPopupWindow();
2540 protected abstract void initContentView();
2541 protected abstract int getTextOffset();
2542 protected abstract int getVerticalLocalPosition(int line);
2543 protected abstract int clipVertically(int positionY);
2544
2545 public PinnedPopupWindow() {
2546 createPopupWindow();
2547
Alan Viverette80ebe0d2015-04-30 15:53:11 -07002548 mPopupWindow.setWindowLayoutType(
2549 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07002550 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
2551 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
2552
2553 initContentView();
2554
2555 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2556 ViewGroup.LayoutParams.WRAP_CONTENT);
2557 mContentView.setLayoutParams(wrapContent);
2558
2559 mPopupWindow.setContentView(mContentView);
2560 }
2561
2562 public void show() {
2563 getPositionListener().addSubscriber(this, false /* offset is fixed */);
2564
2565 computeLocalPosition();
2566
2567 final PositionListener positionListener = getPositionListener();
2568 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
2569 }
2570
2571 protected void measureContent() {
2572 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2573 mContentView.measure(
2574 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
2575 View.MeasureSpec.AT_MOST),
2576 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
2577 View.MeasureSpec.AT_MOST));
2578 }
2579
2580 /* The popup window will be horizontally centered on the getTextOffset() and vertically
2581 * positioned according to viewportToContentHorizontalOffset.
2582 *
2583 * This method assumes that mContentView has properly been measured from its content. */
2584 private void computeLocalPosition() {
2585 measureContent();
2586 final int width = mContentView.getMeasuredWidth();
2587 final int offset = getTextOffset();
2588 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
2589 mPositionX += mTextView.viewportToContentHorizontalOffset();
2590
2591 final int line = mTextView.getLayout().getLineForOffset(offset);
2592 mPositionY = getVerticalLocalPosition(line);
2593 mPositionY += mTextView.viewportToContentVerticalOffset();
2594 }
2595
2596 private void updatePosition(int parentPositionX, int parentPositionY) {
2597 int positionX = parentPositionX + mPositionX;
2598 int positionY = parentPositionY + mPositionY;
2599
2600 positionY = clipVertically(positionY);
2601
2602 // Horizontal clipping
2603 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2604 final int width = mContentView.getMeasuredWidth();
2605 positionX = Math.min(displayMetrics.widthPixels - width, positionX);
2606 positionX = Math.max(0, positionX);
2607
2608 if (isShowing()) {
2609 mPopupWindow.update(positionX, positionY, -1, -1);
2610 } else {
2611 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
2612 positionX, positionY);
2613 }
2614 }
2615
2616 public void hide() {
2617 mPopupWindow.dismiss();
2618 getPositionListener().removeSubscriber(this);
2619 }
2620
2621 @Override
2622 public void updatePosition(int parentPositionX, int parentPositionY,
2623 boolean parentPositionChanged, boolean parentScrolled) {
2624 // Either parentPositionChanged or parentScrolled is true, check if still visible
2625 if (isShowing() && isOffsetVisible(getTextOffset())) {
2626 if (parentScrolled) computeLocalPosition();
2627 updatePosition(parentPositionX, parentPositionY);
2628 } else {
2629 hide();
2630 }
2631 }
2632
2633 public boolean isShowing() {
2634 return mPopupWindow.isShowing();
2635 }
2636 }
2637
2638 private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
2639 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
2640 private static final int ADD_TO_DICTIONARY = -1;
2641 private static final int DELETE_TEXT = -2;
2642 private SuggestionInfo[] mSuggestionInfos;
2643 private int mNumberOfSuggestions;
2644 private boolean mCursorWasVisibleBeforeSuggestions;
2645 private boolean mIsShowingUp = false;
2646 private SuggestionAdapter mSuggestionsAdapter;
2647 private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
2648 private final HashMap<SuggestionSpan, Integer> mSpansLengths;
2649
2650 private class CustomPopupWindow extends PopupWindow {
Alan Viverette617feb92013-09-09 18:09:13 -07002651 public CustomPopupWindow(Context context, int defStyleAttr) {
2652 super(context, null, defStyleAttr);
Gilles Debunned88876a2012-03-16 17:34:04 -07002653 }
2654
2655 @Override
2656 public void dismiss() {
2657 super.dismiss();
2658
2659 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
2660
2661 // Safe cast since show() checks that mTextView.getText() is an Editable
2662 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
2663
2664 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
2665 if (hasInsertionController()) {
2666 getInsertionController().show();
2667 }
2668 }
2669 }
2670
2671 public SuggestionsPopupWindow() {
2672 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2673 mSuggestionSpanComparator = new SuggestionSpanComparator();
2674 mSpansLengths = new HashMap<SuggestionSpan, Integer>();
2675 }
2676
2677 @Override
2678 protected void createPopupWindow() {
2679 mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
2680 com.android.internal.R.attr.textSuggestionsWindowStyle);
2681 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2682 mPopupWindow.setFocusable(true);
2683 mPopupWindow.setClippingEnabled(false);
2684 }
2685
2686 @Override
2687 protected void initContentView() {
2688 ListView listView = new ListView(mTextView.getContext());
2689 mSuggestionsAdapter = new SuggestionAdapter();
2690 listView.setAdapter(mSuggestionsAdapter);
2691 listView.setOnItemClickListener(this);
2692 mContentView = listView;
2693
2694 // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
2695 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
2696 for (int i = 0; i < mSuggestionInfos.length; i++) {
2697 mSuggestionInfos[i] = new SuggestionInfo();
2698 }
2699 }
2700
2701 public boolean isShowingUp() {
2702 return mIsShowingUp;
2703 }
2704
2705 public void onParentLostFocus() {
2706 mIsShowingUp = false;
2707 }
2708
2709 private class SuggestionInfo {
2710 int suggestionStart, suggestionEnd; // range of actual suggestion within text
2711 SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
2712 int suggestionIndex; // the index of this suggestion inside suggestionSpan
2713 SpannableStringBuilder text = new SpannableStringBuilder();
2714 TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(),
2715 android.R.style.TextAppearance_SuggestionHighlight);
2716 }
2717
2718 private class SuggestionAdapter extends BaseAdapter {
2719 private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
2720 getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2721
2722 @Override
2723 public int getCount() {
2724 return mNumberOfSuggestions;
2725 }
2726
2727 @Override
2728 public Object getItem(int position) {
2729 return mSuggestionInfos[position];
2730 }
2731
2732 @Override
2733 public long getItemId(int position) {
2734 return position;
2735 }
2736
2737 @Override
2738 public View getView(int position, View convertView, ViewGroup parent) {
2739 TextView textView = (TextView) convertView;
2740
2741 if (textView == null) {
2742 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
2743 parent, false);
2744 }
2745
2746 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2747 textView.setText(suggestionInfo.text);
2748
Gilles Debunne1daba182012-06-26 11:41:03 -07002749 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY ||
2750 suggestionInfo.suggestionIndex == DELETE_TEXT) {
2751 textView.setBackgroundColor(Color.TRANSPARENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07002752 } else {
Gilles Debunne1daba182012-06-26 11:41:03 -07002753 textView.setBackgroundColor(Color.WHITE);
Gilles Debunned88876a2012-03-16 17:34:04 -07002754 }
2755
2756 return textView;
2757 }
2758 }
2759
2760 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
2761 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
2762 final int flag1 = span1.getFlags();
2763 final int flag2 = span2.getFlags();
2764 if (flag1 != flag2) {
2765 // The order here should match what is used in updateDrawState
2766 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2767 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2768 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2769 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2770 if (easy1 && !misspelled1) return -1;
2771 if (easy2 && !misspelled2) return 1;
2772 if (misspelled1) return -1;
2773 if (misspelled2) return 1;
2774 }
2775
2776 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
2777 }
2778 }
2779
2780 /**
2781 * Returns the suggestion spans that cover the current cursor position. The suggestion
2782 * spans are sorted according to the length of text that they are attached to.
2783 */
2784 private SuggestionSpan[] getSuggestionSpans() {
2785 int pos = mTextView.getSelectionStart();
2786 Spannable spannable = (Spannable) mTextView.getText();
2787 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
2788
2789 mSpansLengths.clear();
2790 for (SuggestionSpan suggestionSpan : suggestionSpans) {
2791 int start = spannable.getSpanStart(suggestionSpan);
2792 int end = spannable.getSpanEnd(suggestionSpan);
2793 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
2794 }
2795
2796 // The suggestions are sorted according to their types (easy correction first, then
2797 // misspelled) and to the length of the text that they cover (shorter first).
2798 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
2799 return suggestionSpans;
2800 }
2801
2802 @Override
2803 public void show() {
2804 if (!(mTextView.getText() instanceof Editable)) return;
2805
2806 if (updateSuggestions()) {
2807 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2808 mTextView.setCursorVisible(false);
2809 mIsShowingUp = true;
2810 super.show();
2811 }
2812 }
2813
2814 @Override
2815 protected void measureContent() {
2816 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2817 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
2818 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
2819 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
2820 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
2821
2822 int width = 0;
2823 View view = null;
2824 for (int i = 0; i < mNumberOfSuggestions; i++) {
2825 view = mSuggestionsAdapter.getView(i, view, mContentView);
2826 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
2827 view.measure(horizontalMeasure, verticalMeasure);
2828 width = Math.max(width, view.getMeasuredWidth());
2829 }
2830
2831 // Enforce the width based on actual text widths
2832 mContentView.measure(
2833 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
2834 verticalMeasure);
2835
2836 Drawable popupBackground = mPopupWindow.getBackground();
2837 if (popupBackground != null) {
2838 if (mTempRect == null) mTempRect = new Rect();
2839 popupBackground.getPadding(mTempRect);
2840 width += mTempRect.left + mTempRect.right;
2841 }
2842 mPopupWindow.setWidth(width);
2843 }
2844
2845 @Override
2846 protected int getTextOffset() {
2847 return mTextView.getSelectionStart();
2848 }
2849
2850 @Override
2851 protected int getVerticalLocalPosition(int line) {
2852 return mTextView.getLayout().getLineBottom(line);
2853 }
2854
2855 @Override
2856 protected int clipVertically(int positionY) {
2857 final int height = mContentView.getMeasuredHeight();
2858 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2859 return Math.min(positionY, displayMetrics.heightPixels - height);
2860 }
2861
2862 @Override
2863 public void hide() {
2864 super.hide();
2865 }
2866
2867 private boolean updateSuggestions() {
2868 Spannable spannable = (Spannable) mTextView.getText();
2869 SuggestionSpan[] suggestionSpans = getSuggestionSpans();
2870
2871 final int nbSpans = suggestionSpans.length;
2872 // Suggestions are shown after a delay: the underlying spans may have been removed
2873 if (nbSpans == 0) return false;
2874
2875 mNumberOfSuggestions = 0;
2876 int spanUnionStart = mTextView.getText().length();
2877 int spanUnionEnd = 0;
2878
2879 SuggestionSpan misspelledSpan = null;
2880 int underlineColor = 0;
2881
2882 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
2883 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
2884 final int spanStart = spannable.getSpanStart(suggestionSpan);
2885 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
2886 spanUnionStart = Math.min(spanStart, spanUnionStart);
2887 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
2888
2889 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2890 misspelledSpan = suggestionSpan;
2891 }
2892
2893 // The first span dictates the background color of the highlighted text
2894 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
2895
2896 String[] suggestions = suggestionSpan.getSuggestions();
2897 int nbSuggestions = suggestions.length;
2898 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
2899 String suggestion = suggestions[suggestionIndex];
2900
2901 boolean suggestionIsDuplicate = false;
2902 for (int i = 0; i < mNumberOfSuggestions; i++) {
2903 if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
2904 SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
2905 final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
2906 final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
2907 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
2908 suggestionIsDuplicate = true;
2909 break;
2910 }
2911 }
2912 }
2913
2914 if (!suggestionIsDuplicate) {
2915 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2916 suggestionInfo.suggestionSpan = suggestionSpan;
2917 suggestionInfo.suggestionIndex = suggestionIndex;
2918 suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
2919
2920 mNumberOfSuggestions++;
2921
2922 if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
2923 // Also end outer for loop
2924 spanIndex = nbSpans;
2925 break;
2926 }
2927 }
2928 }
2929 }
2930
2931 for (int i = 0; i < mNumberOfSuggestions; i++) {
2932 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
2933 }
2934
2935 // Add "Add to dictionary" item if there is a span with the misspelled flag
2936 if (misspelledSpan != null) {
2937 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
2938 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
2939 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
2940 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2941 suggestionInfo.suggestionSpan = misspelledSpan;
2942 suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
2943 suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView.
2944 getContext().getString(com.android.internal.R.string.addToDictionary));
2945 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2946 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2947
2948 mNumberOfSuggestions++;
2949 }
2950 }
2951
2952 // Delete item
2953 SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
2954 suggestionInfo.suggestionSpan = null;
2955 suggestionInfo.suggestionIndex = DELETE_TEXT;
2956 suggestionInfo.text.replace(0, suggestionInfo.text.length(),
2957 mTextView.getContext().getString(com.android.internal.R.string.deleteText));
2958 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
2959 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2960 mNumberOfSuggestions++;
2961
2962 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
2963 if (underlineColor == 0) {
2964 // Fallback on the default highlight color when the first span does not provide one
2965 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
2966 } else {
2967 final float BACKGROUND_TRANSPARENCY = 0.4f;
2968 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
2969 mSuggestionRangeSpan.setBackgroundColor(
2970 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
2971 }
2972 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
2973 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2974
2975 mSuggestionsAdapter.notifyDataSetChanged();
2976 return true;
2977 }
2978
2979 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
2980 int unionEnd) {
2981 final Spannable text = (Spannable) mTextView.getText();
2982 final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
2983 final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
2984
2985 // Adjust the start/end of the suggestion span
2986 suggestionInfo.suggestionStart = spanStart - unionStart;
2987 suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
2988 + suggestionInfo.text.length();
2989
2990 suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
2991 suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2992
2993 // Add the text before and after the span.
2994 final String textAsString = text.toString();
2995 suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
2996 suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
2997 }
2998
2999 @Override
3000 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3001 Editable editable = (Editable) mTextView.getText();
3002 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
3003
3004 if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
3005 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3006 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3007 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3008 // Do not leave two adjacent spaces after deletion, or one at beginning of text
3009 if (spanUnionEnd < editable.length() &&
3010 Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
3011 (spanUnionStart == 0 ||
3012 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
3013 spanUnionEnd = spanUnionEnd + 1;
3014 }
3015 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3016 }
3017 hide();
3018 return;
3019 }
3020
3021 final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
3022 final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
3023 if (spanStart < 0 || spanEnd <= spanStart) {
3024 // Span has been removed
3025 hide();
3026 return;
3027 }
3028
3029 final String originalText = editable.toString().substring(spanStart, spanEnd);
3030
3031 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
3032 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3033 intent.putExtra("word", originalText);
3034 intent.putExtra("locale", mTextView.getTextServicesLocale().toString());
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09003035 // Put a listener to replace the original text with a word which the user
3036 // modified in a user dictionary dialog.
Gilles Debunned88876a2012-03-16 17:34:04 -07003037 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3038 mTextView.getContext().startActivity(intent);
3039 // There is no way to know if the word was indeed added. Re-check.
3040 // TODO The ExtractEditText should remove the span in the original text instead
3041 editable.removeSpan(suggestionInfo.suggestionSpan);
Gilles Debunne2eb70fb2012-04-18 17:57:45 -07003042 Selection.setSelection(editable, spanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003043 updateSpellCheckSpans(spanStart, spanEnd, false);
3044 } else {
3045 // SuggestionSpans are removed by replace: save them before
3046 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
3047 SuggestionSpan.class);
3048 final int length = suggestionSpans.length;
3049 int[] suggestionSpansStarts = new int[length];
3050 int[] suggestionSpansEnds = new int[length];
3051 int[] suggestionSpansFlags = new int[length];
3052 for (int i = 0; i < length; i++) {
3053 final SuggestionSpan suggestionSpan = suggestionSpans[i];
3054 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
3055 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
3056 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
3057
3058 // Remove potential misspelled flags
3059 int suggestionSpanFlags = suggestionSpan.getFlags();
3060 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
3061 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
3062 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
3063 suggestionSpan.setFlags(suggestionSpanFlags);
3064 }
3065 }
3066
3067 final int suggestionStart = suggestionInfo.suggestionStart;
3068 final int suggestionEnd = suggestionInfo.suggestionEnd;
3069 final String suggestion = suggestionInfo.text.subSequence(
3070 suggestionStart, suggestionEnd).toString();
3071 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
3072
Luca Zanolin0c96b81f2012-08-29 11:33:12 +01003073 // Notify source IME of the suggestion pick. Do this before
3074 // swaping texts.
3075 suggestionInfo.suggestionSpan.notifySelection(
3076 mTextView.getContext(), originalText, suggestionInfo.suggestionIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07003077
3078 // Swap text content between actual text and Suggestion span
3079 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
3080 suggestions[suggestionInfo.suggestionIndex] = originalText;
3081
3082 // Restore previous SuggestionSpans
3083 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
3084 for (int i = 0; i < length; i++) {
3085 // Only spans that include the modified region make sense after replacement
3086 // Spans partially included in the replaced region are removed, there is no
3087 // way to assign them a valid range after replacement
3088 if (suggestionSpansStarts[i] <= spanStart &&
3089 suggestionSpansEnds[i] >= spanEnd) {
3090 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
3091 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
3092 }
3093 }
3094
3095 // Move cursor at the end of the replaced word
3096 final int newCursorPosition = spanEnd + lengthDifference;
3097 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
3098 }
3099
3100 hide();
3101 }
3102 }
3103
3104 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003105 * An ActionMode Callback class that is used to provide actions while in text insertion or
3106 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003107 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003108 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3109 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003110 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003111 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003112 private final Path mSelectionPath = new Path();
3113 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003114 private final boolean mHasSelection;
Clara Bayarriea4f1502015-03-18 00:25:01 +00003115
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003116 private int mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00003117
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003118 public TextActionModeCallback(boolean hasSelection) {
3119 mHasSelection = hasSelection;
3120 if (mHasSelection) {
3121 SelectionModifierCursorController selectionController = getSelectionController();
3122 if (selectionController.mStartHandle == null) {
3123 // As these are for initializing selectionController, hide() must be called.
3124 selectionController.initDrawables();
3125 selectionController.initHandles();
3126 selectionController.hide();
3127 }
3128 mHandleHeight = Math.max(
3129 mSelectHandleLeft.getMinimumHeight(),
3130 mSelectHandleRight.getMinimumHeight());
3131 } else {
3132 InsertionPointCursorController insertionController = getInsertionController();
3133 if (insertionController != null) {
3134 insertionController.getHandle();
3135 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
3136 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003137 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003138 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003139
3140 @Override
3141 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003142 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003143 mode.setSubtitle(null);
3144 mode.setTitleOptionalHint(true);
3145 populateMenuWithItems(menu);
3146
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003147 Callback customCallback = getCustomCallback();
3148 if (customCallback != null) {
3149 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003150 // The custom mode can choose to cancel the action mode, dismiss selection.
3151 Selection.setSelection((Spannable) mTextView.getText(),
3152 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003153 return false;
3154 }
3155 }
3156
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003157 addIntentMenuItemsForTextProcessing(menu);
3158
Clara Bayarri13152d12015-04-09 12:02:04 +01003159 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
3160 mTextView.setHasTransientState(true);
3161 return true;
3162 } else {
3163 return false;
3164 }
3165 }
3166
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003167 private Callback getCustomCallback() {
3168 return mHasSelection
3169 ? mCustomSelectionActionModeCallback
3170 : mCustomInsertionActionModeCallback;
3171 }
3172
Clara Bayarri13152d12015-04-09 12:02:04 +01003173 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003174 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003175 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
3176 com.android.internal.R.string.cut).
Gilles Debunned88876a2012-03-16 17:34:04 -07003177 setAlphabeticShortcut('x').
Clara Bayarriaee243b2015-05-18 17:05:46 +01003178 setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003179 }
3180
3181 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003182 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
3183 com.android.internal.R.string.copy).
Gilles Debunned88876a2012-03-16 17:34:04 -07003184 setAlphabeticShortcut('c').
Clara Bayarriaee243b2015-05-18 17:05:46 +01003185 setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003186 }
3187
3188 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003189 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
3190 com.android.internal.R.string.paste).
3191 setAlphabeticShortcut('v').
3192 setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003193 }
3194
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003195 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003196 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
3197 com.android.internal.R.string.share).
3198 setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003199 }
3200
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003201 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003202 updateReplaceItem(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003203 }
3204
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003205 private void addIntentMenuItemsForTextProcessing(Menu menu) {
3206 if (mTextView.canProcessText()) {
3207 PackageManager packageManager = mTextView.getContext().getPackageManager();
3208 List<ResolveInfo> supportedActivities =
3209 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003210 for (int i = 0; i < supportedActivities.size(); ++i) {
3211 ResolveInfo info = supportedActivities.get(i);
3212 menu.add(Menu.NONE, Menu.NONE,
3213 MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
3214 info.loadLabel(packageManager))
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003215 .setIntent(createProcessTextIntentForResolveInfo(info))
Clara Bayarriaee243b2015-05-18 17:05:46 +01003216 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003217 }
3218 }
3219 }
3220
3221 private Intent createProcessTextIntent() {
3222 return new Intent()
3223 .setAction(Intent.ACTION_PROCESS_TEXT)
3224 .setType("text/plain");
3225 }
3226
3227 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
3228 return createProcessTextIntent()
3229 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
3230 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
3231 }
3232
Gilles Debunned88876a2012-03-16 17:34:04 -07003233 @Override
3234 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003235 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003236 updateReplaceItem(menu);
3237
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003238 Callback customCallback = getCustomCallback();
3239 if (customCallback != null) {
3240 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003241 }
3242 return true;
3243 }
3244
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003245 private void updateSelectAllItem(Menu menu) {
3246 boolean canSelectAll = mTextView.canSelectAllText();
3247 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3248 if (canSelectAll && !selectAllItemExists) {
3249 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3250 com.android.internal.R.string.selectAll)
3251 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3252 } else if (!canSelectAll && selectAllItemExists) {
3253 menu.removeItem(TextView.ID_SELECT_ALL);
3254 }
3255 }
3256
Clara Bayarri13152d12015-04-09 12:02:04 +01003257 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09003258 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01003259 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3260 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003261 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3262 com.android.internal.R.string.replace)
3263 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01003264 } else if (!canReplace && replaceItemExists) {
3265 menu.removeItem(TextView.ID_REPLACE);
3266 }
3267 }
3268
Gilles Debunned88876a2012-03-16 17:34:04 -07003269 @Override
3270 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003271 if (item.getIntent() != null
3272 && item.getIntent().getAction().equals(Intent.ACTION_PROCESS_TEXT)) {
3273 item.getIntent().putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText());
Clara Bayarri578286f2015-04-10 15:35:31 +01003274 mPreserveDetachedSelection = true;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003275 mTextView.startActivityForResult(
3276 item.getIntent(), TextView.PROCESS_TEXT_REQUEST_CODE);
3277 return true;
3278 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003279 Callback customCallback = getCustomCallback();
3280 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003281 return true;
3282 }
3283 return mTextView.onTextContextMenuItem(item.getItemId());
3284 }
3285
3286 @Override
3287 public void onDestroyActionMode(ActionMode mode) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003288 Callback customCallback = getCustomCallback();
3289 if (customCallback != null) {
3290 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07003291 }
Adam Powell057a5852012-05-11 10:28:38 -07003292
3293 /*
3294 * If we're ending this mode because we're detaching from a window,
3295 * we still have selection state to preserve. Don't clear it, we'll
3296 * bring back the selection mode when (if) we get reattached.
3297 */
3298 if (!mPreserveDetachedSelection) {
3299 Selection.setSelection((Spannable) mTextView.getText(),
3300 mTextView.getSelectionEnd());
3301 mTextView.setHasTransientState(false);
3302 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003303
3304 if (mSelectionModifierCursorController != null) {
3305 mSelectionModifierCursorController.hide();
3306 }
3307
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003308 mTextActionMode = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07003309 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003310
3311 @Override
3312 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
3313 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
3314 super.onGetContentRect(mode, view, outRect);
3315 return;
3316 }
3317 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
3318 // We have a selection.
3319 mSelectionPath.reset();
3320 mTextView.getLayout().getSelectionPath(
3321 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
3322 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003323 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarrib7419dd2015-04-09 15:24:58 +01003324 } else if (mCursorCount == 2) {
3325 // We have a split cursor. In this case, we take the rectangle that includes both
3326 // parts of the cursor to ensure we don't obscure either of them.
3327 Rect firstCursorBounds = mCursorDrawable[0].getBounds();
3328 Rect secondCursorBounds = mCursorDrawable[1].getBounds();
3329 mSelectionBounds.set(
3330 Math.min(firstCursorBounds.left, secondCursorBounds.left),
3331 Math.min(firstCursorBounds.top, secondCursorBounds.top),
3332 Math.max(firstCursorBounds.right, secondCursorBounds.right),
3333 Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom)
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003334 + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00003335 } else {
3336 // We have a single cursor.
3337 int line = mTextView.getLayout().getLineForOffset(mTextView.getSelectionStart());
3338 float primaryHorizontal =
3339 mTextView.getLayout().getPrimaryHorizontal(mTextView.getSelectionStart());
3340 mSelectionBounds.set(
3341 primaryHorizontal,
3342 mTextView.getLayout().getLineTop(line),
3343 primaryHorizontal + 1,
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003344 mTextView.getLayout().getLineTop(line + 1) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00003345 }
3346 // Take TextView's padding and scroll into account.
3347 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
3348 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
3349 outRect.set(
3350 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
3351 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
3352 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
3353 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
3354 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003355 }
3356
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003357 /**
3358 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
3359 * while the input method is requesting the cursor/anchor position. Does nothing as long as
3360 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
3361 */
3362 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09003363 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003364 final int[] mTmpIntOffset = new int[2];
3365 final Matrix mViewToScreenMatrix = new Matrix();
3366
3367 @Override
3368 public void updatePosition(int parentPositionX, int parentPositionY,
3369 boolean parentPositionChanged, boolean parentScrolled) {
3370 final InputMethodState ims = mInputMethodState;
3371 if (ims == null || ims.mBatchEditNesting > 0) {
3372 return;
3373 }
3374 final InputMethodManager imm = InputMethodManager.peekInstance();
3375 if (null == imm) {
3376 return;
3377 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09003378 if (!imm.isActive(mTextView)) {
3379 return;
3380 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003381 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09003382 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003383 return;
3384 }
3385 Layout layout = mTextView.getLayout();
3386 if (layout == null) {
3387 return;
3388 }
3389
Yohei Yukawac46b5f02014-06-10 12:26:34 +09003390 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003391 builder.reset();
3392
3393 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09003394 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003395
3396 // Construct transformation matrix from view local coordinates to screen coordinates.
3397 mViewToScreenMatrix.set(mTextView.getMatrix());
3398 mTextView.getLocationOnScreen(mTmpIntOffset);
3399 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
3400 builder.setMatrix(mViewToScreenMatrix);
3401
3402 final float viewportToContentHorizontalOffset =
3403 mTextView.viewportToContentHorizontalOffset();
3404 final float viewportToContentVerticalOffset =
3405 mTextView.viewportToContentVerticalOffset();
3406
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09003407 final CharSequence text = mTextView.getText();
3408 if (text instanceof Spannable) {
3409 final Spannable sp = (Spannable) text;
3410 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
3411 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
3412 if (composingTextEnd < composingTextStart) {
3413 final int temp = composingTextEnd;
3414 composingTextEnd = composingTextStart;
3415 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003416 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09003417 final boolean hasComposingText =
3418 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
3419 if (hasComposingText) {
3420 final CharSequence composingText = text.subSequence(composingTextStart,
3421 composingTextEnd);
3422 builder.setComposingText(composingTextStart, composingText);
Yohei Yukawa5f183f02014-09-02 14:18:40 -07003423
3424 final int minLine = layout.getLineForOffset(composingTextStart);
3425 final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
3426 for (int line = minLine; line <= maxLine; ++line) {
3427 final int lineStart = layout.getLineStart(line);
3428 final int lineEnd = layout.getLineEnd(line);
3429 final int offsetStart = Math.max(lineStart, composingTextStart);
3430 final int offsetEnd = Math.min(lineEnd, composingTextEnd);
3431 final boolean ltrLine =
3432 layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
3433 final float[] widths = new float[offsetEnd - offsetStart];
3434 layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
3435 final float top = layout.getLineTop(line);
3436 final float bottom = layout.getLineBottom(line);
3437 for (int offset = offsetStart; offset < offsetEnd; ++offset) {
3438 final float charWidth = widths[offset - offsetStart];
3439 final boolean isRtl = layout.isRtlCharAt(offset);
3440 final float primary = layout.getPrimaryHorizontal(offset);
3441 final float secondary = layout.getSecondaryHorizontal(offset);
3442 // TODO: This doesn't work perfectly for text with custom styles and
3443 // TAB chars.
3444 final float left;
3445 final float right;
3446 if (ltrLine) {
3447 if (isRtl) {
3448 left = secondary - charWidth;
3449 right = secondary;
3450 } else {
3451 left = primary;
3452 right = primary + charWidth;
3453 }
3454 } else {
3455 if (!isRtl) {
3456 left = secondary;
3457 right = secondary + charWidth;
3458 } else {
3459 left = primary - charWidth;
3460 right = primary;
3461 }
3462 }
3463 // TODO: Check top-right and bottom-left as well.
3464 final float localLeft = left + viewportToContentHorizontalOffset;
3465 final float localRight = right + viewportToContentHorizontalOffset;
3466 final float localTop = top + viewportToContentVerticalOffset;
3467 final float localBottom = bottom + viewportToContentVerticalOffset;
3468 final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
3469 final boolean isBottomRightVisible =
3470 isPositionVisible(localRight, localBottom);
3471 int characterBoundsFlags = 0;
3472 if (isTopLeftVisible || isBottomRightVisible) {
3473 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
3474 }
Andreas Gampe4976e2d2015-03-17 16:08:43 -07003475 if (!isTopLeftVisible || !isBottomRightVisible) {
Yohei Yukawa5f183f02014-09-02 14:18:40 -07003476 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
3477 }
3478 if (isRtl) {
3479 characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
3480 }
3481 // Here offset is the index in Java chars.
3482 builder.addCharacterBounds(offset, localLeft, localTop, localRight,
3483 localBottom, characterBoundsFlags);
3484 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003485 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003486 }
3487 }
3488
3489 // Treat selectionStart as the insertion point.
3490 if (0 <= selectionStart) {
3491 final int offset = selectionStart;
3492 final int line = layout.getLineForOffset(offset);
3493 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
3494 + viewportToContentHorizontalOffset;
3495 final float insertionMarkerTop = layout.getLineTop(line)
3496 + viewportToContentVerticalOffset;
3497 final float insertionMarkerBaseline = layout.getLineBaseline(line)
3498 + viewportToContentVerticalOffset;
3499 final float insertionMarkerBottom = layout.getLineBottom(line)
3500 + viewportToContentVerticalOffset;
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07003501 final boolean isTopVisible =
3502 isPositionVisible(insertionMarkerX, insertionMarkerTop);
3503 final boolean isBottomVisible =
3504 isPositionVisible(insertionMarkerX, insertionMarkerBottom);
3505 int insertionMarkerFlags = 0;
3506 if (isTopVisible || isBottomVisible) {
3507 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
3508 }
3509 if (!isTopVisible || !isBottomVisible) {
3510 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
3511 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07003512 if (layout.isRtlCharAt(offset)) {
3513 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
3514 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09003515 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07003516 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003517 }
3518
3519 imm.updateCursorAnchorInfo(mTextView, builder.build());
3520 }
3521 }
3522
Gilles Debunned88876a2012-03-16 17:34:04 -07003523 private abstract class HandleView extends View implements TextViewPositionListener {
3524 protected Drawable mDrawable;
3525 protected Drawable mDrawableLtr;
3526 protected Drawable mDrawableRtl;
3527 private final PopupWindow mContainer;
3528 // Position with respect to the parent TextView
3529 private int mPositionX, mPositionY;
3530 private boolean mIsDragging;
3531 // Offset from touch position to mPosition
3532 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
3533 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07003534 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07003535 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
3536 private float mTouchOffsetY;
3537 // Where the touch position should be on the handle to ensure a maximum cursor visibility
3538 private float mIdealVerticalOffset;
3539 // Parent's (TextView) previous position in window
3540 private int mLastParentX, mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07003541 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07003542 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07003543 // Previous text character offset
3544 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07003545 // Minimum touch target size for handles
3546 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08003547 // Indicates the line of text that the handle is on.
Mady Mellorb9bbbb12015-03-23 11:50:46 -07003548 protected int mPrevLine = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07003549
3550 public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
3551 super(mTextView.getContext());
3552 mContainer = new PopupWindow(mTextView.getContext(), null,
3553 com.android.internal.R.attr.textSelectHandleWindowStyle);
3554 mContainer.setSplitTouchEnabled(true);
3555 mContainer.setClippingEnabled(false);
3556 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09003557 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3558 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07003559 mContainer.setContentView(this);
3560
3561 mDrawableLtr = drawableLtr;
3562 mDrawableRtl = drawableRtl;
Adam Powell3fceabd2014-08-19 18:28:04 -07003563 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
3564 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07003565
3566 updateDrawable();
3567
Adam Powell3fceabd2014-08-19 18:28:04 -07003568 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07003569 mTouchOffsetY = -0.3f * handleHeight;
3570 mIdealVerticalOffset = 0.7f * handleHeight;
3571 }
3572
Mady Mellor7a936442015-05-20 10:05:52 -07003573 public float getIdealVerticalOffset() {
3574 return mIdealVerticalOffset;
3575 }
3576
Gilles Debunned88876a2012-03-16 17:34:04 -07003577 protected void updateDrawable() {
3578 final int offset = getCurrentCursorOffset();
3579 final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09003580 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07003581 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
3582 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07003583 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09003584 if (oldDrawable != mDrawable) {
3585 postInvalidate();
3586 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003587 }
3588
3589 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07003590 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07003591
3592 // Touch-up filter: number of previous positions remembered
3593 private static final int HISTORY_SIZE = 5;
3594 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
3595 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
3596 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
3597 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
3598 private int mPreviousOffsetIndex = 0;
3599 private int mNumberPreviousOffsets = 0;
3600
3601 private void startTouchUpFilter(int offset) {
3602 mNumberPreviousOffsets = 0;
3603 addPositionToTouchUpFilter(offset);
3604 }
3605
3606 private void addPositionToTouchUpFilter(int offset) {
3607 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
3608 mPreviousOffsets[mPreviousOffsetIndex] = offset;
3609 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
3610 mNumberPreviousOffsets++;
3611 }
3612
3613 private void filterOnTouchUp() {
3614 final long now = SystemClock.uptimeMillis();
3615 int i = 0;
3616 int index = mPreviousOffsetIndex;
3617 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
3618 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
3619 i++;
3620 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
3621 }
3622
3623 if (i > 0 && i < iMax &&
3624 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
3625 positionAtCursorOffset(mPreviousOffsets[index], false);
3626 }
3627 }
3628
3629 public boolean offsetHasBeenChanged() {
3630 return mNumberPreviousOffsets > 1;
3631 }
3632
3633 @Override
3634 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07003635 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
3636 }
3637
3638 private int getPreferredWidth() {
3639 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
3640 }
3641
3642 private int getPreferredHeight() {
3643 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07003644 }
3645
3646 public void show() {
3647 if (isShowing()) return;
3648
3649 getPositionListener().addSubscriber(this, true /* local position may change */);
3650
3651 // Make sure the offset is always considered new, even when focusing at same position
3652 mPreviousOffset = -1;
3653 positionAtCursorOffset(getCurrentCursorOffset(), false);
Gilles Debunned88876a2012-03-16 17:34:04 -07003654 }
3655
3656 protected void dismiss() {
3657 mIsDragging = false;
3658 mContainer.dismiss();
3659 onDetached();
3660 }
3661
3662 public void hide() {
3663 dismiss();
3664
3665 getPositionListener().removeSubscriber(this);
3666 }
3667
Gilles Debunned88876a2012-03-16 17:34:04 -07003668 public boolean isShowing() {
3669 return mContainer.isShowing();
3670 }
3671
3672 private boolean isVisible() {
3673 // Always show a dragging handle.
3674 if (mIsDragging) {
3675 return true;
3676 }
3677
3678 if (mTextView.isInBatchEditMode()) {
3679 return false;
3680 }
3681
Keisuke Kuroyanagi6cda1e22015-04-22 21:41:14 +09003682 return isPositionVisible(mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07003683 }
3684
3685 public abstract int getCurrentCursorOffset();
3686
3687 protected abstract void updateSelection(int offset);
3688
3689 public abstract void updatePosition(float x, float y);
3690
3691 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
3692 // A HandleView relies on the layout, which may be nulled by external methods
3693 Layout layout = mTextView.getLayout();
3694 if (layout == null) {
3695 // Will update controllers' state, hiding them and stopping selection mode if needed
3696 prepareCursorControllers();
3697 return;
3698 }
3699
3700 boolean offsetChanged = offset != mPreviousOffset;
3701 if (offsetChanged || parentScrolled) {
3702 if (offsetChanged) {
3703 updateSelection(offset);
3704 addPositionToTouchUpFilter(offset);
3705 }
3706 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07003707 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07003708
Adam Powell3fceabd2014-08-19 18:28:04 -07003709 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
3710 getHorizontalOffset() + getCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07003711 mPositionY = layout.getLineBottom(line);
3712
3713 // Take TextView's padding and scroll into account.
3714 mPositionX += mTextView.viewportToContentHorizontalOffset();
3715 mPositionY += mTextView.viewportToContentVerticalOffset();
3716
3717 mPreviousOffset = offset;
3718 mPositionHasChanged = true;
3719 }
3720 }
3721
3722 public void updatePosition(int parentPositionX, int parentPositionY,
3723 boolean parentPositionChanged, boolean parentScrolled) {
3724 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
3725 if (parentPositionChanged || mPositionHasChanged) {
3726 if (mIsDragging) {
3727 // Update touchToWindow offset in case of parent scrolling while dragging
3728 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
3729 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
3730 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
3731 mLastParentX = parentPositionX;
3732 mLastParentY = parentPositionY;
3733 }
3734
3735 onHandleMoved();
3736 }
3737
3738 if (isVisible()) {
3739 final int positionX = parentPositionX + mPositionX;
3740 final int positionY = parentPositionY + mPositionY;
3741 if (isShowing()) {
3742 mContainer.update(positionX, positionY, -1, -1);
3743 } else {
3744 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3745 positionX, positionY);
3746 }
3747 } else {
3748 if (isShowing()) {
3749 dismiss();
3750 }
3751 }
3752
3753 mPositionHasChanged = false;
3754 }
3755 }
3756
Mady Mellor2ff2cd82015-03-02 10:37:01 -08003757 public void showAtLocation(int offset) {
3758 // TODO - investigate if there's a better way to show the handles
3759 // after the drag accelerator has occured.
3760 int[] tmpCords = new int[2];
3761 mTextView.getLocationInWindow(tmpCords);
3762
3763 Layout layout = mTextView.getLayout();
3764 int posX = tmpCords[0];
3765 int posY = tmpCords[1];
3766
3767 final int line = layout.getLineForOffset(offset);
3768
3769 int startX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f
3770 - mHotspotX - getHorizontalOffset() + getCursorOffset());
3771 int startY = layout.getLineBottom(line);
3772
3773 // Take TextView's padding and scroll into account.
3774 startX += mTextView.viewportToContentHorizontalOffset();
3775 startY += mTextView.viewportToContentVerticalOffset();
3776
3777 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3778 startX + posX, startY + posY);
3779 }
3780
Gilles Debunned88876a2012-03-16 17:34:04 -07003781 @Override
3782 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07003783 final int drawWidth = mDrawable.getIntrinsicWidth();
3784 final int left = getHorizontalOffset();
3785
3786 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07003787 mDrawable.draw(c);
3788 }
3789
Adam Powell3fceabd2014-08-19 18:28:04 -07003790 private int getHorizontalOffset() {
3791 final int width = getPreferredWidth();
3792 final int drawWidth = mDrawable.getIntrinsicWidth();
3793 final int left;
3794 switch (mHorizontalGravity) {
3795 case Gravity.LEFT:
3796 left = 0;
3797 break;
3798 default:
3799 case Gravity.CENTER:
3800 left = (width - drawWidth) / 2;
3801 break;
3802 case Gravity.RIGHT:
3803 left = width - drawWidth;
3804 break;
3805 }
3806 return left;
3807 }
3808
3809 protected int getCursorOffset() {
3810 return 0;
3811 }
3812
Gilles Debunned88876a2012-03-16 17:34:04 -07003813 @Override
3814 public boolean onTouchEvent(MotionEvent ev) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01003815 updateFloatingToolbarVisibility(ev);
3816
Gilles Debunned88876a2012-03-16 17:34:04 -07003817 switch (ev.getActionMasked()) {
3818 case MotionEvent.ACTION_DOWN: {
3819 startTouchUpFilter(getCurrentCursorOffset());
3820 mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
3821 mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
3822
3823 final PositionListener positionListener = getPositionListener();
3824 mLastParentX = positionListener.getPositionX();
3825 mLastParentY = positionListener.getPositionY();
3826 mIsDragging = true;
3827 break;
3828 }
3829
3830 case MotionEvent.ACTION_MOVE: {
3831 final float rawX = ev.getRawX();
3832 final float rawY = ev.getRawY();
3833
3834 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
3835 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
3836 final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
3837 float newVerticalOffset;
3838 if (previousVerticalOffset < mIdealVerticalOffset) {
3839 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
3840 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
3841 } else {
3842 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
3843 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
3844 }
3845 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
3846
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09003847 final float newPosX =
3848 rawX - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
Gilles Debunned88876a2012-03-16 17:34:04 -07003849 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
3850
3851 updatePosition(newPosX, newPosY);
3852 break;
3853 }
3854
3855 case MotionEvent.ACTION_UP:
3856 filterOnTouchUp();
3857 mIsDragging = false;
3858 break;
3859
3860 case MotionEvent.ACTION_CANCEL:
3861 mIsDragging = false;
3862 break;
3863 }
3864 return true;
3865 }
3866
3867 public boolean isDragging() {
3868 return mIsDragging;
3869 }
3870
Clara Bayarri6351e662015-03-16 23:17:59 +00003871 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07003872
Clara Bayarri6351e662015-03-16 23:17:59 +00003873 public void onDetached() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07003874 }
3875
3876 private class InsertionHandleView extends HandleView {
3877 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
3878 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
3879
Clara Bayarrib71dddd2015-06-04 23:17:30 +01003880 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Gilles Debunned88876a2012-03-16 17:34:04 -07003881 private float mDownPositionX, mDownPositionY;
3882 private Runnable mHider;
3883
3884 public InsertionHandleView(Drawable drawable) {
3885 super(drawable, drawable);
3886 }
3887
3888 @Override
3889 public void show() {
3890 super.show();
3891
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01003892 final long durationSinceCutOrCopy =
Andrei Stingaceanu77b9c382015-05-06 13:25:19 +01003893 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01003894
3895 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003896 if (mInsertionActionModeRunnable != null
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01003897 && (mDoubleTap || isCursorInsideEasyCorrectionSpan())) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003898 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01003899 }
3900
3901 // Prepare and schedule the single tap runnable to run exactly after the double tap
3902 // timeout has passed.
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01003903 if (!mDoubleTap && !isCursorInsideEasyCorrectionSpan()
3904 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01003905 if (mTextActionMode == null) {
3906 if (mInsertionActionModeRunnable == null) {
3907 mInsertionActionModeRunnable = new Runnable() {
3908 @Override
3909 public void run() {
3910 startInsertionActionMode();
3911 }
3912 };
3913 }
3914 mTextView.postDelayed(
3915 mInsertionActionModeRunnable,
3916 ViewConfiguration.getDoubleTapTimeout() + 1);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01003917 }
3918
Gilles Debunned88876a2012-03-16 17:34:04 -07003919 }
3920
3921 hideAfterDelay();
3922 }
3923
Gilles Debunned88876a2012-03-16 17:34:04 -07003924 private void hideAfterDelay() {
3925 if (mHider == null) {
3926 mHider = new Runnable() {
3927 public void run() {
3928 hide();
3929 }
3930 };
3931 } else {
3932 removeHiderCallback();
3933 }
3934 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
3935 }
3936
3937 private void removeHiderCallback() {
3938 if (mHider != null) {
3939 mTextView.removeCallbacks(mHider);
3940 }
3941 }
3942
3943 @Override
3944 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3945 return drawable.getIntrinsicWidth() / 2;
3946 }
3947
3948 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07003949 protected int getHorizontalGravity(boolean isRtlRun) {
3950 return Gravity.CENTER_HORIZONTAL;
3951 }
3952
3953 @Override
3954 protected int getCursorOffset() {
3955 int offset = super.getCursorOffset();
3956 final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null;
3957 if (cursor != null) {
3958 cursor.getPadding(mTempRect);
3959 offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2;
3960 }
3961 return offset;
3962 }
3963
3964 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003965 public boolean onTouchEvent(MotionEvent ev) {
3966 final boolean result = super.onTouchEvent(ev);
3967
3968 switch (ev.getActionMasked()) {
3969 case MotionEvent.ACTION_DOWN:
3970 mDownPositionX = ev.getRawX();
3971 mDownPositionY = ev.getRawY();
3972 break;
3973
3974 case MotionEvent.ACTION_UP:
3975 if (!offsetHasBeenChanged()) {
3976 final float deltaX = mDownPositionX - ev.getRawX();
3977 final float deltaY = mDownPositionY - ev.getRawY();
3978 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
3979
3980 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
3981 mTextView.getContext());
3982 final int touchSlop = viewConfiguration.getScaledTouchSlop();
3983
3984 if (distanceSquared < touchSlop * touchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01003985 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003986 if (mTextActionMode != null) {
3987 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07003988 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003989 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07003990 }
3991 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01003992 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003993 if (mTextActionMode != null) {
3994 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01003995 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003996 }
3997 hideAfterDelay();
3998 break;
3999
4000 case MotionEvent.ACTION_CANCEL:
4001 hideAfterDelay();
4002 break;
4003
4004 default:
4005 break;
4006 }
4007
4008 return result;
4009 }
4010
4011 @Override
4012 public int getCurrentCursorOffset() {
4013 return mTextView.getSelectionStart();
4014 }
4015
4016 @Override
4017 public void updateSelection(int offset) {
4018 Selection.setSelection((Spannable) mTextView.getText(), offset);
4019 }
4020
4021 @Override
4022 public void updatePosition(float x, float y) {
4023 positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004024 if (mTextActionMode != null) {
4025 mTextActionMode.invalidate();
Clara Bayarri1baed512015-05-11 15:29:16 +01004026 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004027 }
4028
4029 @Override
4030 void onHandleMoved() {
4031 super.onHandleMoved();
4032 removeHiderCallback();
4033 }
4034
4035 @Override
4036 public void onDetached() {
4037 super.onDetached();
4038 removeHiderCallback();
4039 }
4040 }
4041
4042 private class SelectionStartHandleView extends HandleView {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004043 // Indicates whether the cursor is making adjustments within a word.
4044 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004045 // Difference between touch position and word boundary position.
4046 private float mTouchWordDelta;
Gilles Debunned88876a2012-03-16 17:34:04 -07004047
4048 public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
4049 super(drawableLtr, drawableRtl);
4050 }
4051
4052 @Override
4053 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Mady Mellor709386f2015-05-14 12:41:18 -07004054 if (isRtlRun) {
4055 return drawable.getIntrinsicWidth() / 4;
4056 } else {
4057 return (drawable.getIntrinsicWidth() * 3) / 4;
4058 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004059 }
4060
4061 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004062 protected int getHorizontalGravity(boolean isRtlRun) {
Mady Mellor2e577d22015-05-13 07:15:17 -07004063 return isRtlRun ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07004064 }
4065
4066 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004067 public int getCurrentCursorOffset() {
4068 return mTextView.getSelectionStart();
4069 }
4070
4071 @Override
4072 public void updateSelection(int offset) {
4073 Selection.setSelection((Spannable) mTextView.getText(), offset,
4074 mTextView.getSelectionEnd());
4075 updateDrawable();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004076 if (mTextActionMode != null) {
4077 mTextActionMode.invalidate();
Clara Bayarri13152d12015-04-09 12:02:04 +01004078 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004079 }
4080
4081 @Override
4082 public void updatePosition(float x, float y) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004083 final int selectionEnd = mTextView.getSelectionEnd();
4084 final Layout layout = mTextView.getLayout();
4085 int initialOffset = mTextView.getOffsetForPosition(x, y);
4086 int currLine = mTextView.getLineAtCoordinate(y);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004087 boolean positionCursor = false;
Mady Mellor81fa3e82015-05-14 09:17:41 -07004088
4089 if (initialOffset >= selectionEnd) {
4090 // Handles have crossed, bound it to the last selected line and
4091 // adjust by word / char as normal.
4092 currLine = layout != null ? layout.getLineForOffset(selectionEnd) : mPrevLine;
4093 initialOffset = mTextView.getOffsetAtCoordinate(currLine, x);
4094 }
4095
4096 int offset = initialOffset;
Mady Mellor6c7b4ad2015-04-15 14:23:26 -07004097 int end = getWordEnd(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004098 int start = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07004099
Mady Mellorc2225b92015-04-01 15:59:20 -07004100 if (offset < mPreviousOffset) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004101 // User is increasing the selection.
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004102 if (!mInWord || currLine < mPrevLine) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004103 // We're not in a word, or we're on a different line so we'll expand by
4104 // word. First ensure the user has at least entered the next word.
4105 int offsetToWord = Math.min((end - start) / 2, 2);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004106 if (offset <= end - offsetToWord || currLine < mPrevLine) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004107 offset = start;
4108 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07004109 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004110 }
4111 }
Mady Mellor81fa3e82015-05-14 09:17:41 -07004112 if (layout != null && offset < initialOffset) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004113 final float adjustedX = layout.getPrimaryHorizontal(offset);
4114 mTouchWordDelta =
4115 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09004116 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004117 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004118 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004119 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004120 } else {
4121 final int adjustedOffset =
4122 mTextView.getOffsetAtCoordinate(currLine, x - mTouchWordDelta);
4123 if (adjustedOffset > mPreviousOffset || currLine > mPrevLine) {
4124 // User is shrinking the selection.
4125 if (currLine > mPrevLine) {
4126 // We're on a different line, so we'll snap to word boundaries.
4127 offset = start;
Mady Mellor81fa3e82015-05-14 09:17:41 -07004128 if (layout != null && offset < initialOffset) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004129 final float adjustedX = layout.getPrimaryHorizontal(offset);
4130 mTouchWordDelta =
4131 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4132 } else {
4133 mTouchWordDelta = 0.0f;
4134 }
4135 } else {
4136 offset = adjustedOffset;
4137 }
4138 positionCursor = true;
4139 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004140 }
4141
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004142 if (positionCursor) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004143 // Handles can not cross and selection is at least one character.
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004144 if (offset >= selectionEnd) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004145 offset = getNextCursorOffset(selectionEnd, false);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004146 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004147 }
4148 positionAtCursorOffset(offset, false);
4149 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004150 }
4151
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004152 @Override
Mady Mellor36d5a7b2015-05-22 10:31:12 -07004153 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
4154 super.positionAtCursorOffset(offset, parentScrolled);
4155 mInWord = !getWordIteratorWithText().isBoundary(offset);
4156 }
4157
4158 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004159 public boolean onTouchEvent(MotionEvent event) {
4160 boolean superResult = super.onTouchEvent(event);
4161 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
4162 // Reset the touch word offset when the user has lifted their finger.
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004163 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004164 }
4165 return superResult;
4166 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004167 }
4168
4169 private class SelectionEndHandleView extends HandleView {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004170 // Indicates whether the cursor is making adjustments within a word.
4171 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004172 // Difference between touch position and word boundary position.
4173 private float mTouchWordDelta;
Gilles Debunned88876a2012-03-16 17:34:04 -07004174
4175 public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
4176 super(drawableLtr, drawableRtl);
4177 }
4178
4179 @Override
4180 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Mady Mellor709386f2015-05-14 12:41:18 -07004181 if (isRtlRun) {
4182 return (drawable.getIntrinsicWidth() * 3) / 4;
4183 } else {
4184 return drawable.getIntrinsicWidth() / 4;
4185 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004186 }
4187
4188 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004189 protected int getHorizontalGravity(boolean isRtlRun) {
Mady Mellor2e577d22015-05-13 07:15:17 -07004190 return isRtlRun ? Gravity.RIGHT : Gravity.LEFT;
Adam Powell3fceabd2014-08-19 18:28:04 -07004191 }
4192
4193 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004194 public int getCurrentCursorOffset() {
4195 return mTextView.getSelectionEnd();
4196 }
4197
4198 @Override
4199 public void updateSelection(int offset) {
4200 Selection.setSelection((Spannable) mTextView.getText(),
4201 mTextView.getSelectionStart(), offset);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004202 if (mTextActionMode != null) {
4203 mTextActionMode.invalidate();
Clara Bayarri13152d12015-04-09 12:02:04 +01004204 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004205 updateDrawable();
4206 }
4207
4208 @Override
4209 public void updatePosition(float x, float y) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004210 final int selectionStart = mTextView.getSelectionStart();
4211 final Layout layout = mTextView.getLayout();
4212 int initialOffset = mTextView.getOffsetForPosition(x, y);
4213 int currLine = mTextView.getLineAtCoordinate(y);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004214 boolean positionCursor = false;
Mady Mellor81fa3e82015-05-14 09:17:41 -07004215
4216 if (initialOffset <= selectionStart) {
4217 // Handles have crossed, bound it to the first selected line and
4218 // adjust by word / char as normal.
4219 currLine = layout != null ? layout.getLineForOffset(selectionStart) : mPrevLine;
4220 initialOffset = mTextView.getOffsetAtCoordinate(currLine, x);
4221 }
4222
4223 int offset = initialOffset;
Mady Mellor6c7b4ad2015-04-15 14:23:26 -07004224 int end = getWordEnd(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004225 int start = getWordStart(offset);
4226
Mady Mellorc2225b92015-04-01 15:59:20 -07004227 if (offset > mPreviousOffset) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004228 // User is increasing the selection.
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004229 if (!mInWord || currLine > mPrevLine) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004230 // We're not in a word, or we're on a different line so we'll expand by
4231 // word. First ensure the user has at least entered the next word.
4232 int midPoint = Math.min((end - start) / 2, 2);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004233 if (offset >= start + midPoint || currLine > mPrevLine) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004234 offset = end;
4235 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07004236 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004237 }
4238 }
Mady Mellor81fa3e82015-05-14 09:17:41 -07004239 if (layout != null && offset > initialOffset) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004240 final float adjustedX = layout.getPrimaryHorizontal(offset);
4241 mTouchWordDelta =
4242 adjustedX - mTextView.convertToLocalHorizontalCoordinate(x);
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09004243 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004244 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004245 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004246 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004247 } else {
4248 final int adjustedOffset =
4249 mTextView.getOffsetAtCoordinate(currLine, x + mTouchWordDelta);
4250 if (adjustedOffset < mPreviousOffset || currLine < mPrevLine) {
4251 // User is shrinking the selection.
4252 if (currLine < mPrevLine) {
4253 // We're on a different line, so we'll snap to word boundaries.
4254 offset = end;
Mady Mellor81fa3e82015-05-14 09:17:41 -07004255 if (layout != null && offset > initialOffset) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004256 final float adjustedX = layout.getPrimaryHorizontal(offset);
4257 mTouchWordDelta =
4258 adjustedX - mTextView.convertToLocalHorizontalCoordinate(x);
4259 } else {
4260 mTouchWordDelta = 0.0f;
4261 }
4262 } else {
4263 offset = adjustedOffset;
4264 }
4265 positionCursor = true;
4266 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004267 }
4268
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004269 if (positionCursor) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004270 // Handles can not cross and selection is at least one character.
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004271 if (offset <= selectionStart) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004272 offset = getNextCursorOffset(selectionStart, true);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004273 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004274 }
4275 positionAtCursorOffset(offset, false);
4276 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004277 }
4278
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004279 @Override
Mady Mellor36d5a7b2015-05-22 10:31:12 -07004280 protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
4281 super.positionAtCursorOffset(offset, parentScrolled);
4282 mInWord = !getWordIteratorWithText().isBoundary(offset);
4283 }
4284
4285 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004286 public boolean onTouchEvent(MotionEvent event) {
4287 boolean superResult = super.onTouchEvent(event);
4288 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
4289 // Reset the touch word offset when the user has lifted their finger.
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004290 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004291 }
4292 return superResult;
4293 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004294 }
4295
4296 /**
4297 * A CursorController instance can be used to control a cursor in the text.
4298 */
4299 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
4300 /**
4301 * Makes the cursor controller visible on screen.
4302 * See also {@link #hide()}.
4303 */
4304 public void show();
4305
4306 /**
4307 * Hide the cursor controller from screen.
4308 * See also {@link #show()}.
4309 */
4310 public void hide();
4311
4312 /**
4313 * Called when the view is detached from window. Perform house keeping task, such as
4314 * stopping Runnable thread that would otherwise keep a reference on the context, thus
4315 * preventing the activity from being recycled.
4316 */
4317 public void onDetached();
4318 }
4319
4320 private class InsertionPointCursorController implements CursorController {
4321 private InsertionHandleView mHandle;
4322
4323 public void show() {
4324 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01004325
4326 if (mSelectionModifierCursorController != null) {
4327 mSelectionModifierCursorController.hide();
4328 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004329 }
4330
Gilles Debunned88876a2012-03-16 17:34:04 -07004331 public void hide() {
4332 if (mHandle != null) {
4333 mHandle.hide();
4334 }
4335 }
4336
4337 public void onTouchModeChanged(boolean isInTouchMode) {
4338 if (!isInTouchMode) {
4339 hide();
4340 }
4341 }
4342
4343 private InsertionHandleView getHandle() {
4344 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08004345 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07004346 mTextView.mTextSelectHandleRes);
4347 }
4348 if (mHandle == null) {
4349 mHandle = new InsertionHandleView(mSelectHandleCenter);
4350 }
4351 return mHandle;
4352 }
4353
4354 @Override
4355 public void onDetached() {
4356 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
4357 observer.removeOnTouchModeChangeListener(this);
4358
4359 if (mHandle != null) mHandle.onDetached();
4360 }
4361 }
4362
4363 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07004364 // The cursor controller handles, lazily created when shown.
4365 private SelectionStartHandleView mStartHandle;
4366 private SelectionEndHandleView mEndHandle;
4367 // The offsets of that last touch down event. Remembered to start selection there.
4368 private int mMinTouchOffset, mMaxTouchOffset;
4369
Gilles Debunned88876a2012-03-16 17:34:04 -07004370 private float mDownPositionX, mDownPositionY;
4371 private boolean mGestureStayedInTapRegion;
4372
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004373 // Where the user first starts the drag motion.
4374 private int mStartOffset = -1;
4375 // Indicates whether the user is selecting text and using the drag accelerator.
4376 private boolean mDragAcceleratorActive;
Mady Mellor7a936442015-05-20 10:05:52 -07004377 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004378
Gilles Debunned88876a2012-03-16 17:34:04 -07004379 SelectionModifierCursorController() {
4380 resetTouchOffsets();
4381 }
4382
4383 public void show() {
4384 if (mTextView.isInBatchEditMode()) {
4385 return;
4386 }
4387 initDrawables();
4388 initHandles();
4389 hideInsertionPointCursorController();
4390 }
4391
4392 private void initDrawables() {
4393 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08004394 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07004395 mTextView.mTextSelectHandleLeftRes);
4396 }
4397 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08004398 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07004399 mTextView.mTextSelectHandleRightRes);
4400 }
4401 }
4402
4403 private void initHandles() {
4404 // Lazy object creation has to be done before updatePosition() is called.
4405 if (mStartHandle == null) {
4406 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
4407 }
4408 if (mEndHandle == null) {
4409 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
4410 }
4411
4412 mStartHandle.show();
4413 mEndHandle.show();
4414
Gilles Debunned88876a2012-03-16 17:34:04 -07004415 hideInsertionPointCursorController();
4416 }
4417
4418 public void hide() {
4419 if (mStartHandle != null) mStartHandle.hide();
4420 if (mEndHandle != null) mEndHandle.hide();
4421 }
4422
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004423 public void enterDrag() {
4424 // Just need to init the handles / hide insertion cursor.
4425 show();
4426 mDragAcceleratorActive = true;
4427 // Start location of selection.
4428 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
4429 mLastDownPositionY);
4430 // Don't show the handles until user has lifted finger.
4431 hide();
4432
4433 // This stops scrolling parents from intercepting the touch event, allowing
4434 // the user to continue dragging across the screen to select text; TextView will
4435 // scroll as necessary.
4436 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
4437 }
4438
Gilles Debunned88876a2012-03-16 17:34:04 -07004439 public void onTouchEvent(MotionEvent event) {
4440 // This is done even when the View does not have focus, so that long presses can start
4441 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07004442 final float eventX = event.getX();
4443 final float eventY = event.getY();
Gilles Debunned88876a2012-03-16 17:34:04 -07004444 switch (event.getActionMasked()) {
4445 case MotionEvent.ACTION_DOWN:
Gilles Debunned88876a2012-03-16 17:34:04 -07004446
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004447 // Remember finger down position, to be able to start selection from there.
Mady Mellor7a936442015-05-20 10:05:52 -07004448 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
4449 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004450
4451 // Double tap detection
4452 if (mGestureStayedInTapRegion) {
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004453 if (mDoubleTap) {
Mady Mellor7a936442015-05-20 10:05:52 -07004454 final float deltaX = eventX - mDownPositionX;
4455 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004456 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4457
4458 ViewConfiguration viewConfiguration = ViewConfiguration.get(
4459 mTextView.getContext());
4460 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
4461 boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
4462
Mady Mellor7a936442015-05-20 10:05:52 -07004463 if (stayedInArea && isPositionOnText(eventX, eventY)) {
Clara Bayarridfac4432015-05-15 12:18:24 +01004464 selectCurrentWordAndStartDrag();
Gilles Debunned88876a2012-03-16 17:34:04 -07004465 mDiscardNextActionUp = true;
4466 }
4467 }
4468 }
4469
Mady Mellor7a936442015-05-20 10:05:52 -07004470 mDownPositionX = eventX;
4471 mDownPositionY = eventY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004472 mGestureStayedInTapRegion = true;
Mady Mellor7a936442015-05-20 10:05:52 -07004473 mHaventMovedEnoughToStartDrag = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07004474 break;
4475
4476 case MotionEvent.ACTION_POINTER_DOWN:
4477 case MotionEvent.ACTION_POINTER_UP:
4478 // Handle multi-point gestures. Keep min and max offset positions.
4479 // Only activated for devices that correctly handle multi-touch.
4480 if (mTextView.getContext().getPackageManager().hasSystemFeature(
4481 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
4482 updateMinAndMaxOffsets(event);
4483 }
4484 break;
4485
4486 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07004487 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004488 mTextView.getContext());
Mady Mellor7a936442015-05-20 10:05:52 -07004489 final int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004490
Mady Mellor7a936442015-05-20 10:05:52 -07004491 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
4492 final float deltaX = eventX - mDownPositionX;
4493 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004494 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4495
Mady Mellor7a936442015-05-20 10:05:52 -07004496 if (mGestureStayedInTapRegion) {
4497 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
4498 mGestureStayedInTapRegion =
4499 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
4500 }
4501 if (mHaventMovedEnoughToStartDrag) {
4502 // We don't start dragging until the user has moved enough.
4503 mHaventMovedEnoughToStartDrag =
4504 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07004505 }
4506 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004507
4508 if (mStartHandle != null && mStartHandle.isShowing()) {
4509 // Don't do the drag if the handles are showing already.
4510 break;
4511 }
4512
4513 if (mStartOffset != -1) {
Mady Mellor7a936442015-05-20 10:05:52 -07004514 if (!mHaventMovedEnoughToStartDrag) {
4515 // Offset the finger by the same vertical offset as the handles. This
4516 // improves visibility of the content being selected by shifting
4517 // the finger below the content.
4518 final float fingerOffset = (mStartHandle != null)
4519 ? mStartHandle.getIdealVerticalOffset()
4520 : touchSlop;
4521 int offset =
4522 mTextView.getOffsetForPosition(eventX, eventY - fingerOffset);
4523 int startOffset;
4524 // Snap to word boundaries.
4525 if (mStartOffset < offset) {
4526 // Expanding with end handle.
4527 offset = getWordEnd(offset);
4528 startOffset = getWordStart(mStartOffset);
4529 } else {
4530 // Expanding with start handle.
4531 offset = getWordStart(offset);
4532 startOffset = getWordEnd(mStartOffset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004533 }
Mady Mellor7a936442015-05-20 10:05:52 -07004534 Selection.setSelection((Spannable) mTextView.getText(),
4535 startOffset, offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004536 }
4537 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004538 break;
4539
4540 case MotionEvent.ACTION_UP:
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004541 if (mDragAcceleratorActive) {
4542 // No longer dragging to select text, let the parent intercept events.
4543 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
4544
4545 show();
4546 int startOffset = mTextView.getSelectionStart();
4547 int endOffset = mTextView.getSelectionEnd();
4548
4549 // Since we don't let drag handles pass once they're visible, we need to
4550 // make sure the start / end locations are correct because the user *can*
4551 // switch directions during the initial drag.
4552 if (endOffset < startOffset) {
4553 int tmp = endOffset;
4554 endOffset = startOffset;
4555 startOffset = tmp;
4556
4557 // Also update the selection with the right offsets in this case.
4558 Selection.setSelection((Spannable) mTextView.getText(),
4559 startOffset, endOffset);
4560 }
4561
4562 // Need to do this to display the handles.
4563 mStartHandle.showAtLocation(startOffset);
4564 mEndHandle.showAtLocation(endOffset);
4565
4566 // No longer the first dragging motion, reset.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004567 startSelectionActionMode();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004568 mDragAcceleratorActive = false;
4569 mStartOffset = -1;
4570 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004571 break;
4572 }
4573 }
4574
4575 /**
4576 * @param event
4577 */
4578 private void updateMinAndMaxOffsets(MotionEvent event) {
4579 int pointerCount = event.getPointerCount();
4580 for (int index = 0; index < pointerCount; index++) {
4581 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
4582 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
4583 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
4584 }
4585 }
4586
4587 public int getMinTouchOffset() {
4588 return mMinTouchOffset;
4589 }
4590
4591 public int getMaxTouchOffset() {
4592 return mMaxTouchOffset;
4593 }
4594
4595 public void resetTouchOffsets() {
4596 mMinTouchOffset = mMaxTouchOffset = -1;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004597 mStartOffset = -1;
4598 mDragAcceleratorActive = false;
Gilles Debunned88876a2012-03-16 17:34:04 -07004599 }
4600
4601 /**
4602 * @return true iff this controller is currently used to move the selection start.
4603 */
4604 public boolean isSelectionStartDragged() {
4605 return mStartHandle != null && mStartHandle.isDragging();
4606 }
4607
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004608 /**
4609 * @return true if the user is selecting text using the drag accelerator.
4610 */
4611 public boolean isDragAcceleratorActive() {
4612 return mDragAcceleratorActive;
4613 }
4614
Gilles Debunned88876a2012-03-16 17:34:04 -07004615 public void onTouchModeChanged(boolean isInTouchMode) {
4616 if (!isInTouchMode) {
4617 hide();
4618 }
4619 }
4620
4621 @Override
4622 public void onDetached() {
4623 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
4624 observer.removeOnTouchModeChangeListener(this);
4625
4626 if (mStartHandle != null) mStartHandle.onDetached();
4627 if (mEndHandle != null) mEndHandle.onDetached();
4628 }
4629 }
4630
4631 private class CorrectionHighlighter {
4632 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07004633 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07004634 private int mStart, mEnd;
4635 private long mFadingStartTime;
4636 private RectF mTempRectF;
4637 private final static int FADE_OUT_DURATION = 400;
4638
4639 public CorrectionHighlighter() {
4640 mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
4641 applicationScale);
4642 mPaint.setStyle(Paint.Style.FILL);
4643 }
4644
4645 public void highlight(CorrectionInfo info) {
4646 mStart = info.getOffset();
4647 mEnd = mStart + info.getNewText().length();
4648 mFadingStartTime = SystemClock.uptimeMillis();
4649
4650 if (mStart < 0 || mEnd < 0) {
4651 stopAnimation();
4652 }
4653 }
4654
4655 public void draw(Canvas canvas, int cursorOffsetVertical) {
4656 if (updatePath() && updatePaint()) {
4657 if (cursorOffsetVertical != 0) {
4658 canvas.translate(0, cursorOffsetVertical);
4659 }
4660
4661 canvas.drawPath(mPath, mPaint);
4662
4663 if (cursorOffsetVertical != 0) {
4664 canvas.translate(0, -cursorOffsetVertical);
4665 }
4666 invalidate(true); // TODO invalidate cursor region only
4667 } else {
4668 stopAnimation();
4669 invalidate(false); // TODO invalidate cursor region only
4670 }
4671 }
4672
4673 private boolean updatePaint() {
4674 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
4675 if (duration > FADE_OUT_DURATION) return false;
4676
4677 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
4678 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
4679 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
4680 ((int) (highlightColorAlpha * coef) << 24);
4681 mPaint.setColor(color);
4682 return true;
4683 }
4684
4685 private boolean updatePath() {
4686 final Layout layout = mTextView.getLayout();
4687 if (layout == null) return false;
4688
4689 // Update in case text is edited while the animation is run
4690 final int length = mTextView.getText().length();
4691 int start = Math.min(length, mStart);
4692 int end = Math.min(length, mEnd);
4693
4694 mPath.reset();
4695 layout.getSelectionPath(start, end, mPath);
4696 return true;
4697 }
4698
4699 private void invalidate(boolean delayed) {
4700 if (mTextView.getLayout() == null) return;
4701
4702 if (mTempRectF == null) mTempRectF = new RectF();
4703 mPath.computeBounds(mTempRectF, false);
4704
4705 int left = mTextView.getCompoundPaddingLeft();
4706 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
4707
4708 if (delayed) {
4709 mTextView.postInvalidateOnAnimation(
4710 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
4711 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
4712 } else {
4713 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
4714 (int) mTempRectF.right, (int) mTempRectF.bottom);
4715 }
4716 }
4717
4718 private void stopAnimation() {
4719 Editor.this.mCorrectionHighlighter = null;
4720 }
4721 }
4722
4723 private static class ErrorPopup extends PopupWindow {
4724 private boolean mAbove = false;
4725 private final TextView mView;
4726 private int mPopupInlineErrorBackgroundId = 0;
4727 private int mPopupInlineErrorAboveBackgroundId = 0;
4728
4729 ErrorPopup(TextView v, int width, int height) {
4730 super(v, width, height);
4731 mView = v;
4732 // 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 -08004733 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07004734 // dimensions identical to the above version for this to work (and is more likely).
4735 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4736 com.android.internal.R.styleable.Theme_errorMessageBackground);
4737 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
4738 }
4739
4740 void fixDirection(boolean above) {
4741 mAbove = above;
4742
4743 if (above) {
4744 mPopupInlineErrorAboveBackgroundId =
4745 getResourceId(mPopupInlineErrorAboveBackgroundId,
4746 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
4747 } else {
4748 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4749 com.android.internal.R.styleable.Theme_errorMessageBackground);
4750 }
4751
4752 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
4753 mPopupInlineErrorBackgroundId);
4754 }
4755
4756 private int getResourceId(int currentId, int index) {
4757 if (currentId == 0) {
4758 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
4759 R.styleable.Theme);
4760 currentId = styledAttributes.getResourceId(index, 0);
4761 styledAttributes.recycle();
4762 }
4763 return currentId;
4764 }
4765
4766 @Override
4767 public void update(int x, int y, int w, int h, boolean force) {
4768 super.update(x, y, w, h, force);
4769
4770 boolean above = isAboveAnchor();
4771 if (above != mAbove) {
4772 fixDirection(above);
4773 }
4774 }
4775 }
4776
4777 static class InputContentType {
4778 int imeOptions = EditorInfo.IME_NULL;
4779 String privateImeOptions;
4780 CharSequence imeActionLabel;
4781 int imeActionId;
4782 Bundle extras;
4783 OnEditorActionListener onEditorActionListener;
4784 boolean enterDown;
4785 }
4786
4787 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07004788 ExtractedTextRequest mExtractedTextRequest;
4789 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07004790 int mBatchEditNesting;
4791 boolean mCursorChanged;
4792 boolean mSelectionModeChanged;
4793 boolean mContentChanged;
4794 int mChangedStart, mChangedEnd, mChangedDelta;
4795 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09004796
James Cookf59152c2015-02-26 18:03:58 -08004797 /**
James Cook471559f2015-02-27 10:31:20 -08004798 * @return True iff (start, end) is a valid range within the text.
4799 */
4800 private static boolean isValidRange(CharSequence text, int start, int end) {
4801 return 0 <= start && start <= end && end <= text.length();
4802 }
4803
4804 /**
James Cookf59152c2015-02-26 18:03:58 -08004805 * An InputFilter that monitors text input to maintain undo history. It does not modify the
4806 * text being typed (and hence always returns null from the filter() method).
4807 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004808 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08004809 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004810
James Cook48e0fac2015-02-25 15:44:51 -08004811 // Whether the current filter pass is directly caused by an end-user text edit.
4812 private boolean mIsUserEdit;
4813
James Cookd2026682015-03-03 14:40:14 -08004814 // Whether the text field is handling an IME composition. Must be parceled in case the user
4815 // rotates the screen during composition.
4816 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08004817
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004818 public UndoInputFilter(Editor editor) {
4819 mEditor = editor;
4820 }
4821
James Cookd2026682015-03-03 14:40:14 -08004822 public void saveInstanceState(Parcel parcel) {
4823 parcel.writeInt(mIsUserEdit ? 1 : 0);
4824 parcel.writeInt(mHasComposition ? 1 : 0);
4825 }
4826
4827 public void restoreInstanceState(Parcel parcel) {
4828 mIsUserEdit = parcel.readInt() != 0;
4829 mHasComposition = parcel.readInt() != 0;
4830 }
4831
James Cook48e0fac2015-02-25 15:44:51 -08004832 /**
4833 * Signals that a user-triggered edit is starting.
4834 */
4835 public void beginBatchEdit() {
4836 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
4837 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08004838 }
4839
4840 public void endBatchEdit() {
4841 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
4842 mIsUserEdit = false;
4843 }
4844
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004845 @Override
4846 public CharSequence filter(CharSequence source, int start, int end,
4847 Spanned dest, int dstart, int dend) {
4848 if (DEBUG_UNDO) {
James Cook471559f2015-02-27 10:31:20 -08004849 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " +
4850 "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004851 }
James Cookf1dad1e2015-02-27 11:00:01 -08004852
James Cook48e0fac2015-02-25 15:44:51 -08004853 // Check to see if this edit should be tracked for undo.
4854 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08004855 return null;
4856 }
4857
James Cookd2026682015-03-03 14:40:14 -08004858 // Check for and handle IME composition edits.
4859 if (handleCompositionEdit(source, start, end, dstart)) {
4860 return null;
4861 }
4862
4863 // Handle keyboard edits.
4864 handleKeyboardEdit(source, start, end, dest, dstart, dend);
4865 return null;
4866 }
4867
4868 /**
4869 * Returns true iff the edit was handled, either because it should be ignored or because
4870 * this function created an undo operation for it.
4871 */
4872 private boolean handleCompositionEdit(CharSequence source, int start, int end, int dstart) {
4873 // Ignore edits while the user is composing.
4874 if (isComposition(source)) {
4875 mHasComposition = true;
4876 return true;
4877 }
4878 final boolean hadComposition = mHasComposition;
4879 mHasComposition = false;
4880
4881 // Check for the transition out of the composing state.
4882 if (hadComposition) {
4883 // If there was no text the user canceled composition. Ignore the edit.
4884 if (start == end) {
4885 return true;
4886 }
4887
4888 // Otherwise the user inserted the composition.
4889 String newText = TextUtils.substring(source, start, end);
James Cook22054252015-03-25 14:04:01 -07004890 EditOperation edit = new EditOperation(mEditor, "", dstart, newText);
4891 recordEdit(edit, false /* forceMerge */);
James Cookd2026682015-03-03 14:40:14 -08004892 return true;
4893 }
4894
4895 // This was neither a composition event nor a transition out of composing.
4896 return false;
4897 }
4898
4899 private void handleKeyboardEdit(CharSequence source, int start, int end,
4900 Spanned dest, int dstart, int dend) {
James Cook48e0fac2015-02-25 15:44:51 -08004901 // An application may install a TextWatcher to provide additional modifications after
4902 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
4903 // string). This results in multiple filter() calls for what the user considers to be
4904 // a single operation. Always undo the whole set of changes in one step.
James Cookd2026682015-03-03 14:40:14 -08004905 final boolean forceMerge = isInTextWatcher();
James Cook471559f2015-02-27 10:31:20 -08004906
4907 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08004908 String newText = TextUtils.substring(source, start, end);
4909 String oldText = TextUtils.substring(dest, dstart, dend);
James Cook22054252015-03-25 14:04:01 -07004910 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText);
4911 recordEdit(edit, forceMerge);
James Cookd2026682015-03-03 14:40:14 -08004912 }
James Cook471559f2015-02-27 10:31:20 -08004913
James Cook22054252015-03-25 14:04:01 -07004914 /**
4915 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
4916 * If forceMerge is true then the new edit is always merged.
4917 */
4918 private void recordEdit(EditOperation edit, boolean forceMerge) {
James Cook471559f2015-02-27 10:31:20 -08004919 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08004920 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004921 um.beginUpdate("Edit text");
James Cook471559f2015-02-27 10:31:20 -08004922 EditOperation lastEdit = um.getLastOperation(
4923 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
4924 if (lastEdit == null) {
4925 // Add this as the first edit.
4926 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
4927 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook22054252015-03-25 14:04:01 -07004928 } else if (forceMerge) {
4929 // Forced merges take priority because they could be the result of a non-user-edit
4930 // change and this case should not create a new undo operation.
4931 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
4932 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08004933 } else if (!mIsUserEdit) {
4934 // An application directly modified the Editable outside of a text edit. Treat this
4935 // as a new change and don't attempt to merge.
4936 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
4937 um.commitState(mEditor.mUndoOwner);
4938 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook471559f2015-02-27 10:31:20 -08004939 } else if (lastEdit.mergeWith(edit)) {
4940 // Merge succeeded, nothing else to do.
4941 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08004942 } else {
James Cook471559f2015-02-27 10:31:20 -08004943 // Could not merge with the last edit, so commit the last edit and add this edit.
4944 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
4945 um.commitState(mEditor.mUndoOwner);
4946 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08004947 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004948 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004949 }
James Cook48e0fac2015-02-25 15:44:51 -08004950
4951 private boolean canUndoEdit(CharSequence source, int start, int end,
4952 Spanned dest, int dstart, int dend) {
4953 if (!mEditor.mAllowUndo) {
4954 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
4955 return false;
4956 }
4957
4958 if (mEditor.mUndoManager.isInUndo()) {
4959 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
4960 return false;
4961 }
4962
4963 // Text filters run before input operations are applied. However, some input operations
4964 // are invalid and will throw exceptions when applied. This is common in tests. Don't
4965 // attempt to undo invalid operations.
4966 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
4967 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
4968 return false;
4969 }
4970
4971 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
4972 // on an input field. Skip no-op changes.
4973 if (start == end && dstart == dend) {
4974 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
4975 return false;
4976 }
4977
4978 return true;
4979 }
James Cookd2026682015-03-03 14:40:14 -08004980
4981 private boolean isComposition(CharSequence source) {
4982 if (!(source instanceof Spannable)) {
4983 return false;
4984 }
4985 // This is a composition edit if the source has a non-zero-length composing span.
4986 Spannable text = (Spannable) source;
4987 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
4988 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
4989 return composeBegin < composeEnd;
4990 }
4991
4992 private boolean isInTextWatcher() {
4993 CharSequence text = mEditor.mTextView.getText();
4994 return (text instanceof SpannableStringBuilder)
4995 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
4996 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07004997 }
4998
James Cookf59152c2015-02-26 18:03:58 -08004999 /**
5000 * An operation to undo a single "edit" to a text view.
5001 */
James Cook471559f2015-02-27 10:31:20 -08005002 public static class EditOperation extends UndoOperation<Editor> {
5003 private static final int TYPE_INSERT = 0;
5004 private static final int TYPE_DELETE = 1;
5005 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005006
James Cook471559f2015-02-27 10:31:20 -08005007 private int mType;
5008 private String mOldText;
5009 private int mOldTextStart;
5010 private String mNewText;
5011 private int mNewTextStart;
5012
5013 private int mOldCursorPos;
5014 private int mNewCursorPos;
5015
5016 /**
James Cookd2026682015-03-03 14:40:14 -08005017 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07005018 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08005019 */
James Cook22054252015-03-25 14:04:01 -07005020 public EditOperation(Editor editor, String oldText, int dstart, String newText) {
James Cook471559f2015-02-27 10:31:20 -08005021 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08005022 mOldText = oldText;
5023 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08005024
5025 // Determine the type of the edit and store where it occurred. Avoid storing
5026 // irrevelant data (e.g. mNewTextStart for a delete) because that makes the
5027 // merging logic more complex (e.g. merging deletes could lead to mNewTextStart being
5028 // outside the bounds of the final text).
5029 if (mNewText.length() > 0 && mOldText.length() == 0) {
5030 mType = TYPE_INSERT;
5031 mNewTextStart = dstart;
5032 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
5033 mType = TYPE_DELETE;
5034 mOldTextStart = dstart;
5035 } else {
5036 mType = TYPE_REPLACE;
5037 mOldTextStart = mNewTextStart = dstart;
5038 }
5039
5040 // Store cursor data.
5041 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08005042 mNewCursorPos = dstart + mNewText.length();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005043 }
5044
James Cook471559f2015-02-27 10:31:20 -08005045 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005046 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08005047 mType = src.readInt();
5048 mOldText = src.readString();
5049 mOldTextStart = src.readInt();
5050 mNewText = src.readString();
5051 mNewTextStart = src.readInt();
5052 mOldCursorPos = src.readInt();
5053 mNewCursorPos = src.readInt();
5054 }
5055
5056 @Override
5057 public void writeToParcel(Parcel dest, int flags) {
5058 dest.writeInt(mType);
5059 dest.writeString(mOldText);
5060 dest.writeInt(mOldTextStart);
5061 dest.writeString(mNewText);
5062 dest.writeInt(mNewTextStart);
5063 dest.writeInt(mOldCursorPos);
5064 dest.writeInt(mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005065 }
5066
James Cook48e0fac2015-02-25 15:44:51 -08005067 private int getNewTextEnd() {
5068 return mNewTextStart + mNewText.length();
5069 }
5070
5071 private int getOldTextEnd() {
5072 return mOldTextStart + mOldText.length();
5073 }
5074
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005075 @Override
5076 public void commit() {
5077 }
5078
5079 @Override
5080 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08005081 if (DEBUG_UNDO) Log.d(TAG, "undo");
5082 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08005083 Editor editor = getOwnerData();
5084 Editable text = (Editable) editor.mTextView.getText();
5085 modifyText(text, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart,
5086 mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005087 }
5088
5089 @Override
5090 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08005091 if (DEBUG_UNDO) Log.d(TAG, "redo");
5092 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08005093 Editor editor = getOwnerData();
5094 Editable text = (Editable) editor.mTextView.getText();
5095 modifyText(text, mOldTextStart, getOldTextEnd(), mNewText, mNewTextStart,
5096 mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005097 }
5098
James Cook471559f2015-02-27 10:31:20 -08005099 /**
5100 * Attempts to merge this existing operation with a new edit.
5101 * @param edit The new edit operation.
5102 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
5103 * object unchanged.
5104 */
5105 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08005106 if (DEBUG_UNDO) {
5107 Log.d(TAG, "mergeWith old " + this);
5108 Log.d(TAG, "mergeWith new " + edit);
5109 }
James Cook471559f2015-02-27 10:31:20 -08005110 switch (mType) {
5111 case TYPE_INSERT:
5112 return mergeInsertWith(edit);
5113 case TYPE_DELETE:
5114 return mergeDeleteWith(edit);
5115 case TYPE_REPLACE:
5116 return mergeReplaceWith(edit);
5117 default:
5118 return false;
5119 }
5120 }
5121
5122 private boolean mergeInsertWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08005123 // Only merge continuous insertions.
5124 if (edit.mType != TYPE_INSERT) {
5125 return false;
5126 }
5127 // Only merge insertions that are contiguous.
5128 if (getNewTextEnd() != edit.mNewTextStart) {
5129 return false;
5130 }
5131 mNewText += edit.mNewText;
5132 mNewCursorPos = edit.mNewCursorPos;
5133 return true;
5134 }
5135
5136 // TODO: Support forward delete.
5137 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08005138 // Only merge continuous deletes.
5139 if (edit.mType != TYPE_DELETE) {
5140 return false;
5141 }
5142 // Only merge deletions that are contiguous.
5143 if (mOldTextStart != edit.getOldTextEnd()) {
5144 return false;
5145 }
5146 mOldTextStart = edit.mOldTextStart;
5147 mOldText = edit.mOldText + mOldText;
5148 mNewCursorPos = edit.mNewCursorPos;
5149 return true;
5150 }
5151
5152 private boolean mergeReplaceWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08005153 // Replacements can merge only with adjacent inserts.
5154 if (edit.mType != TYPE_INSERT || getNewTextEnd() != edit.mNewTextStart) {
James Cook471559f2015-02-27 10:31:20 -08005155 return false;
5156 }
5157 mOldText += edit.mOldText;
5158 mNewText += edit.mNewText;
5159 mNewCursorPos = edit.mNewCursorPos;
5160 return true;
5161 }
5162
James Cook48e0fac2015-02-25 15:44:51 -08005163 /**
5164 * Forcibly creates a single merged edit operation by simulating the entire text
5165 * contents being replaced.
5166 */
James Cook22054252015-03-25 14:04:01 -07005167 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08005168 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
James Cookf59152c2015-02-26 18:03:58 -08005169 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08005170
5171 // Copy the text of the current field.
5172 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
5173 // but would require two parallel implementations of modifyText() because Editable and
5174 // StringBuilder do not share an interface for replace/delete/insert.
5175 Editable editable = (Editable) editor.mTextView.getText();
5176 Editable originalText = new SpannableStringBuilder(editable.toString());
5177
5178 // Roll back the last operation.
5179 modifyText(originalText, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart,
5180 mOldCursorPos);
5181
5182 // Clone the text again and apply the new operation.
5183 Editable finalText = new SpannableStringBuilder(editable.toString());
5184 modifyText(finalText, edit.mOldTextStart, edit.getOldTextEnd(), edit.mNewText,
5185 edit.mNewTextStart, edit.mNewCursorPos);
5186
5187 // Convert this operation into a non-mergeable replacement of the entire string.
5188 mType = TYPE_REPLACE;
5189 mNewText = finalText.toString();
5190 mNewTextStart = 0;
5191 mOldText = originalText.toString();
5192 mOldTextStart = 0;
5193 mNewCursorPos = edit.mNewCursorPos;
5194 // mOldCursorPos is unchanged.
5195 }
5196
5197 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
5198 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08005199 // Apply the edit if it is still valid.
5200 if (isValidRange(text, deleteFrom, deleteTo) &&
5201 newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
5202 if (deleteFrom != deleteTo) {
5203 text.delete(deleteFrom, deleteTo);
5204 }
5205 if (newText.length() != 0) {
5206 text.insert(newTextInsertAt, newText);
5207 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005208 }
James Cook900185d2015-03-10 09:48:11 -07005209 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
5210 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08005211 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07005212 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08005213 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005214 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005215 }
5216
James Cook48e0fac2015-02-25 15:44:51 -08005217 private String getTypeString() {
5218 switch (mType) {
5219 case TYPE_INSERT:
5220 return "insert";
5221 case TYPE_DELETE:
5222 return "delete";
5223 case TYPE_REPLACE:
5224 return "replace";
5225 default:
5226 return "";
5227 }
5228 }
5229
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005230 @Override
James Cook471559f2015-02-27 10:31:20 -08005231 public String toString() {
James Cook48e0fac2015-02-25 15:44:51 -08005232 return "[mType=" + getTypeString() + ", " +
James Cook471559f2015-02-27 10:31:20 -08005233 "mOldText=" + mOldText + ", " +
5234 "mOldTextStart=" + mOldTextStart + ", " +
5235 "mNewText=" + mNewText + ", " +
5236 "mNewTextStart=" + mNewTextStart + ", " +
5237 "mOldCursorPos=" + mOldCursorPos + ", " +
5238 "mNewCursorPos=" + mNewCursorPos + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005239 }
5240
James Cook471559f2015-02-27 10:31:20 -08005241 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR
5242 = new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08005243 @Override
James Cook471559f2015-02-27 10:31:20 -08005244 public EditOperation createFromParcel(Parcel in) {
5245 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005246 }
5247
James Cookf59152c2015-02-26 18:03:58 -08005248 @Override
James Cook471559f2015-02-27 10:31:20 -08005249 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
5250 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005251 }
5252
James Cookf59152c2015-02-26 18:03:58 -08005253 @Override
James Cook471559f2015-02-27 10:31:20 -08005254 public EditOperation[] newArray(int size) {
5255 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005256 }
5257 };
5258 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005259}