ArrowKeyMovementMethod.java revision 8cdb684163051c12f37e8a5f9031f17efd9d0fa4
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    private int getOffset(int x, int y, TextView widget){
137      // Converts the absolute X,Y coordinates to the character offset for the
138      // character whose position is closest to the specified
139      // horizontal position.
140      x -= widget.getTotalPaddingLeft();
141      y -= widget.getTotalPaddingTop();
142
143      // Clamp the position to inside of the view.
144      if (x < 0) {
145          x = 0;
146      } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) {
147          x = widget.getWidth()-widget.getTotalPaddingRight() - 1;
148      }
149      if (y < 0) {
150          y = 0;
151      } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) {
152          y = widget.getHeight()-widget.getTotalPaddingBottom() - 1;
153      }
154
155      x += widget.getScrollX();
156      y += widget.getScrollY();
157
158      Layout layout = widget.getLayout();
159      int line = layout.getLineForVertical(y);
160
161      int offset = layout.getOffsetForHorizontal(line, x);
162      return offset;
163    }
164
165    public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
166        if (executeDown(widget, buffer, keyCode)) {
167            MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
168            MetaKeyKeyListener.resetLockedMeta(buffer);
169            return true;
170        }
171
172        return false;
173    }
174
175    private boolean executeDown(TextView widget, Spannable buffer, int keyCode) {
176        boolean handled = false;
177
178        switch (keyCode) {
179        case KeyEvent.KEYCODE_DPAD_UP:
180            handled |= up(widget, buffer);
181            break;
182
183        case KeyEvent.KEYCODE_DPAD_DOWN:
184            handled |= down(widget, buffer);
185            break;
186
187        case KeyEvent.KEYCODE_DPAD_LEFT:
188            handled |= left(widget, buffer);
189            break;
190
191        case KeyEvent.KEYCODE_DPAD_RIGHT:
192            handled |= right(widget, buffer);
193            break;
194
195        case KeyEvent.KEYCODE_DPAD_CENTER:
196            if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
197                if (widget.showContextMenu()) {
198                    handled = true;
199                }
200            }
201        }
202
203        if (handled) {
204            MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
205            MetaKeyKeyListener.resetLockedMeta(buffer);
206        }
207
208        return handled;
209    }
210
211    public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
212        return false;
213    }
214
215    public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) {
216        int code = event.getKeyCode();
217        if (code != KeyEvent.KEYCODE_UNKNOWN
218                && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
219            int repeat = event.getRepeatCount();
220            boolean handled = false;
221            while ((--repeat) > 0) {
222                handled |= executeDown(view, text, code);
223            }
224            return handled;
225        }
226        return false;
227    }
228
229    public boolean onTrackballEvent(TextView widget, Spannable text,
230            MotionEvent event) {
231        return false;
232    }
233
234    public boolean onTouchEvent(TextView widget, Spannable buffer,
235                                MotionEvent event) {
236        int initialScrollX = -1, initialScrollY = -1;
237        if (event.getAction() == MotionEvent.ACTION_UP) {
238            initialScrollX = Touch.getInitialScrollX(widget, buffer);
239            initialScrollY = Touch.getInitialScrollY(widget, buffer);
240        }
241
242        boolean handled = Touch.onTouchEvent(widget, buffer, event);
243
244        if (widget.isFocused() && !widget.didTouchFocusSelect()) {
245            if (event.getAction() == MotionEvent.ACTION_DOWN) {
246              boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
247                              KeyEvent.META_SHIFT_ON) == 1) ||
248                            (MetaKeyKeyListener.getMetaState(buffer,
249                              MetaKeyKeyListener.META_SELECTING) != 0);
250              int x = (int) event.getX();
251              int y = (int) event.getY();
252              int offset = getOffset(x, y, widget);
253
254              if (cap) {
255                  buffer.setSpan(LAST_TAP_DOWN, offset, offset,
256                                 Spannable.SPAN_POINT_POINT);
257
258                  // Disallow intercepting of the touch events, so that
259                  // users can scroll and select at the same time.
260                  // without this, users would get booted out of select
261                  // mode once the view detected it needed to scroll.
262                  widget.getParent().requestDisallowInterceptTouchEvent(true);
263              } else {
264                  OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(),
265                      OnePointFiveTapState.class);
266
267                  if (tap.length > 0) {
268                      if (event.getEventTime() - tap[0].mWhen <=
269                          ViewConfiguration.getDoubleTapTimeout() &&
270                          sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) {
271
272                          tap[0].active = true;
273                          MetaKeyKeyListener.startSelecting(widget, buffer);
274                          widget.getParent().requestDisallowInterceptTouchEvent(true);
275                          buffer.setSpan(LAST_TAP_DOWN, offset, offset,
276                              Spannable.SPAN_POINT_POINT);
277                      }
278
279                      tap[0].mWhen = event.getEventTime();
280                  } else {
281                      OnePointFiveTapState newtap = new OnePointFiveTapState();
282                      newtap.mWhen = event.getEventTime();
283                      newtap.active = false;
284                      buffer.setSpan(newtap, 0, buffer.length(),
285                          Spannable.SPAN_INCLUSIVE_INCLUSIVE);
286                  }
287              }
288            } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
289                boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
290                                KeyEvent.META_SHIFT_ON) == 1) ||
291                              (MetaKeyKeyListener.getMetaState(buffer,
292                                MetaKeyKeyListener.META_SELECTING) != 0);
293
294                if (cap && handled) {
295                    // Before selecting, make sure we've moved out of the "slop".
296                    // handled will be true, if we're in select mode AND we're
297                    // OUT of the slop
298
299                    // Turn long press off while we're selecting. User needs to
300                    // re-tap on the selection to enable longpress
301                    widget.cancelLongPress();
302
303                    // Update selection as we're moving the selection area.
304
305                    // Get the current touch position
306                    int x = (int) event.getX();
307                    int y = (int) event.getY();
308                    int offset = getOffset(x, y, widget);
309
310                    final OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(),
311                            OnePointFiveTapState.class);
312
313                    if (tap.length > 0 && tap[0].active) {
314                        // Get the last down touch position (the position at which the
315                        // user started the selection)
316                        int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN);
317
318                        // Compute the selection boundaries
319                        int spanstart;
320                        int spanend;
321                        if (offset >= lastDownOffset) {
322                            // Expand from word start of the original tap to new word
323                            // end, since we are selecting "forwards"
324                            spanstart = findWordStart(buffer, lastDownOffset);
325                            spanend = findWordEnd(buffer, offset);
326                        } else {
327                            // Expand to from new word start to word end of the original
328                            // tap since we are selecting "backwards".
329                            // The spanend will always need to be associated with the touch
330                            // up position, so that refining the selection with the
331                            // trackball will work as expected.
332                            spanstart = findWordEnd(buffer, lastDownOffset);
333                            spanend = findWordStart(buffer, offset);
334                        }
335                        Selection.setSelection(buffer, spanstart, spanend);
336                    } else {
337                        Selection.extendSelection(buffer, offset);
338                    }
339                    return true;
340                }
341            } else if (event.getAction() == MotionEvent.ACTION_UP) {
342                // If we have scrolled, then the up shouldn't move the cursor,
343                // but we do need to make sure the cursor is still visible at
344                // the current scroll offset to avoid the scroll jumping later
345                // to show it.
346                if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
347                        (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
348                    widget.moveCursorToVisibleOffset();
349                    return true;
350                }
351
352                int x = (int) event.getX();
353                int y = (int) event.getY();
354                int off = getOffset(x, y, widget);
355
356                // XXX should do the same adjust for x as we do for the line.
357
358                OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(),
359                    OnePointFiveTapState.class);
360                if (onepointfivetap.length > 0 && onepointfivetap[0].active &&
361                    Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) {
362                    // If we've set select mode, because there was a onepointfivetap,
363                    // but there was no ensuing swipe gesture, undo the select mode
364                    // and remove reference to the last onepointfivetap.
365                    MetaKeyKeyListener.stopSelecting(widget, buffer);
366                    for (int i=0; i < onepointfivetap.length; i++) {
367                        buffer.removeSpan(onepointfivetap[i]);
368                    }
369                    buffer.removeSpan(LAST_TAP_DOWN);
370                }
371                boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
372                                KeyEvent.META_SHIFT_ON) == 1) ||
373                              (MetaKeyKeyListener.getMetaState(buffer,
374                                MetaKeyKeyListener.META_SELECTING) != 0);
375
376                DoubleTapState[] tap = buffer.getSpans(0, buffer.length(),
377                                                       DoubleTapState.class);
378                boolean doubletap = false;
379
380                if (tap.length > 0) {
381                    if (event.getEventTime() - tap[0].mWhen <=
382                        ViewConfiguration.getDoubleTapTimeout() &&
383                        sameWord(buffer, off, Selection.getSelectionEnd(buffer))) {
384
385                        doubletap = true;
386                    }
387
388                    tap[0].mWhen = event.getEventTime();
389                } else {
390                    DoubleTapState newtap = new DoubleTapState();
391                    newtap.mWhen = event.getEventTime();
392                    buffer.setSpan(newtap, 0, buffer.length(),
393                                   Spannable.SPAN_INCLUSIVE_INCLUSIVE);
394                }
395
396                if (cap) {
397                    buffer.removeSpan(LAST_TAP_DOWN);
398                    if (onepointfivetap.length > 0 && onepointfivetap[0].active) {
399                        // If we selecting something with the onepointfivetap-and
400                        // swipe gesture, stop it on finger up.
401                        MetaKeyKeyListener.stopSelecting(widget, buffer);
402                    } else {
403                        Selection.extendSelection(buffer, off);
404                    }
405                } else if (doubletap) {
406                    Selection.setSelection(buffer,
407                                           findWordStart(buffer, off),
408                                           findWordEnd(buffer, off));
409                } else {
410                    Selection.setSelection(buffer, off);
411                }
412
413                MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
414                MetaKeyKeyListener.resetLockedMeta(buffer);
415
416                return true;
417            }
418        }
419
420        return handled;
421    }
422
423    private static class DoubleTapState implements NoCopySpan {
424        long mWhen;
425    }
426
427    /* We check for a onepointfive tap. This is similar to
428    *  doubletap gesture (where a finger goes down, up, down, up, in a short
429    *  time period), except in the onepointfive tap, a users finger only needs
430    *  to go down, up, down in a short time period. We detect this type of tap
431    *  to implement the onepointfivetap-and-swipe selection gesture.
432    *  This gesture allows users to select a segment of text without going
433    *  through the "select text" option in the context menu.
434    */
435    private static class OnePointFiveTapState implements NoCopySpan {
436        long mWhen;
437        boolean active;
438    }
439
440    private static boolean sameWord(CharSequence text, int one, int two) {
441        int start = findWordStart(text, one);
442        int end = findWordEnd(text, one);
443
444        if (end == start) {
445            return false;
446        }
447
448        return start == findWordStart(text, two) &&
449               end == findWordEnd(text, two);
450    }
451
452    // TODO: Unify with TextView.getWordForDictionary()
453    private static int findWordStart(CharSequence text, int start) {
454        for (; start > 0; start--) {
455            char c = text.charAt(start - 1);
456            int type = Character.getType(c);
457
458            if (c != '\'' &&
459                type != Character.UPPERCASE_LETTER &&
460                type != Character.LOWERCASE_LETTER &&
461                type != Character.TITLECASE_LETTER &&
462                type != Character.MODIFIER_LETTER &&
463                type != Character.DECIMAL_DIGIT_NUMBER) {
464                break;
465            }
466        }
467
468        return start;
469    }
470
471    // TODO: Unify with TextView.getWordForDictionary()
472    private static int findWordEnd(CharSequence text, int end) {
473        int len = text.length();
474
475        for (; end < len; end++) {
476            char c = text.charAt(end);
477            int type = Character.getType(c);
478
479            if (c != '\'' &&
480                type != Character.UPPERCASE_LETTER &&
481                type != Character.LOWERCASE_LETTER &&
482                type != Character.TITLECASE_LETTER &&
483                type != Character.MODIFIER_LETTER &&
484                type != Character.DECIMAL_DIGIT_NUMBER) {
485                break;
486            }
487        }
488
489        return end;
490    }
491
492    public boolean canSelectArbitrarily() {
493        return true;
494    }
495
496    public void initialize(TextView widget, Spannable text) {
497        Selection.setSelection(text, 0);
498    }
499
500    public void onTakeFocus(TextView view, Spannable text, int dir) {
501        if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
502            Layout layout = view.getLayout();
503
504            if (layout == null) {
505                /*
506                 * This shouldn't be null, but do something sensible if it is.
507                 */
508                Selection.setSelection(text, text.length());
509            } else {
510                /*
511                 * Put the cursor at the end of the first line, which is
512                 * either the last offset if there is only one line, or the
513                 * offset before the first character of the second line
514                 * if there is more than one line.
515                 */
516                if (layout.getLineCount() == 1) {
517                    Selection.setSelection(text, text.length());
518                } else {
519                    Selection.setSelection(text, layout.getLineStart(1) - 1);
520                }
521            }
522        } else {
523            Selection.setSelection(text, text.length());
524        }
525    }
526
527    public static MovementMethod getInstance() {
528        if (sInstance == null)
529            sInstance = new ArrowKeyMovementMethod();
530
531        return sInstance;
532    }
533
534
535    private static final Object LAST_TAP_DOWN = new Object();
536    private static ArrowKeyMovementMethod sInstance;
537}
538