blob: de509b2d7b9a5c601161f342174915a8b3b598a7 [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
Keisuke Kuroyanagi88d92da2015-04-16 17:41:06 +0900243 if (widget.didTouchFocusSelect() && !isMouse) {
244 return handled;
245 }
246 if (action == MotionEvent.ACTION_DOWN) {
247 // Capture the mouse pointer down location to ensure selection starts
248 // right under the mouse (and is not influenced by cursor location).
249 // The code below needs to run for mouse events.
250 // For touch events, the code should run only when selection is active.
251 if (isMouse || isTouchSelecting(isMouse, buffer)) {
252 if (!widget.isFocused()) {
253 if (!widget.requestFocus()) {
254 return handled;
255 }
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700256 }
Keisuke Kuroyanagi88d92da2015-04-16 17:41:06 +0900257 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
258 buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
259 // Disallow intercepting of the touch events, so that
260 // users can scroll and select at the same time.
261 // without this, users would get booted out of select
262 // mode once the view detected it needed to scroll.
263 widget.getParent().requestDisallowInterceptTouchEvent(true);
264 }
265 } else if (widget.isFocused()) {
266 if (action == MotionEvent.ACTION_MOVE) {
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700267 // Cursor can be active at any location in the text while mouse pointer can start
268 // selection from a totally different location. Use LAST_TAP_DOWN span to ensure
269 // text selection will start from mouse pointer location.
270 if (isMouse && Touch.isSelectionStarted(buffer)) {
271 int offset = buffer.getSpanStart(LAST_TAP_DOWN);
272 Selection.setSelection(buffer, offset);
273 }
274
275 if (isTouchSelecting(isMouse, buffer) && handled) {
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500276 // Before selecting, make sure we've moved out of the "slop".
277 // handled will be true, if we're in select mode AND we're
278 // OUT of the slop
Maryam Garrettce083792009-12-01 14:40:03 -0500279
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500280 // Turn long press off while we're selecting. User needs to
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700281 // re-tap on the selection to enable long press
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500282 widget.cancelLongPress();
Maryam Garrettce083792009-12-01 14:40:03 -0500283
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500284 // Update selection as we're moving the selection area.
Maryam Garrettce083792009-12-01 14:40:03 -0500285
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500286 // Get the current touch position
Gilles Debunne3bca69b2011-05-23 18:20:22 -0700287 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500288
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700289 Selection.extendSelection(buffer, offset);
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500290 return true;
Maryam Garrettce083792009-12-01 14:40:03 -0500291 }
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800292 } else if (action == MotionEvent.ACTION_UP) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700293 // If we have scrolled, then the up shouldn't move the cursor,
294 // but we do need to make sure the cursor is still visible at
295 // the current scroll offset to avoid the scroll jumping later
296 // to show it.
297 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700298 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700299 widget.moveCursorToVisibleOffset();
300 return true;
301 }
Maryam Garrettce083792009-12-01 14:40:03 -0500302
Gilles Debunne3bca69b2011-05-23 18:20:22 -0700303 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
Sujith Ramakrishnancc32bd82014-05-19 15:32:13 -0700304 if (isTouchSelecting(isMouse, buffer)) {
Maryam Garrett62c4ad32010-01-06 13:06:20 -0500305 buffer.removeSpan(LAST_TAP_DOWN);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700306 Selection.extendSelection(buffer, offset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800307 }
308
309 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
310 MetaKeyKeyListener.resetLockedMeta(buffer);
311
312 return true;
313 }
314 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800315 return handled;
316 }
317
Jeff Brown67b6ab72010-12-17 18:33:02 -0800318 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800319 public boolean canSelectArbitrarily() {
320 return true;
321 }
322
Jeff Brown67b6ab72010-12-17 18:33:02 -0800323 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800324 public void initialize(TextView widget, Spannable text) {
325 Selection.setSelection(text, 0);
326 }
327
Jeff Brown67b6ab72010-12-17 18:33:02 -0800328 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800329 public void onTakeFocus(TextView view, Spannable text, int dir) {
330 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
Gilles Debunne2703a422010-08-23 15:14:03 -0700331 if (view.getLayout() == null) {
332 // This shouldn't be null, but do something sensible if it is.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800333 Selection.setSelection(text, text.length());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800334 }
335 } else {
336 Selection.setSelection(text, text.length());
337 }
338 }
339
340 public static MovementMethod getInstance() {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700341 if (sInstance == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800342 sInstance = new ArrowKeyMovementMethod();
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700343 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800344
345 return sInstance;
346 }
347
Maryam Garrettce083792009-12-01 14:40:03 -0500348 private static final Object LAST_TAP_DOWN = new Object();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800349 private static ArrowKeyMovementMethod sInstance;
350}