1 /*
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 
17 package android.text.method;
18 
19 import android.graphics.Rect;
20 import android.text.Layout;
21 import android.text.Selection;
22 import android.text.Spannable;
23 import android.view.InputDevice;
24 import android.view.KeyEvent;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.widget.TextView;
28 
29 /**
30  * A movement method that provides cursor movement and selection.
31  * Supports displaying the context menu on DPad Center.
32  */
33 public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
isSelecting(Spannable buffer)34     private static boolean isSelecting(Spannable buffer) {
35         return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) ||
36                 (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
37     }
38 
getCurrentLineTop(Spannable buffer, Layout layout)39     private static int getCurrentLineTop(Spannable buffer, Layout layout) {
40         return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
41     }
42 
getPageHeight(TextView widget)43     private static int getPageHeight(TextView widget) {
44         // 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;
49     }
50 
51     @Override
handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event)52     protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
53             int movementMetaState, KeyEvent event) {
54         switch (keyCode) {
55             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,
60                                         MetaKeyKeyListener.META_SELECTING, event) != 0) {
61                         return widget.showContextMenu();
62                     }
63                 }
64                 break;
65         }
66         return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
67     }
68 
69     @Override
left(TextView widget, Spannable buffer)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     }
78 
79     @Override
right(TextView widget, Spannable buffer)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     }
88 
89     @Override
up(TextView widget, Spannable buffer)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     }
98 
99     @Override
down(TextView widget, Spannable buffer)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
pageUp(TextView widget, Spannable buffer)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;
128             }
129         }
130         return handled;
131     }
132 
133     @Override
pageDown(TextView widget, Spannable buffer)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);
145             }
146             if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
147                 break;
148             }
149             handled = true;
150             if (getCurrentLineTop(buffer, layout) >= targetY) {
151                 break;
152             }
153         }
154         return handled;
155     }
156 
157     @Override
top(TextView widget, Spannable buffer)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;
165     }
166 
167     @Override
bottom(TextView widget, Spannable buffer)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
lineStart(TextView widget, Spannable buffer)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
lineEnd(TextView widget, Spannable buffer)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 
197     /** {@hide} */
198     @Override
leftWord(TextView widget, Spannable buffer)199     protected boolean leftWord(TextView widget, Spannable buffer) {
200         final int selectionEnd = widget.getSelectionEnd();
201         final WordIterator wordIterator = widget.getWordIterator();
202         wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
203         return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer));
204     }
205 
206     /** {@hide} */
207     @Override
rightWord(TextView widget, Spannable buffer)208     protected boolean rightWord(TextView widget, Spannable buffer) {
209         final int selectionEnd = widget.getSelectionEnd();
210         final WordIterator wordIterator = widget.getWordIterator();
211         wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
212         return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer));
213     }
214 
215     @Override
home(TextView widget, Spannable buffer)216     protected boolean home(TextView widget, Spannable buffer) {
217         return lineStart(widget, buffer);
218     }
219 
220     @Override
end(TextView widget, Spannable buffer)221     protected boolean end(TextView widget, Spannable buffer) {
222         return lineEnd(widget, buffer);
223     }
224 
isTouchSelecting(boolean isMouse, Spannable buffer)225     private static boolean isTouchSelecting(boolean isMouse, Spannable buffer) {
226         return isMouse ? Touch.isActivelySelecting(buffer) : isSelecting(buffer);
227     }
228 
229     @Override
onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)230     public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
231         int initialScrollX = -1;
232         int initialScrollY = -1;
233         final int action = event.getAction();
234         final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
235 
236         if (action == MotionEvent.ACTION_UP) {
237             initialScrollX = Touch.getInitialScrollX(widget, buffer);
238             initialScrollY = Touch.getInitialScrollY(widget, buffer);
239         }
240 
241         boolean handled = Touch.onTouchEvent(widget, buffer, event);
242 
243         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                     }
256                 }
257                 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) {
267                 // 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) {
276                     // 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
279 
280                     // Turn long press off while we're selecting. User needs to
281                     // re-tap on the selection to enable long press
282                     widget.cancelLongPress();
283 
284                     // Update selection as we're moving the selection area.
285 
286                     // Get the current touch position
287                     int offset = widget.getOffsetForPosition(event.getX(), event.getY());
288 
289                     Selection.extendSelection(buffer, offset);
290                     return true;
291                 }
292             } else if (action == MotionEvent.ACTION_UP) {
293                 // 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()) ||
298                     (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
299                     widget.moveCursorToVisibleOffset();
300                     return true;
301                 }
302 
303                 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
304                 if (isTouchSelecting(isMouse, buffer)) {
305                     buffer.removeSpan(LAST_TAP_DOWN);
306                     Selection.extendSelection(buffer, offset);
307                 }
308 
309                 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
310                 MetaKeyKeyListener.resetLockedMeta(buffer);
311 
312                 return true;
313             }
314         }
315         return handled;
316     }
317 
318     @Override
canSelectArbitrarily()319     public boolean canSelectArbitrarily() {
320         return true;
321     }
322 
323     @Override
initialize(TextView widget, Spannable text)324     public void initialize(TextView widget, Spannable text) {
325         Selection.setSelection(text, 0);
326     }
327 
328     @Override
onTakeFocus(TextView view, Spannable text, int dir)329     public void onTakeFocus(TextView view, Spannable text, int dir) {
330         if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
331             if (view.getLayout() == null) {
332                 // This shouldn't be null, but do something sensible if it is.
333                 Selection.setSelection(text, text.length());
334             }
335         } else {
336             Selection.setSelection(text, text.length());
337         }
338     }
339 
getInstance()340     public static MovementMethod getInstance() {
341         if (sInstance == null) {
342             sInstance = new ArrowKeyMovementMethod();
343         }
344 
345         return sInstance;
346     }
347 
348     private static final Object LAST_TAP_DOWN = new Object();
349     private static ArrowKeyMovementMethod sInstance;
350 }
351