1/*
2 * Copyright (C) 2010 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.text.Layout;
20import android.text.Spannable;
21import android.view.InputDevice;
22import android.view.KeyEvent;
23import android.view.MotionEvent;
24import android.widget.TextView;
25
26/**
27 * Base classes for movement methods.
28 */
29public class BaseMovementMethod implements MovementMethod {
30    @Override
31    public boolean canSelectArbitrarily() {
32        return false;
33    }
34
35    @Override
36    public void initialize(TextView widget, Spannable text) {
37    }
38
39    @Override
40    public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) {
41        final int movementMetaState = getMovementMetaState(text, event);
42        boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event);
43        if (handled) {
44            MetaKeyKeyListener.adjustMetaAfterKeypress(text);
45            MetaKeyKeyListener.resetLockedMeta(text);
46        }
47        return handled;
48    }
49
50    @Override
51    public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) {
52        final int movementMetaState = getMovementMetaState(text, event);
53        final int keyCode = event.getKeyCode();
54        if (keyCode != KeyEvent.KEYCODE_UNKNOWN
55                && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
56            final int repeat = event.getRepeatCount();
57            boolean handled = false;
58            for (int i = 0; i < repeat; i++) {
59                if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) {
60                    break;
61                }
62                handled = true;
63            }
64            if (handled) {
65                MetaKeyKeyListener.adjustMetaAfterKeypress(text);
66                MetaKeyKeyListener.resetLockedMeta(text);
67            }
68            return handled;
69        }
70        return false;
71    }
72
73    @Override
74    public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
75        return false;
76    }
77
78    @Override
79    public void onTakeFocus(TextView widget, Spannable text, int direction) {
80    }
81
82    @Override
83    public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) {
84        return false;
85    }
86
87    @Override
88    public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
89        return false;
90    }
91
92    @Override
93    public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) {
94        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
95            switch (event.getAction()) {
96                case MotionEvent.ACTION_SCROLL: {
97                    final float vscroll;
98                    final float hscroll;
99                    if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
100                        vscroll = 0;
101                        hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
102                    } else {
103                        vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
104                        hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
105                    }
106
107                    boolean handled = false;
108                    if (hscroll < 0) {
109                        handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll));
110                    } else if (hscroll > 0) {
111                        handled |= scrollRight(widget, text, (int)Math.ceil(hscroll));
112                    }
113                    if (vscroll < 0) {
114                        handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll));
115                    } else if (vscroll > 0) {
116                        handled |= scrollDown(widget, text, (int)Math.ceil(vscroll));
117                    }
118                    return handled;
119                }
120            }
121        }
122        return false;
123    }
124
125    /**
126     * Gets the meta state used for movement using the modifiers tracked by the text
127     * buffer as well as those present in the key event.
128     *
129     * The movement meta state excludes the state of locked modifiers or the SHIFT key
130     * since they are not used by movement actions (but they may be used for selection).
131     *
132     * @param buffer The text buffer.
133     * @param event The key event.
134     * @return The keyboard meta states used for movement.
135     */
136    protected int getMovementMetaState(Spannable buffer, KeyEvent event) {
137        // We ignore locked modifiers and SHIFT.
138        int metaState = MetaKeyKeyListener.getMetaState(buffer, event)
139                & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED);
140        return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK;
141    }
142
143    /**
144     * Performs a movement key action.
145     * The default implementation decodes the key down and invokes movement actions
146     * such as {@link #down} and {@link #up}.
147     * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once
148     * to handle an {@link KeyEvent#ACTION_DOWN}.
149     * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly
150     * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}.
151     *
152     * @param widget The text view.
153     * @param buffer The text buffer.
154     * @param event The key event.
155     * @param keyCode The key code.
156     * @param movementMetaState The keyboard meta states used for movement.
157     * @param event The key event.
158     * @return True if the event was handled.
159     */
160    protected boolean handleMovementKey(TextView widget, Spannable buffer,
161            int keyCode, int movementMetaState, KeyEvent event) {
162        switch (keyCode) {
163            case KeyEvent.KEYCODE_DPAD_LEFT:
164                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
165                    return left(widget, buffer);
166                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
167                        KeyEvent.META_CTRL_ON)) {
168                    return leftWord(widget, buffer);
169                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
170                        KeyEvent.META_ALT_ON)) {
171                    return lineStart(widget, buffer);
172                }
173                break;
174
175            case KeyEvent.KEYCODE_DPAD_RIGHT:
176                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
177                    return right(widget, buffer);
178                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
179                        KeyEvent.META_CTRL_ON)) {
180                    return rightWord(widget, buffer);
181                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
182                        KeyEvent.META_ALT_ON)) {
183                    return lineEnd(widget, buffer);
184                }
185                break;
186
187            case KeyEvent.KEYCODE_DPAD_UP:
188                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
189                    return up(widget, buffer);
190                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
191                        KeyEvent.META_ALT_ON)) {
192                    return top(widget, buffer);
193                }
194                break;
195
196            case KeyEvent.KEYCODE_DPAD_DOWN:
197                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
198                    return down(widget, buffer);
199                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
200                        KeyEvent.META_ALT_ON)) {
201                    return bottom(widget, buffer);
202                }
203                break;
204
205            case KeyEvent.KEYCODE_PAGE_UP:
206                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
207                    return pageUp(widget, buffer);
208                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
209                        KeyEvent.META_ALT_ON)) {
210                    return top(widget, buffer);
211                }
212                break;
213
214            case KeyEvent.KEYCODE_PAGE_DOWN:
215                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
216                    return pageDown(widget, buffer);
217                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
218                        KeyEvent.META_ALT_ON)) {
219                    return bottom(widget, buffer);
220                }
221                break;
222
223            case KeyEvent.KEYCODE_MOVE_HOME:
224                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
225                    return home(widget, buffer);
226                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
227                        KeyEvent.META_CTRL_ON)) {
228                    return top(widget, buffer);
229                }
230                break;
231
232            case KeyEvent.KEYCODE_MOVE_END:
233                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
234                    return end(widget, buffer);
235                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
236                        KeyEvent.META_CTRL_ON)) {
237                    return bottom(widget, buffer);
238                }
239                break;
240        }
241        return false;
242    }
243
244    /**
245     * Performs a left movement action.
246     * Moves the cursor or scrolls left by one character.
247     *
248     * @param widget The text view.
249     * @param buffer The text buffer.
250     * @return True if the event was handled.
251     */
252    protected boolean left(TextView widget, Spannable buffer) {
253        return false;
254    }
255
256    /**
257     * Performs a right movement action.
258     * Moves the cursor or scrolls right by one character.
259     *
260     * @param widget The text view.
261     * @param buffer The text buffer.
262     * @return True if the event was handled.
263     */
264    protected boolean right(TextView widget, Spannable buffer) {
265        return false;
266    }
267
268    /**
269     * Performs an up movement action.
270     * Moves the cursor or scrolls up by one line.
271     *
272     * @param widget The text view.
273     * @param buffer The text buffer.
274     * @return True if the event was handled.
275     */
276    protected boolean up(TextView widget, Spannable buffer) {
277        return false;
278    }
279
280    /**
281     * Performs a down movement action.
282     * Moves the cursor or scrolls down by one line.
283     *
284     * @param widget The text view.
285     * @param buffer The text buffer.
286     * @return True if the event was handled.
287     */
288    protected boolean down(TextView widget, Spannable buffer) {
289        return false;
290    }
291
292    /**
293     * Performs a page-up movement action.
294     * Moves the cursor or scrolls up by one page.
295     *
296     * @param widget The text view.
297     * @param buffer The text buffer.
298     * @return True if the event was handled.
299     */
300    protected boolean pageUp(TextView widget, Spannable buffer) {
301        return false;
302    }
303
304    /**
305     * Performs a page-down movement action.
306     * Moves the cursor or scrolls down by one page.
307     *
308     * @param widget The text view.
309     * @param buffer The text buffer.
310     * @return True if the event was handled.
311     */
312    protected boolean pageDown(TextView widget, Spannable buffer) {
313        return false;
314    }
315
316    /**
317     * Performs a top movement action.
318     * Moves the cursor or scrolls to the top of the buffer.
319     *
320     * @param widget The text view.
321     * @param buffer The text buffer.
322     * @return True if the event was handled.
323     */
324    protected boolean top(TextView widget, Spannable buffer) {
325        return false;
326    }
327
328    /**
329     * Performs a bottom movement action.
330     * Moves the cursor or scrolls to the bottom of the buffer.
331     *
332     * @param widget The text view.
333     * @param buffer The text buffer.
334     * @return True if the event was handled.
335     */
336    protected boolean bottom(TextView widget, Spannable buffer) {
337        return false;
338    }
339
340    /**
341     * Performs a line-start movement action.
342     * Moves the cursor or scrolls to the start of the line.
343     *
344     * @param widget The text view.
345     * @param buffer The text buffer.
346     * @return True if the event was handled.
347     */
348    protected boolean lineStart(TextView widget, Spannable buffer) {
349        return false;
350    }
351
352    /**
353     * Performs a line-end movement action.
354     * Moves the cursor or scrolls to the end of the line.
355     *
356     * @param widget The text view.
357     * @param buffer The text buffer.
358     * @return True if the event was handled.
359     */
360    protected boolean lineEnd(TextView widget, Spannable buffer) {
361        return false;
362    }
363
364    /** {@hide} */
365    protected boolean leftWord(TextView widget, Spannable buffer) {
366        return false;
367    }
368
369    /** {@hide} */
370    protected boolean rightWord(TextView widget, Spannable buffer) {
371        return false;
372    }
373
374    /**
375     * Performs a home movement action.
376     * Moves the cursor or scrolls to the start of the line or to the top of the
377     * document depending on whether the insertion point is being moved or
378     * the document is being scrolled.
379     *
380     * @param widget The text view.
381     * @param buffer The text buffer.
382     * @return True if the event was handled.
383     */
384    protected boolean home(TextView widget, Spannable buffer) {
385        return false;
386    }
387
388    /**
389     * Performs an end movement action.
390     * Moves the cursor or scrolls to the start of the line or to the top of the
391     * document depending on whether the insertion point is being moved or
392     * the document is being scrolled.
393     *
394     * @param widget The text view.
395     * @param buffer The text buffer.
396     * @return True if the event was handled.
397     */
398    protected boolean end(TextView widget, Spannable buffer) {
399        return false;
400    }
401
402    private int getTopLine(TextView widget) {
403        return widget.getLayout().getLineForVertical(widget.getScrollY());
404    }
405
406    private int getBottomLine(TextView widget) {
407        return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget));
408    }
409
410    private int getInnerWidth(TextView widget) {
411        return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight();
412    }
413
414    private int getInnerHeight(TextView widget) {
415        return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom();
416    }
417
418    private int getCharacterWidth(TextView widget) {
419        return (int) Math.ceil(widget.getPaint().getFontSpacing());
420    }
421
422    private int getScrollBoundsLeft(TextView widget) {
423        final Layout layout = widget.getLayout();
424        final int topLine = getTopLine(widget);
425        final int bottomLine = getBottomLine(widget);
426        if (topLine > bottomLine) {
427            return 0;
428        }
429        int left = Integer.MAX_VALUE;
430        for (int line = topLine; line <= bottomLine; line++) {
431            final int lineLeft = (int) Math.floor(layout.getLineLeft(line));
432            if (lineLeft < left) {
433                left = lineLeft;
434            }
435        }
436        return left;
437    }
438
439    private int getScrollBoundsRight(TextView widget) {
440        final Layout layout = widget.getLayout();
441        final int topLine = getTopLine(widget);
442        final int bottomLine = getBottomLine(widget);
443        if (topLine > bottomLine) {
444            return 0;
445        }
446        int right = Integer.MIN_VALUE;
447        for (int line = topLine; line <= bottomLine; line++) {
448            final int lineRight = (int) Math.ceil(layout.getLineRight(line));
449            if (lineRight > right) {
450                right = lineRight;
451            }
452        }
453        return right;
454    }
455
456    /**
457     * Performs a scroll left action.
458     * Scrolls left by the specified number of characters.
459     *
460     * @param widget The text view.
461     * @param buffer The text buffer.
462     * @param amount The number of characters to scroll by.  Must be at least 1.
463     * @return True if the event was handled.
464     * @hide
465     */
466    protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) {
467        final int minScrollX = getScrollBoundsLeft(widget);
468        int scrollX = widget.getScrollX();
469        if (scrollX > minScrollX) {
470            scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX);
471            widget.scrollTo(scrollX, widget.getScrollY());
472            return true;
473        }
474        return false;
475    }
476
477    /**
478     * Performs a scroll right action.
479     * Scrolls right by the specified number of characters.
480     *
481     * @param widget The text view.
482     * @param buffer The text buffer.
483     * @param amount The number of characters to scroll by.  Must be at least 1.
484     * @return True if the event was handled.
485     * @hide
486     */
487    protected boolean scrollRight(TextView widget, Spannable buffer, int amount) {
488        final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
489        int scrollX = widget.getScrollX();
490        if (scrollX < maxScrollX) {
491            scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX);
492            widget.scrollTo(scrollX, widget.getScrollY());
493            return true;
494        }
495        return false;
496    }
497
498    /**
499     * Performs a scroll up action.
500     * Scrolls up by the specified number of lines.
501     *
502     * @param widget The text view.
503     * @param buffer The text buffer.
504     * @param amount The number of lines to scroll by.  Must be at least 1.
505     * @return True if the event was handled.
506     * @hide
507     */
508    protected boolean scrollUp(TextView widget, Spannable buffer, int amount) {
509        final Layout layout = widget.getLayout();
510        final int top = widget.getScrollY();
511        int topLine = layout.getLineForVertical(top);
512        if (layout.getLineTop(topLine) == top) {
513            // If the top line is partially visible, bring it all the way
514            // into view; otherwise, bring the previous line into view.
515            topLine -= 1;
516        }
517        if (topLine >= 0) {
518            topLine = Math.max(topLine - amount + 1, 0);
519            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
520            return true;
521        }
522        return false;
523    }
524
525    /**
526     * Performs a scroll down action.
527     * Scrolls down by the specified number of lines.
528     *
529     * @param widget The text view.
530     * @param buffer The text buffer.
531     * @param amount The number of lines to scroll by.  Must be at least 1.
532     * @return True if the event was handled.
533     * @hide
534     */
535    protected boolean scrollDown(TextView widget, Spannable buffer, int amount) {
536        final Layout layout = widget.getLayout();
537        final int innerHeight = getInnerHeight(widget);
538        final int bottom = widget.getScrollY() + innerHeight;
539        int bottomLine = layout.getLineForVertical(bottom);
540        if (layout.getLineTop(bottomLine + 1) < bottom + 1) {
541            // Less than a pixel of this line is out of view,
542            // so we must have tried to make it entirely in view
543            // and now want the next line to be in view instead.
544            bottomLine += 1;
545        }
546        final int limit = layout.getLineCount() - 1;
547        if (bottomLine <= limit) {
548            bottomLine = Math.min(bottomLine + amount - 1, limit);
549            Touch.scrollTo(widget, layout, widget.getScrollX(),
550                    layout.getLineTop(bottomLine + 1) - innerHeight);
551            return true;
552        }
553        return false;
554    }
555
556    /**
557     * Performs a scroll page up action.
558     * Scrolls up by one page.
559     *
560     * @param widget The text view.
561     * @param buffer The text buffer.
562     * @return True if the event was handled.
563     * @hide
564     */
565    protected boolean scrollPageUp(TextView widget, Spannable buffer) {
566        final Layout layout = widget.getLayout();
567        final int top = widget.getScrollY() - getInnerHeight(widget);
568        int topLine = layout.getLineForVertical(top);
569        if (topLine >= 0) {
570            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
571            return true;
572        }
573        return false;
574    }
575
576    /**
577     * Performs a scroll page up action.
578     * Scrolls down by one page.
579     *
580     * @param widget The text view.
581     * @param buffer The text buffer.
582     * @return True if the event was handled.
583     * @hide
584     */
585    protected boolean scrollPageDown(TextView widget, Spannable buffer) {
586        final Layout layout = widget.getLayout();
587        final int innerHeight = getInnerHeight(widget);
588        final int bottom = widget.getScrollY() + innerHeight + innerHeight;
589        int bottomLine = layout.getLineForVertical(bottom);
590        if (bottomLine <= layout.getLineCount() - 1) {
591            Touch.scrollTo(widget, layout, widget.getScrollX(),
592                    layout.getLineTop(bottomLine + 1) - innerHeight);
593            return true;
594        }
595        return false;
596    }
597
598    /**
599     * Performs a scroll to top action.
600     * Scrolls to the top of the document.
601     *
602     * @param widget The text view.
603     * @param buffer The text buffer.
604     * @return True if the event was handled.
605     * @hide
606     */
607    protected boolean scrollTop(TextView widget, Spannable buffer) {
608        final Layout layout = widget.getLayout();
609        if (getTopLine(widget) >= 0) {
610            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0));
611            return true;
612        }
613        return false;
614    }
615
616    /**
617     * Performs a scroll to bottom action.
618     * Scrolls to the bottom of the document.
619     *
620     * @param widget The text view.
621     * @param buffer The text buffer.
622     * @return True if the event was handled.
623     * @hide
624     */
625    protected boolean scrollBottom(TextView widget, Spannable buffer) {
626        final Layout layout = widget.getLayout();
627        final int lineCount = layout.getLineCount();
628        if (getBottomLine(widget) <= lineCount - 1) {
629            Touch.scrollTo(widget, layout, widget.getScrollX(),
630                    layout.getLineTop(lineCount) - getInnerHeight(widget));
631            return true;
632        }
633        return false;
634    }
635
636    /**
637     * Performs a scroll to line start action.
638     * Scrolls to the start of the line.
639     *
640     * @param widget The text view.
641     * @param buffer The text buffer.
642     * @return True if the event was handled.
643     * @hide
644     */
645    protected boolean scrollLineStart(TextView widget, Spannable buffer) {
646        final int minScrollX = getScrollBoundsLeft(widget);
647        int scrollX = widget.getScrollX();
648        if (scrollX > minScrollX) {
649            widget.scrollTo(minScrollX, widget.getScrollY());
650            return true;
651        }
652        return false;
653    }
654
655    /**
656     * Performs a scroll to line end action.
657     * Scrolls to the end of the line.
658     *
659     * @param widget The text view.
660     * @param buffer The text buffer.
661     * @return True if the event was handled.
662     * @hide
663     */
664    protected boolean scrollLineEnd(TextView widget, Spannable buffer) {
665        final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
666        int scrollX = widget.getScrollX();
667        if (scrollX < maxScrollX) {
668            widget.scrollTo(maxScrollX, widget.getScrollY());
669            return true;
670        }
671        return false;
672    }
673}
674