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.KeyEvent;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.widget.TextView;
27 
28 /**
29  * A movement method that provides cursor movement and selection.
30  * Supports displaying the context menu on DPad Center.
31  */
32 public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
isSelecting(Spannable buffer)33     private static boolean isSelecting(Spannable buffer) {
34         return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) ||
35                 (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
36     }
37 
getCurrentLineTop(Spannable buffer, Layout layout)38     private static int getCurrentLineTop(Spannable buffer, Layout layout) {
39         return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
40     }
41 
getPageHeight(TextView widget)42     private static int getPageHeight(TextView widget) {
43         // This calculation does not take into account the view transformations that
44         // may have been applied to the child or its containers.  In case of scaling or
45         // rotation, the calculated page height may be incorrect.
46         final Rect rect = new Rect();
47         return widget.getGlobalVisibleRect(rect) ? rect.height() : 0;
48     }
49 
50     @Override
handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event)51     protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
52             int movementMetaState, KeyEvent event) {
53         switch (keyCode) {
54             case KeyEvent.KEYCODE_DPAD_CENTER:
55                 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
56                     if (event.getAction() == KeyEvent.ACTION_DOWN
57                             && event.getRepeatCount() == 0
58                             && MetaKeyKeyListener.getMetaState(buffer,
59                                         MetaKeyKeyListener.META_SELECTING, event) != 0) {
60                         return widget.showContextMenu();
61                     }
62                 }
63                 break;
64         }
65         return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
66     }
67 
68     @Override
left(TextView widget, Spannable buffer)69     protected boolean left(TextView widget, Spannable buffer) {
70         final Layout layout = widget.getLayout();
71         if (isSelecting(buffer)) {
72             return Selection.extendLeft(buffer, layout);
73         } else {
74             return Selection.moveLeft(buffer, layout);
75         }
76     }
77 
78     @Override
right(TextView widget, Spannable buffer)79     protected boolean right(TextView widget, Spannable buffer) {
80         final Layout layout = widget.getLayout();
81         if (isSelecting(buffer)) {
82             return Selection.extendRight(buffer, layout);
83         } else {
84             return Selection.moveRight(buffer, layout);
85         }
86     }
87 
88     @Override
up(TextView widget, Spannable buffer)89     protected boolean up(TextView widget, Spannable buffer) {
90         final Layout layout = widget.getLayout();
91         if (isSelecting(buffer)) {
92             return Selection.extendUp(buffer, layout);
93         } else {
94             return Selection.moveUp(buffer, layout);
95         }
96     }
97 
98     @Override
down(TextView widget, Spannable buffer)99     protected boolean down(TextView widget, Spannable buffer) {
100         final Layout layout = widget.getLayout();
101         if (isSelecting(buffer)) {
102             return Selection.extendDown(buffer, layout);
103         } else {
104             return Selection.moveDown(buffer, layout);
105         }
106     }
107 
108     @Override
pageUp(TextView widget, Spannable buffer)109     protected boolean pageUp(TextView widget, Spannable buffer) {
110         final Layout layout = widget.getLayout();
111         final boolean selecting = isSelecting(buffer);
112         final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget);
113         boolean handled = false;
114         for (;;) {
115             final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
116             if (selecting) {
117                 Selection.extendUp(buffer, layout);
118             } else {
119                 Selection.moveUp(buffer, layout);
120             }
121             if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
122                 break;
123             }
124             handled = true;
125             if (getCurrentLineTop(buffer, layout) <= targetY) {
126                 break;
127             }
128         }
129         return handled;
130     }
131 
132     @Override
pageDown(TextView widget, Spannable buffer)133     protected boolean pageDown(TextView widget, Spannable buffer) {
134         final Layout layout = widget.getLayout();
135         final boolean selecting = isSelecting(buffer);
136         final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget);
137         boolean handled = false;
138         for (;;) {
139             final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
140             if (selecting) {
141                 Selection.extendDown(buffer, layout);
142             } else {
143                 Selection.moveDown(buffer, layout);
144             }
145             if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
146                 break;
147             }
148             handled = true;
149             if (getCurrentLineTop(buffer, layout) >= targetY) {
150                 break;
151             }
152         }
153         return handled;
154     }
155 
156     @Override
top(TextView widget, Spannable buffer)157     protected boolean top(TextView widget, Spannable buffer) {
158         if (isSelecting(buffer)) {
159             Selection.extendSelection(buffer, 0);
160         } else {
161             Selection.setSelection(buffer, 0);
162         }
163         return true;
164     }
165 
166     @Override
bottom(TextView widget, Spannable buffer)167     protected boolean bottom(TextView widget, Spannable buffer) {
168         if (isSelecting(buffer)) {
169             Selection.extendSelection(buffer, buffer.length());
170         } else {
171             Selection.setSelection(buffer, buffer.length());
172         }
173         return true;
174     }
175 
176     @Override
lineStart(TextView widget, Spannable buffer)177     protected boolean lineStart(TextView widget, Spannable buffer) {
178         final Layout layout = widget.getLayout();
179         if (isSelecting(buffer)) {
180             return Selection.extendToLeftEdge(buffer, layout);
181         } else {
182             return Selection.moveToLeftEdge(buffer, layout);
183         }
184     }
185 
186     @Override
lineEnd(TextView widget, Spannable buffer)187     protected boolean lineEnd(TextView widget, Spannable buffer) {
188         final Layout layout = widget.getLayout();
189         if (isSelecting(buffer)) {
190             return Selection.extendToRightEdge(buffer, layout);
191         } else {
192             return Selection.moveToRightEdge(buffer, layout);
193         }
194     }
195 
196     /** {@hide} */
197     @Override
leftWord(TextView widget, Spannable buffer)198     protected boolean leftWord(TextView widget, Spannable buffer) {
199         final int selectionEnd = widget.getSelectionEnd();
200         final WordIterator wordIterator = widget.getWordIterator();
201         wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
202         return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer));
203     }
204 
205     /** {@hide} */
206     @Override
rightWord(TextView widget, Spannable buffer)207     protected boolean rightWord(TextView widget, Spannable buffer) {
208         final int selectionEnd = widget.getSelectionEnd();
209         final WordIterator wordIterator = widget.getWordIterator();
210         wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
211         return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer));
212     }
213 
214     @Override
home(TextView widget, Spannable buffer)215     protected boolean home(TextView widget, Spannable buffer) {
216         return lineStart(widget, buffer);
217     }
218 
219     @Override
end(TextView widget, Spannable buffer)220     protected boolean end(TextView widget, Spannable buffer) {
221         return lineEnd(widget, buffer);
222     }
223 
224     @Override
onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)225     public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
226         int initialScrollX = -1;
227         int initialScrollY = -1;
228         final int action = event.getAction();
229 
230         if (action == MotionEvent.ACTION_UP) {
231             initialScrollX = Touch.getInitialScrollX(widget, buffer);
232             initialScrollY = Touch.getInitialScrollY(widget, buffer);
233         }
234 
235         boolean wasTouchSelecting = isSelecting(buffer);
236         boolean handled = Touch.onTouchEvent(widget, buffer, event);
237 
238         if (widget.didTouchFocusSelect()) {
239             return handled;
240         }
241         if (action == MotionEvent.ACTION_DOWN) {
242             // For touch events, the code should run only when selection is active.
243             if (isSelecting(buffer)) {
244                 if (!widget.isFocused()) {
245                     if (!widget.requestFocus()) {
246                         return handled;
247                     }
248                 }
249                 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
250                 buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
251                 // Disallow intercepting of the touch events, so that
252                 // users can scroll and select at the same time.
253                 // without this, users would get booted out of select
254                 // mode once the view detected it needed to scroll.
255                 widget.getParent().requestDisallowInterceptTouchEvent(true);
256             }
257         } else if (widget.isFocused()) {
258             if (action == MotionEvent.ACTION_MOVE) {
259                 if (isSelecting(buffer) && handled) {
260                     final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
261                     // Before selecting, make sure we've moved out of the "slop".
262                     // handled will be true, if we're in select mode AND we're
263                     // OUT of the slop
264 
265                     // Turn long press off while we're selecting. User needs to
266                     // re-tap on the selection to enable long press
267                     widget.cancelLongPress();
268 
269                     // Update selection as we're moving the selection area.
270 
271                     // Get the current touch position
272                     final int offset = widget.getOffsetForPosition(event.getX(), event.getY());
273                     Selection.setSelection(buffer, Math.min(startOffset, offset),
274                             Math.max(startOffset, offset));
275                     return true;
276                 }
277             } else if (action == MotionEvent.ACTION_UP) {
278                 // If we have scrolled, then the up shouldn't move the cursor,
279                 // but we do need to make sure the cursor is still visible at
280                 // the current scroll offset to avoid the scroll jumping later
281                 // to show it.
282                 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
283                     (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
284                     widget.moveCursorToVisibleOffset();
285                     return true;
286                 }
287 
288                 if (wasTouchSelecting) {
289                     final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
290                     final int endOffset = widget.getOffsetForPosition(event.getX(), event.getY());
291                     Selection.setSelection(buffer, Math.min(startOffset, endOffset),
292                             Math.max(startOffset, endOffset));
293                     buffer.removeSpan(LAST_TAP_DOWN);
294                 }
295 
296                 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
297                 MetaKeyKeyListener.resetLockedMeta(buffer);
298 
299                 return true;
300             }
301         }
302         return handled;
303     }
304 
305     @Override
canSelectArbitrarily()306     public boolean canSelectArbitrarily() {
307         return true;
308     }
309 
310     @Override
initialize(TextView widget, Spannable text)311     public void initialize(TextView widget, Spannable text) {
312         Selection.setSelection(text, 0);
313     }
314 
315     @Override
onTakeFocus(TextView view, Spannable text, int dir)316     public void onTakeFocus(TextView view, Spannable text, int dir) {
317         if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
318             if (view.getLayout() == null) {
319                 // This shouldn't be null, but do something sensible if it is.
320                 Selection.setSelection(text, text.length());
321             }
322         } else {
323             Selection.setSelection(text, text.length());
324         }
325     }
326 
getInstance()327     public static MovementMethod getInstance() {
328         if (sInstance == null) {
329             sInstance = new ArrowKeyMovementMethod();
330         }
331 
332         return sInstance;
333     }
334 
335     private static final Object LAST_TAP_DOWN = new Object();
336     private static ArrowKeyMovementMethod sInstance;
337 }
338