blob: fcc3a4007e42079e4b3c401d9b6beffe87ec367c [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 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.text.method;
18
Jeff Brown67b6ab72010-12-17 18:33:02 -080019import android.graphics.Rect;
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070020import android.text.Layout;
21import android.text.Selection;
22import android.text.Spannable;
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -070023import android.view.InputDevice;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024import android.view.KeyEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.view.MotionEvent;
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070026import android.view.View;
27import android.widget.TextView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028
Jeff Brown67b6ab72010-12-17 18:33:02 -080029/**
30 * A movement method that provides cursor movement and selection.
31 * Supports displaying the context menu on DPad Center.
32 */
33public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
34 private static boolean isSelecting(Spannable buffer) {
Jeff Brown497a92c2010-09-12 17:55:08 -070035 return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) ||
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070036 (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
37 }
38
Gilles Debunne287d6c62011-10-05 18:22:11 -070039 private static int getCurrentLineTop(Spannable buffer, Layout layout) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080040 return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070041 }
42
Gilles Debunne287d6c62011-10-05 18:22:11 -070043 private static int getPageHeight(TextView widget) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080044 // This calculation does not take into account the view transformations that
45 // may have been applied to the child or its containers. In case of scaling or
46 // rotation, the calculated page height may be incorrect.
47 final Rect rect = new Rect();
48 return widget.getGlobalVisibleRect(rect) ? rect.height() : 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080049 }
50
Jeff Brown67b6ab72010-12-17 18:33:02 -080051 @Override
52 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
53 int movementMetaState, KeyEvent event) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054 switch (keyCode) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080055 case KeyEvent.KEYCODE_DPAD_CENTER:
56 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
57 if (event.getAction() == KeyEvent.ACTION_DOWN
58 && event.getRepeatCount() == 0
59 && MetaKeyKeyListener.getMetaState(buffer,
Raph Levien14f10e52013-09-18 13:01:11 -070060 MetaKeyKeyListener.META_SELECTING, event) != 0) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080061 return widget.showContextMenu();
62 }
63 }
64 break;
65 }
66 return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
67 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068
Jeff Brown67b6ab72010-12-17 18:33:02 -080069 @Override
70 protected boolean left(TextView widget, Spannable buffer) {
71 final Layout layout = widget.getLayout();
72 if (isSelecting(buffer)) {
73 return Selection.extendLeft(buffer, layout);
74 } else {
75 return Selection.moveLeft(buffer, layout);
76 }
77 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078
Jeff Brown67b6ab72010-12-17 18:33:02 -080079 @Override
80 protected boolean right(TextView widget, Spannable buffer) {
81 final Layout layout = widget.getLayout();
82 if (isSelecting(buffer)) {
83 return Selection.extendRight(buffer, layout);
84 } else {
85 return Selection.moveRight(buffer, layout);
86 }
87 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080088
Jeff Brown67b6ab72010-12-17 18:33:02 -080089 @Override
90 protected boolean up(TextView widget, Spannable buffer) {
91 final Layout layout = widget.getLayout();
92 if (isSelecting(buffer)) {
93 return Selection.extendUp(buffer, layout);
94 } else {
95 return Selection.moveUp(buffer, layout);
96 }
97 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080098
Jeff Brown67b6ab72010-12-17 18:33:02 -080099 @Override
100 protected boolean down(TextView widget, Spannable buffer) {
101 final Layout layout = widget.getLayout();
102 if (isSelecting(buffer)) {
103 return Selection.extendDown(buffer, layout);
104 } else {
105 return Selection.moveDown(buffer, layout);
106 }
107 }
108
109 @Override
110 protected boolean pageUp(TextView widget, Spannable buffer) {
111 final Layout layout = widget.getLayout();
112 final boolean selecting = isSelecting(buffer);
113 final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget);
114 boolean handled = false;
115 for (;;) {
116 final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
117 if (selecting) {
118 Selection.extendUp(buffer, layout);
119 } else {
120 Selection.moveUp(buffer, layout);
121 }
122 if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
123 break;
124 }
125 handled = true;
126 if (getCurrentLineTop(buffer, layout) <= targetY) {
127 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800128 }
129 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800130 return handled;
131 }
132
Jeff Brown67b6ab72010-12-17 18:33:02 -0800133 @Override
134 protected boolean pageDown(TextView widget, Spannable buffer) {
135 final Layout layout = widget.getLayout();
136 final boolean selecting = isSelecting(buffer);
137 final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget);
138 boolean handled = false;
139 for (;;) {
140 final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
141 if (selecting) {
142 Selection.extendDown(buffer, layout);
143 } else {
144 Selection.moveDown(buffer, layout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800145 }
Jeff Brown67b6ab72010-12-17 18:33:02 -0800146 if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
147 break;
148 }
149 handled = true;
150 if (getCurrentLineTop(buffer, layout) >= targetY) {
151 break;
152 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800153 }
Jeff Brown67b6ab72010-12-17 18:33:02 -0800154 return handled;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800155 }
Maryam Garrettce083792009-12-01 14:40:03 -0500156
Jeff Brown67b6ab72010-12-17 18:33:02 -0800157 @Override
158 protected boolean top(TextView widget, Spannable buffer) {
159 if (isSelecting(buffer)) {
160 Selection.extendSelection(buffer, 0);
161 } else {
162 Selection.setSelection(buffer, 0);
163 }
164 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800165 }
Maryam Garrettce083792009-12-01 14:40:03 -0500166
Jeff Brown67b6ab72010-12-17 18:33:02 -0800167 @Override
168 protected boolean bottom(TextView widget, Spannable buffer) {
169 if (isSelecting(buffer)) {
170 Selection.extendSelection(buffer, buffer.length());
171 } else {
172 Selection.setSelection(buffer, buffer.length());
173 }
174 return true;
175 }
176
177 @Override
178 protected boolean lineStart(TextView widget, Spannable buffer) {
179 final Layout layout = widget.getLayout();
180 if (isSelecting(buffer)) {
181 return Selection.extendToLeftEdge(buffer, layout);
182 } else {
183 return Selection.moveToLeftEdge(buffer, layout);
184 }
185 }
186
187 @Override
188 protected boolean lineEnd(TextView widget, Spannable buffer) {
189 final Layout layout = widget.getLayout();
190 if (isSelecting(buffer)) {
191 return Selection.extendToRightEdge(buffer, layout);
192 } else {
193 return Selection.moveToRightEdge(buffer, layout);
194 }
195 }
196
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700197 /** {@hide} */
198 @Override
199 protected boolean leftWord(TextView widget, Spannable buffer) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700200 final int selectionEnd = widget.getSelectionEnd();
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700201 final WordIterator wordIterator = widget.getWordIterator();
202 wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
203 return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer));
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700204 }
205
206 /** {@hide} */
207 @Override
208 protected boolean rightWord(TextView widget, Spannable buffer) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700209 final int selectionEnd = widget.getSelectionEnd();
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700210 final WordIterator wordIterator = widget.getWordIterator();
211 wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
212 return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer));
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700213 }
214
Jeff Brown67b6ab72010-12-17 18:33:02 -0800215 @Override
216 protected boolean home(TextView widget, Spannable buffer) {
217 return lineStart(widget, buffer);
218 }
219
220 @Override
221 protected boolean end(TextView widget, Spannable buffer) {
222 return lineEnd(widget, buffer);
223 }
224
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700225 private static boolean isTouchSelecting(boolean isMouse, Spannable buffer) {
226 return isMouse ? Touch.isActivelySelecting(buffer) : isSelecting(buffer);
227 }
228
Jeff Brown67b6ab72010-12-17 18:33:02 -0800229 @Override
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700230 public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700231 int initialScrollX = -1;
232 int initialScrollY = -1;
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800233 final int action = event.getAction();
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700234 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800235
236 if (action == MotionEvent.ACTION_UP) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700237 initialScrollX = Touch.getInitialScrollX(widget, buffer);
238 initialScrollY = Touch.getInitialScrollY(widget, buffer);
239 }
Maryam Garrettce083792009-12-01 14:40:03 -0500240
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800241 boolean handled = Touch.onTouchEvent(widget, buffer, event);
242
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700243 if (widget.isFocused() && !widget.didTouchFocusSelect()) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800244 if (action == MotionEvent.ACTION_DOWN) {
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700245 // Capture the mouse pointer down location to ensure selection starts
246 // right under the mouse (and is not influenced by cursor location).
247 // The code below needs to run for mouse events.
248 // For touch events, the code should run only when selection is active.
249 if (isMouse || isTouchSelecting(isMouse, buffer)) {
250 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
251 buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
252 // Disallow intercepting of the touch events, so that
253 // users can scroll and select at the same time.
254 // without this, users would get booted out of select
255 // mode once the view detected it needed to scroll.
256 widget.getParent().requestDisallowInterceptTouchEvent(true);
257 }
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800258 } else if (action == MotionEvent.ACTION_MOVE) {
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700259
260 // Cursor can be active at any location in the text while mouse pointer can start
261 // selection from a totally different location. Use LAST_TAP_DOWN span to ensure
262 // text selection will start from mouse pointer location.
263 if (isMouse && Touch.isSelectionStarted(buffer)) {
264 int offset = buffer.getSpanStart(LAST_TAP_DOWN);
265 Selection.setSelection(buffer, offset);
266 }
267
268 if (isTouchSelecting(isMouse, buffer) && handled) {
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500269 // Before selecting, make sure we've moved out of the "slop".
270 // handled will be true, if we're in select mode AND we're
271 // OUT of the slop
Maryam Garrettce083792009-12-01 14:40:03 -0500272
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500273 // Turn long press off while we're selecting. User needs to
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700274 // re-tap on the selection to enable long press
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500275 widget.cancelLongPress();
Maryam Garrettce083792009-12-01 14:40:03 -0500276
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500277 // Update selection as we're moving the selection area.
Maryam Garrettce083792009-12-01 14:40:03 -0500278
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500279 // Get the current touch position
Gilles Debunne3bca69b2011-05-23 18:20:22 -0700280 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500281
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700282 Selection.extendSelection(buffer, offset);
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500283 return true;
Maryam Garrettce083792009-12-01 14:40:03 -0500284 }
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800285 } else if (action == MotionEvent.ACTION_UP) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700286 // If we have scrolled, then the up shouldn't move the cursor,
287 // but we do need to make sure the cursor is still visible at
288 // the current scroll offset to avoid the scroll jumping later
289 // to show it.
290 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700291 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700292 widget.moveCursorToVisibleOffset();
293 return true;
294 }
Maryam Garrettce083792009-12-01 14:40:03 -0500295
Gilles Debunne3bca69b2011-05-23 18:20:22 -0700296 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700297 if (isTouchSelecting(isMouse, buffer)) {
Maryam Garrett62c4ad32010-01-06 13:06:20 -0500298 buffer.removeSpan(LAST_TAP_DOWN);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700299 Selection.extendSelection(buffer, offset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800300 }
301
302 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
303 MetaKeyKeyListener.resetLockedMeta(buffer);
304
305 return true;
306 }
307 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800308 return handled;
309 }
310
Jeff Brown67b6ab72010-12-17 18:33:02 -0800311 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800312 public boolean canSelectArbitrarily() {
313 return true;
314 }
315
Jeff Brown67b6ab72010-12-17 18:33:02 -0800316 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800317 public void initialize(TextView widget, Spannable text) {
318 Selection.setSelection(text, 0);
319 }
320
Jeff Brown67b6ab72010-12-17 18:33:02 -0800321 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800322 public void onTakeFocus(TextView view, Spannable text, int dir) {
323 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
Gilles Debunne2703a422010-08-23 15:14:03 -0700324 if (view.getLayout() == null) {
325 // This shouldn't be null, but do something sensible if it is.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800326 Selection.setSelection(text, text.length());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800327 }
328 } else {
329 Selection.setSelection(text, text.length());
330 }
331 }
332
333 public static MovementMethod getInstance() {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700334 if (sInstance == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800335 sInstance = new ArrowKeyMovementMethod();
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700336 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800337
338 return sInstance;
339 }
340
Maryam Garrettce083792009-12-01 14:40:03 -0500341 private static final Object LAST_TAP_DOWN = new Object();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800342 private static ArrowKeyMovementMethod sInstance;
343}