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
17package android.text.method;
18
19import android.graphics.Rect;
20import android.text.Layout;
21import android.text.Selection;
22import android.text.Spannable;
23import android.view.InputDevice;
24import android.view.KeyEvent;
25import android.view.MotionEvent;
26import android.view.View;
27import 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 */
33public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
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
39    private static int getCurrentLineTop(Spannable buffer, Layout layout) {
40        return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
41    }
42
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
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
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
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
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
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;
128            }
129        }
130        return handled;
131    }
132
133    @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);
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
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
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
197    /** {@hide} */
198    @Override
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
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
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
225    private static boolean isTouchSelecting(boolean isMouse, Spannable buffer) {
226        return isMouse ? Touch.isActivelySelecting(buffer) : isSelecting(buffer);
227    }
228
229    @Override
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.isFocused() && !widget.didTouchFocusSelect()) {
244            if (action == MotionEvent.ACTION_DOWN) {
245                // 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                }
258            } else if (action == MotionEvent.ACTION_MOVE) {
259
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) {
269                    // 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
272
273                    // Turn long press off while we're selecting. User needs to
274                    // re-tap on the selection to enable long press
275                    widget.cancelLongPress();
276
277                    // Update selection as we're moving the selection area.
278
279                    // Get the current touch position
280                    int offset = widget.getOffsetForPosition(event.getX(), event.getY());
281
282                    Selection.extendSelection(buffer, offset);
283                    return true;
284                }
285            } else if (action == MotionEvent.ACTION_UP) {
286                // 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()) ||
291                    (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
292                    widget.moveCursorToVisibleOffset();
293                    return true;
294                }
295
296                int offset = widget.getOffsetForPosition(event.getX(), event.getY());
297                if (isTouchSelecting(isMouse, buffer)) {
298                    buffer.removeSpan(LAST_TAP_DOWN);
299                    Selection.extendSelection(buffer, offset);
300                }
301
302                MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
303                MetaKeyKeyListener.resetLockedMeta(buffer);
304
305                return true;
306            }
307        }
308        return handled;
309    }
310
311    @Override
312    public boolean canSelectArbitrarily() {
313        return true;
314    }
315
316    @Override
317    public void initialize(TextView widget, Spannable text) {
318        Selection.setSelection(text, 0);
319    }
320
321    @Override
322    public void onTakeFocus(TextView view, Spannable text, int dir) {
323        if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
324            if (view.getLayout() == null) {
325                // This shouldn't be null, but do something sensible if it is.
326                Selection.setSelection(text, text.length());
327            }
328        } else {
329            Selection.setSelection(text, text.length());
330        }
331    }
332
333    public static MovementMethod getInstance() {
334        if (sInstance == null) {
335            sInstance = new ArrowKeyMovementMethod();
336        }
337
338        return sInstance;
339    }
340
341    private static final Object LAST_TAP_DOWN = new Object();
342    private static ArrowKeyMovementMethod sInstance;
343}
344