ArrowKeyMovementMethod.java revision a6e50454890629fe369538d8e473945bc9d68136
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.util.Log;
20import android.view.KeyEvent;
21import android.graphics.Rect;
22import android.text.*;
23import android.widget.TextView;
24import android.view.View;
25import android.view.ViewConfiguration;
26import android.view.MotionEvent;
27
28// XXX this doesn't extend MetaKeyKeyListener because the signatures
29// don't match.  Need to figure that out.  Meanwhile the meta keys
30// won't work in fields that don't take input.
31
32public class
33ArrowKeyMovementMethod
34implements MovementMethod
35{
36    private boolean up(TextView widget, Spannable buffer) {
37        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
38                        KeyEvent.META_SHIFT_ON) == 1) ||
39                      (MetaKeyKeyListener.getMetaState(buffer,
40                        MetaKeyKeyListener.META_SELECTING) != 0);
41        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
42                        KeyEvent.META_ALT_ON) == 1;
43        Layout layout = widget.getLayout();
44
45        if (cap) {
46            if (alt) {
47                Selection.extendSelection(buffer, 0);
48                return true;
49            } else {
50                return Selection.extendUp(buffer, layout);
51            }
52        } else {
53            if (alt) {
54                Selection.setSelection(buffer, 0);
55                return true;
56            } else {
57                return Selection.moveUp(buffer, layout);
58            }
59        }
60    }
61
62    private boolean down(TextView widget, Spannable buffer) {
63        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
64                        KeyEvent.META_SHIFT_ON) == 1) ||
65                      (MetaKeyKeyListener.getMetaState(buffer,
66                        MetaKeyKeyListener.META_SELECTING) != 0);
67        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
68                        KeyEvent.META_ALT_ON) == 1;
69        Layout layout = widget.getLayout();
70
71        if (cap) {
72            if (alt) {
73                Selection.extendSelection(buffer, buffer.length());
74                return true;
75            } else {
76                return Selection.extendDown(buffer, layout);
77            }
78        } else {
79            if (alt) {
80                Selection.setSelection(buffer, buffer.length());
81                return true;
82            } else {
83                return Selection.moveDown(buffer, layout);
84            }
85        }
86    }
87
88    private boolean left(TextView widget, Spannable buffer) {
89        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
90                        KeyEvent.META_SHIFT_ON) == 1) ||
91                      (MetaKeyKeyListener.getMetaState(buffer,
92                        MetaKeyKeyListener.META_SELECTING) != 0);
93        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
94                        KeyEvent.META_ALT_ON) == 1;
95        Layout layout = widget.getLayout();
96
97        if (cap) {
98            if (alt) {
99                return Selection.extendToLeftEdge(buffer, layout);
100            } else {
101                return Selection.extendLeft(buffer, layout);
102            }
103        } else {
104            if (alt) {
105                return Selection.moveToLeftEdge(buffer, layout);
106            } else {
107                return Selection.moveLeft(buffer, layout);
108            }
109        }
110    }
111
112    private boolean right(TextView widget, Spannable buffer) {
113        boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
114                        KeyEvent.META_SHIFT_ON) == 1) ||
115                      (MetaKeyKeyListener.getMetaState(buffer,
116                        MetaKeyKeyListener.META_SELECTING) != 0);
117        boolean alt = MetaKeyKeyListener.getMetaState(buffer,
118                        KeyEvent.META_ALT_ON) == 1;
119        Layout layout = widget.getLayout();
120
121        if (cap) {
122            if (alt) {
123                return Selection.extendToRightEdge(buffer, layout);
124            } else {
125                return Selection.extendRight(buffer, layout);
126            }
127        } else {
128            if (alt) {
129                return Selection.moveToRightEdge(buffer, layout);
130            } else {
131                return Selection.moveRight(buffer, layout);
132            }
133        }
134    }
135
136    public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
137        if (executeDown(widget, buffer, keyCode)) {
138            MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
139            MetaKeyKeyListener.resetLockedMeta(buffer);
140            return true;
141        }
142
143        return false;
144    }
145
146    private boolean executeDown(TextView widget, Spannable buffer, int keyCode) {
147        boolean handled = false;
148
149        switch (keyCode) {
150        case KeyEvent.KEYCODE_DPAD_UP:
151            handled |= up(widget, buffer);
152            break;
153
154        case KeyEvent.KEYCODE_DPAD_DOWN:
155            handled |= down(widget, buffer);
156            break;
157
158        case KeyEvent.KEYCODE_DPAD_LEFT:
159            handled |= left(widget, buffer);
160            break;
161
162        case KeyEvent.KEYCODE_DPAD_RIGHT:
163            handled |= right(widget, buffer);
164            break;
165
166        case KeyEvent.KEYCODE_DPAD_CENTER:
167            if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
168                if (widget.showContextMenu()) {
169                    handled = true;
170                }
171            }
172        }
173
174        if (handled) {
175            MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
176            MetaKeyKeyListener.resetLockedMeta(buffer);
177        }
178
179        return handled;
180    }
181
182    public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
183        return false;
184    }
185
186    public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) {
187        int code = event.getKeyCode();
188        if (code != KeyEvent.KEYCODE_UNKNOWN
189                && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
190            int repeat = event.getRepeatCount();
191            boolean handled = false;
192            while ((--repeat) > 0) {
193                handled |= executeDown(view, text, code);
194            }
195            return handled;
196        }
197        return false;
198    }
199
200    public boolean onTrackballEvent(TextView widget, Spannable text,
201            MotionEvent event) {
202        return false;
203    }
204
205    public boolean onTouchEvent(TextView widget, Spannable buffer,
206                                MotionEvent event) {
207        int initialScrollX = -1, initialScrollY = -1;
208        if (event.getAction() == MotionEvent.ACTION_UP) {
209            initialScrollX = Touch.getInitialScrollX(widget, buffer);
210            initialScrollY = Touch.getInitialScrollY(widget, buffer);
211        }
212
213        boolean handled = Touch.onTouchEvent(widget, buffer, event);
214
215        if (widget.isFocused() && !widget.didTouchFocusSelect()) {
216            if (event.getAction() == MotionEvent.ACTION_UP) {
217                // If we have scrolled, then the up shouldn't move the cursor,
218                // but we do need to make sure the cursor is still visible at
219                // the current scroll offset to avoid the scroll jumping later
220                // to show it.
221                if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
222                        (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
223                    widget.moveCursorToVisibleOffset();
224                    return true;
225                }
226
227                int x = (int) event.getX();
228                int y = (int) event.getY();
229
230                x -= widget.getTotalPaddingLeft();
231                y -= widget.getTotalPaddingTop();
232
233                // Clamp the position to inside of the view.
234                if (x < 0) {
235                    x = 0;
236                } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) {
237                    x = widget.getWidth()-widget.getTotalPaddingRight() - 1;
238                }
239                if (y < 0) {
240                    y = 0;
241                } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) {
242                    y = widget.getHeight()-widget.getTotalPaddingBottom() - 1;
243                }
244
245                x += widget.getScrollX();
246                y += widget.getScrollY();
247
248                Layout layout = widget.getLayout();
249                int line = layout.getLineForVertical(y);
250
251                int off = layout.getOffsetForHorizontal(line, x);
252
253                // XXX should do the same adjust for x as we do for the line.
254
255                boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
256                                KeyEvent.META_SHIFT_ON) == 1) ||
257                              (MetaKeyKeyListener.getMetaState(buffer,
258                                MetaKeyKeyListener.META_SELECTING) != 0);
259
260                DoubleTapState[] tap = buffer.getSpans(0, buffer.length(),
261                                                       DoubleTapState.class);
262                boolean doubletap = false;
263
264                if (tap.length > 0) {
265                    if (event.getEventTime() - tap[0].mWhen <=
266                        ViewConfiguration.getDoubleTapTimeout()) {
267                        if (sameWord(buffer, off, Selection.getSelectionEnd(buffer))) {
268                            doubletap = true;
269                        }
270                    }
271
272                    tap[0].mWhen = event.getEventTime();
273                } else {
274                    DoubleTapState newtap = new DoubleTapState();
275                    newtap.mWhen = event.getEventTime();
276                    buffer.setSpan(newtap, 0, buffer.length(),
277                                   Spannable.SPAN_INCLUSIVE_INCLUSIVE);
278                }
279
280                if (cap) {
281                    Selection.extendSelection(buffer, off);
282                } else if (doubletap) {
283                    Selection.setSelection(buffer,
284                                           findWordStart(buffer, off),
285                                           findWordEnd(buffer, off));
286                } else {
287                    Selection.setSelection(buffer, off);
288                }
289
290                MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
291                MetaKeyKeyListener.resetLockedMeta(buffer);
292
293                return true;
294            }
295        }
296
297        return handled;
298    }
299
300    private static class DoubleTapState implements NoCopySpan {
301        long mWhen;
302    }
303
304    private static boolean sameWord(CharSequence text, int one, int two) {
305        int start = findWordStart(text, one);
306        int end = findWordEnd(text, one);
307
308        if (end == start) {
309            return false;
310        }
311
312        return start == findWordStart(text, two) &&
313               end == findWordEnd(text, two);
314    }
315
316    // TODO: Unify with TextView.getWordForDictionary()
317    private static int findWordStart(CharSequence text, int start) {
318        for (; start > 0; start--) {
319            char c = text.charAt(start - 1);
320            int type = Character.getType(c);
321
322            if (c != '\'' &&
323                type != Character.UPPERCASE_LETTER &&
324                type != Character.LOWERCASE_LETTER &&
325                type != Character.TITLECASE_LETTER &&
326                type != Character.MODIFIER_LETTER &&
327                type != Character.DECIMAL_DIGIT_NUMBER) {
328                break;
329            }
330        }
331
332        return start;
333    }
334
335    // TODO: Unify with TextView.getWordForDictionary()
336    private static int findWordEnd(CharSequence text, int end) {
337        int len = text.length();
338
339        for (; end < len; end++) {
340            char c = text.charAt(end);
341            int type = Character.getType(c);
342
343            if (c != '\'' &&
344                type != Character.UPPERCASE_LETTER &&
345                type != Character.LOWERCASE_LETTER &&
346                type != Character.TITLECASE_LETTER &&
347                type != Character.MODIFIER_LETTER &&
348                type != Character.DECIMAL_DIGIT_NUMBER) {
349                break;
350            }
351        }
352
353        return end;
354    }
355
356    public boolean canSelectArbitrarily() {
357        return true;
358    }
359
360    public void initialize(TextView widget, Spannable text) {
361        Selection.setSelection(text, 0);
362    }
363
364    public void onTakeFocus(TextView view, Spannable text, int dir) {
365        if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
366            Layout layout = view.getLayout();
367
368            if (layout == null) {
369                /*
370                 * This shouldn't be null, but do something sensible if it is.
371                 */
372                Selection.setSelection(text, text.length());
373            } else {
374                /*
375                 * Put the cursor at the end of the first line, which is
376                 * either the last offset if there is only one line, or the
377                 * offset before the first character of the second line
378                 * if there is more than one line.
379                 */
380                if (layout.getLineCount() == 1) {
381                    Selection.setSelection(text, text.length());
382                } else {
383                    Selection.setSelection(text, layout.getLineStart(1) - 1);
384                }
385            }
386        } else {
387            Selection.setSelection(text, text.length());
388        }
389    }
390
391    public static MovementMethod getInstance() {
392        if (sInstance == null)
393            sInstance = new ArrowKeyMovementMethod();
394
395        return sInstance;
396    }
397
398    private static ArrowKeyMovementMethod sInstance;
399}
400