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.KeyEvent;
24import android.view.MotionEvent;
25import android.view.View;
26import 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 */
32public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
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
38    private static int getCurrentLineTop(Spannable buffer, Layout layout) {
39        return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
40    }
41
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
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
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
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
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
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
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
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
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
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
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
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
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
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
215    protected boolean home(TextView widget, Spannable buffer) {
216        return lineStart(widget, buffer);
217    }
218
219    @Override
220    protected boolean end(TextView widget, Spannable buffer) {
221        return lineEnd(widget, buffer);
222    }
223
224    @Override
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
306    public boolean canSelectArbitrarily() {
307        return true;
308    }
309
310    @Override
311    public void initialize(TextView widget, Spannable text) {
312        Selection.setSelection(text, 0);
313    }
314
315    @Override
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
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