BaseKeyListener.java revision bba8d97c369f02a9d1988217324724a24842079f
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.Paint;
20import android.icu.lang.UCharacter;
21import android.icu.lang.UProperty;
22import android.view.KeyEvent;
23import android.view.View;
24import android.text.*;
25import android.text.method.TextKeyListener.Capitalize;
26import android.text.style.ReplacementSpan;
27import android.widget.TextView;
28
29import com.android.internal.annotations.GuardedBy;
30
31import java.text.BreakIterator;
32import java.util.Arrays;
33import java.util.Collections;
34import java.util.HashSet;
35
36/**
37 * Abstract base class for key listeners.
38 *
39 * Provides a basic foundation for entering and editing text.
40 * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert
41 * characters as keys are pressed.
42 * <p></p>
43 * As for all implementations of {@link KeyListener}, this class is only concerned
44 * with hardware keyboards.  Software input methods have no obligation to trigger
45 * the methods in this class.
46 */
47public abstract class BaseKeyListener extends MetaKeyKeyListener
48        implements KeyListener {
49    /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();
50
51    private static final int LINE_FEED = 0x0A;
52    private static final int CARRIAGE_RETURN = 0x0D;
53
54    private final Object mLock = new Object();
55
56    @GuardedBy("mLock")
57    static Paint sCachedPaint = null;
58
59    /**
60     * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
61     * a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
62     * deletes the character before the cursor, if any; ALT+DEL deletes everything on
63     * the line the cursor is on.
64     *
65     * @return true if anything was deleted; false otherwise.
66     */
67    public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
68        return backspaceOrForwardDelete(view, content, keyCode, event, false);
69    }
70
71    /**
72     * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL}
73     * key in a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
74     * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on
75     * the line the cursor is on.
76     *
77     * @return true if anything was deleted; false otherwise.
78     */
79    public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) {
80        return backspaceOrForwardDelete(view, content, keyCode, event, true);
81    }
82
83    // Returns true if the given code point is a variation selector.
84    private static boolean isVariationSelector(int codepoint) {
85        return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR);
86    }
87
88    // Returns the offset of the replacement span edge if the offset is inside of the replacement
89    // span.  Otherwise, does nothing and returns the input offset value.
90    private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) {
91        if (!(text instanceof Spanned)) {
92            return offset;
93        }
94
95        ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class);
96        for (int i = 0; i < spans.length; i++) {
97            final int start = ((Spanned) text).getSpanStart(spans[i]);
98            final int end = ((Spanned) text).getSpanEnd(spans[i]);
99
100            if (start < offset && end > offset) {
101                offset = moveToStart ? start : end;
102            }
103        }
104        return offset;
105    }
106
107    // Returns the start offset to be deleted by a backspace key from the given offset.
108    private static int getOffsetForBackspaceKey(CharSequence text, int offset) {
109        if (offset <= 1) {
110            return 0;
111        }
112
113        // Initial state
114        final int STATE_START = 0;
115
116        // The offset is immediately before line feed.
117        final int STATE_LF = 1;
118
119        // The offset is immediately before a KEYCAP.
120        final int STATE_BEFORE_KEYCAP = 2;
121        // The offset is immediately before a variation selector and a KEYCAP.
122        final int STATE_BEFORE_VS_AND_KEYCAP = 3;
123
124        // The offset is immediately before an emoji modifier.
125        final int STATE_BEFORE_EMOJI_MODIFIER = 4;
126        // The offset is immediately before a variation selector and an emoji modifier.
127        final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5;
128
129        // The offset is immediately before a variation selector.
130        final int STATE_BEFORE_VS = 6;
131
132        // The offset is immediately before an emoji.
133        final int STATE_BEFORE_EMOJI = 7;
134        // The offset is immediately before a ZWJ that were seen before a ZWJ emoji.
135        final int STATE_BEFORE_ZWJ = 8;
136        // The offset is immediately before a variation selector and a ZWJ that were seen before a
137        // ZWJ emoji.
138        final int STATE_BEFORE_VS_AND_ZWJ = 9;
139
140        // The number of following RIS code points is odd.
141        final int STATE_ODD_NUMBERED_RIS = 10;
142        // The number of following RIS code points is even.
143        final int STATE_EVEN_NUMBERED_RIS = 11;
144
145        // The state machine has been stopped.
146        final int STATE_FINISHED = 12;
147
148        int deleteCharCount = 0;  // Char count to be deleted by backspace.
149        int lastSeenVSCharCount = 0;  // Char count of previous variation selector.
150
151        int state = STATE_START;
152
153        int tmpOffset = offset;
154        do {
155            final int codePoint = Character.codePointBefore(text, tmpOffset);
156            tmpOffset -= Character.charCount(codePoint);
157
158            switch (state) {
159                case STATE_START:
160                    deleteCharCount = Character.charCount(codePoint);
161                    if (codePoint == LINE_FEED) {
162                        state = STATE_LF;
163                    } else if (isVariationSelector(codePoint)) {
164                        state = STATE_BEFORE_VS;
165                    } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
166                        state = STATE_ODD_NUMBERED_RIS;
167                    } else if (Emoji.isEmojiModifier(codePoint)) {
168                        state = STATE_BEFORE_EMOJI_MODIFIER;
169                    } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) {
170                        state = STATE_BEFORE_KEYCAP;
171                    } else if (Emoji.isEmoji(codePoint)) {
172                        state = STATE_BEFORE_EMOJI;
173                    } else {
174                        state = STATE_FINISHED;
175                    }
176                    break;
177                case STATE_LF:
178                    if (codePoint == CARRIAGE_RETURN) {
179                        ++deleteCharCount;
180                    }
181                    state = STATE_FINISHED;
182                    break;
183                case STATE_ODD_NUMBERED_RIS:
184                    if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
185                        deleteCharCount += 2; /* Char count of RIS */
186                        state = STATE_EVEN_NUMBERED_RIS;
187                    } else {
188                        state = STATE_FINISHED;
189                    }
190                    break;
191                case STATE_EVEN_NUMBERED_RIS:
192                    if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
193                        deleteCharCount -= 2; /* Char count of RIS */
194                        state = STATE_ODD_NUMBERED_RIS;
195                    } else {
196                        state = STATE_FINISHED;
197                    }
198                    break;
199                case STATE_BEFORE_KEYCAP:
200                    if (isVariationSelector(codePoint)) {
201                        lastSeenVSCharCount = Character.charCount(codePoint);
202                        state = STATE_BEFORE_VS_AND_KEYCAP;
203                        break;
204                    }
205
206                    if (Emoji.isKeycapBase(codePoint)) {
207                        deleteCharCount += Character.charCount(codePoint);
208                    }
209                    state = STATE_FINISHED;
210                    break;
211                case STATE_BEFORE_VS_AND_KEYCAP:
212                    if (Emoji.isKeycapBase(codePoint)) {
213                        deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
214                    }
215                    state = STATE_FINISHED;
216                    break;
217                case STATE_BEFORE_EMOJI_MODIFIER:
218                    if (isVariationSelector(codePoint)) {
219                        lastSeenVSCharCount = Character.charCount(codePoint);
220                        state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER;
221                        break;
222                    } else if (Emoji.isEmojiModifierBase(codePoint)) {
223                        deleteCharCount += Character.charCount(codePoint);
224                    }
225                    state = STATE_FINISHED;
226                    break;
227                case STATE_BEFORE_VS_AND_EMOJI_MODIFIER:
228                    if (Emoji.isEmojiModifierBase(codePoint)) {
229                        deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
230                    }
231                    state = STATE_FINISHED;
232                    break;
233                case STATE_BEFORE_VS:
234                    if (Emoji.isEmoji(codePoint)) {
235                        deleteCharCount += Character.charCount(codePoint);
236                        state = STATE_BEFORE_EMOJI;
237                        break;
238                    }
239
240                    if (!isVariationSelector(codePoint) &&
241                            UCharacter.getCombiningClass(codePoint) == 0) {
242                        deleteCharCount += Character.charCount(codePoint);
243                    }
244                    state = STATE_FINISHED;
245                    break;
246                case STATE_BEFORE_EMOJI:
247                    if (codePoint == Emoji.ZERO_WIDTH_JOINER) {
248                        state = STATE_BEFORE_ZWJ;
249                    } else {
250                        state = STATE_FINISHED;
251                    }
252                    break;
253                case STATE_BEFORE_ZWJ:
254                    if (Emoji.isEmoji(codePoint)) {
255                        deleteCharCount += Character.charCount(codePoint) + 1;  // +1 for ZWJ.
256                        state = Emoji.isEmojiModifier(codePoint) ?
257                                STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI;
258                    } else if (isVariationSelector(codePoint)) {
259                        lastSeenVSCharCount = Character.charCount(codePoint);
260                        state = STATE_BEFORE_VS_AND_ZWJ;
261                    } else {
262                        state = STATE_FINISHED;
263                    }
264                    break;
265                case STATE_BEFORE_VS_AND_ZWJ:
266                    if (Emoji.isEmoji(codePoint)) {
267                        // +1 for ZWJ.
268                        deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
269                        lastSeenVSCharCount = 0;
270                        state = STATE_BEFORE_EMOJI;
271                    } else {
272                        state = STATE_FINISHED;
273                    }
274                    break;
275                default:
276                    throw new IllegalArgumentException("state " + state + " is unknown");
277            }
278        } while (tmpOffset > 0 && state != STATE_FINISHED);
279
280        return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
281    }
282
283    // Returns the end offset to be deleted by a forward delete key from the given offset.
284    private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
285        final int len = text.length();
286
287        if (offset >= len - 1) {
288            return len;
289        }
290
291        offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
292                offset, Paint.CURSOR_AFTER);
293
294        return adjustReplacementSpan(text, offset, false /* move to the end */);
295    }
296
297    private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
298            KeyEvent event, boolean isForwardDelete) {
299        // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
300        if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
301                & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
302            return false;
303        }
304
305        // If there is a current selection, delete it.
306        if (deleteSelection(view, content)) {
307            return true;
308        }
309
310        // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
311        boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
312        boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
313        boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
314
315        if (isCtrlActive) {
316            if (isAltActive || isShiftActive) {
317                // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
318                return false;
319            }
320            return deleteUntilWordBoundary(view, content, isForwardDelete);
321        }
322
323        // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
324        if (isAltActive && deleteLine(view, content)) {
325            return true;
326        }
327
328        // Delete a character.
329        final int start = Selection.getSelectionEnd(content);
330        final int end;
331        if (isForwardDelete) {
332            final Paint paint;
333            if (view instanceof TextView) {
334                paint = ((TextView)view).getPaint();
335            } else {
336                synchronized (mLock) {
337                    if (sCachedPaint == null) {
338                        sCachedPaint = new Paint();
339                    }
340                    paint = sCachedPaint;
341                }
342            }
343            end = getOffsetForForwardDeleteKey(content, start, paint);
344        } else {
345            end = getOffsetForBackspaceKey(content, start);
346        }
347        if (start != end) {
348            content.delete(Math.min(start, end), Math.max(start, end));
349            return true;
350        }
351        return false;
352    }
353
354    private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
355        int currentCursorOffset = Selection.getSelectionStart(content);
356
357        // If there is a selection, do nothing.
358        if (currentCursorOffset != Selection.getSelectionEnd(content)) {
359            return false;
360        }
361
362        // Early exit if there is no contents to delete.
363        if ((!isForwardDelete && currentCursorOffset == 0) ||
364            (isForwardDelete && currentCursorOffset == content.length())) {
365            return false;
366        }
367
368        WordIterator wordIterator = null;
369        if (view instanceof TextView) {
370            wordIterator = ((TextView)view).getWordIterator();
371        }
372
373        if (wordIterator == null) {
374            // Default locale is used for WordIterator since the appropriate locale is not clear
375            // here.
376            // TODO: Use appropriate locale for WordIterator.
377            wordIterator = new WordIterator();
378        }
379
380        int deleteFrom;
381        int deleteTo;
382
383        if (isForwardDelete) {
384            deleteFrom = currentCursorOffset;
385            wordIterator.setCharSequence(content, deleteFrom, content.length());
386            deleteTo = wordIterator.following(currentCursorOffset);
387            if (deleteTo == BreakIterator.DONE) {
388                deleteTo = content.length();
389            }
390        } else {
391            deleteTo = currentCursorOffset;
392            wordIterator.setCharSequence(content, 0, deleteTo);
393            deleteFrom = wordIterator.preceding(currentCursorOffset);
394            if (deleteFrom == BreakIterator.DONE) {
395                deleteFrom = 0;
396            }
397        }
398        content.delete(deleteFrom, deleteTo);
399        return true;
400    }
401
402    private boolean deleteSelection(View view, Editable content) {
403        int selectionStart = Selection.getSelectionStart(content);
404        int selectionEnd = Selection.getSelectionEnd(content);
405        if (selectionEnd < selectionStart) {
406            int temp = selectionEnd;
407            selectionEnd = selectionStart;
408            selectionStart = temp;
409        }
410        if (selectionStart != selectionEnd) {
411            content.delete(selectionStart, selectionEnd);
412            return true;
413        }
414        return false;
415    }
416
417    private boolean deleteLine(View view, Editable content) {
418        if (view instanceof TextView) {
419            final Layout layout = ((TextView) view).getLayout();
420            if (layout != null) {
421                final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
422                final int start = layout.getLineStart(line);
423                final int end = layout.getLineEnd(line);
424                if (end != start) {
425                    content.delete(start, end);
426                    return true;
427                }
428            }
429        }
430        return false;
431    }
432
433    static int makeTextContentType(Capitalize caps, boolean autoText) {
434        int contentType = InputType.TYPE_CLASS_TEXT;
435        switch (caps) {
436            case CHARACTERS:
437                contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
438                break;
439            case WORDS:
440                contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
441                break;
442            case SENTENCES:
443                contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
444                break;
445        }
446        if (autoText) {
447            contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
448        }
449        return contentType;
450    }
451
452    public boolean onKeyDown(View view, Editable content,
453                             int keyCode, KeyEvent event) {
454        boolean handled;
455        switch (keyCode) {
456            case KeyEvent.KEYCODE_DEL:
457                handled = backspace(view, content, keyCode, event);
458                break;
459            case KeyEvent.KEYCODE_FORWARD_DEL:
460                handled = forwardDelete(view, content, keyCode, event);
461                break;
462            default:
463                handled = false;
464                break;
465        }
466
467        if (handled) {
468            adjustMetaAfterKeypress(content);
469            return true;
470        }
471
472        return super.onKeyDown(view, content, keyCode, event);
473    }
474
475    /**
476     * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
477     * the event's text into the content.
478     */
479    public boolean onKeyOther(View view, Editable content, KeyEvent event) {
480        if (event.getAction() != KeyEvent.ACTION_MULTIPLE
481                || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
482            // Not something we are interested in.
483            return false;
484        }
485
486        int selectionStart = Selection.getSelectionStart(content);
487        int selectionEnd = Selection.getSelectionEnd(content);
488        if (selectionEnd < selectionStart) {
489            int temp = selectionEnd;
490            selectionEnd = selectionStart;
491            selectionStart = temp;
492        }
493
494        CharSequence text = event.getCharacters();
495        if (text == null) {
496            return false;
497        }
498
499        content.replace(selectionStart, selectionEnd, text);
500        return true;
501    }
502}
503