QwertyKeyListener.java revision 14d0ca1473b991288b2dfab57409054dec7cd2fa
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.text.*;
20import android.text.method.TextKeyListener.Capitalize;
21import android.util.SparseArray;
22import android.view.KeyCharacterMap;
23import android.view.KeyEvent;
24import android.view.View;
25
26/**
27 * This is the standard key listener for alphabetic input on qwerty
28 * keyboards.  You should generally not need to instantiate this yourself;
29 * TextKeyListener will do it for you.
30 */
31public class QwertyKeyListener extends BaseKeyListener {
32    private static QwertyKeyListener[] sInstance =
33        new QwertyKeyListener[Capitalize.values().length * 2];
34    private static QwertyKeyListener sFullKeyboardInstance;
35
36    private Capitalize mAutoCap;
37    private boolean mAutoText;
38    private boolean mFullKeyboard;
39
40    private QwertyKeyListener(Capitalize cap, boolean autoText, boolean fullKeyboard) {
41        mAutoCap = cap;
42        mAutoText = autoText;
43        mFullKeyboard = fullKeyboard;
44    }
45
46    public QwertyKeyListener(Capitalize cap, boolean autoText) {
47        this(cap, autoText, false);
48    }
49
50    /**
51     * Returns a new or existing instance with the specified capitalization
52     * and correction properties.
53     */
54    public static QwertyKeyListener getInstance(boolean autoText, Capitalize cap) {
55        int off = cap.ordinal() * 2 + (autoText ? 1 : 0);
56
57        if (sInstance[off] == null) {
58            sInstance[off] = new QwertyKeyListener(cap, autoText);
59        }
60
61        return sInstance[off];
62    }
63
64    /**
65     * Gets an instance of the listener suitable for use with full keyboards.
66     * Disables auto-capitalization, auto-text and long-press initiated on-screen
67     * character pickers.
68     */
69    public static QwertyKeyListener getInstanceForFullKeyboard() {
70        if (sFullKeyboardInstance == null) {
71            sFullKeyboardInstance = new QwertyKeyListener(Capitalize.NONE, false, true);
72        }
73        return sFullKeyboardInstance;
74    }
75
76    public int getInputType() {
77        return makeTextContentType(mAutoCap, mAutoText);
78    }
79
80    public boolean onKeyDown(View view, Editable content,
81                             int keyCode, KeyEvent event) {
82        int selStart, selEnd;
83        int pref = 0;
84
85        if (view != null) {
86            pref = TextKeyListener.getInstance().getPrefs(view.getContext());
87        }
88
89        {
90            int a = Selection.getSelectionStart(content);
91            int b = Selection.getSelectionEnd(content);
92
93            selStart = Math.min(a, b);
94            selEnd = Math.max(a, b);
95
96            if (selStart < 0 || selEnd < 0) {
97                selStart = selEnd = 0;
98                Selection.setSelection(content, 0, 0);
99            }
100        }
101
102        int activeStart = content.getSpanStart(TextKeyListener.ACTIVE);
103        int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE);
104
105        // QWERTY keyboard normal case
106
107        int i = event.getUnicodeChar(event.getMetaState() | getMetaState(content));
108
109        if (!mFullKeyboard) {
110            int count = event.getRepeatCount();
111            if (count > 0 && selStart == selEnd && selStart > 0) {
112                char c = content.charAt(selStart - 1);
113
114                if (c == i || c == Character.toUpperCase(i) && view != null) {
115                    if (showCharacterPicker(view, content, c, false, count)) {
116                        resetMetaState(content);
117                        return true;
118                    }
119                }
120            }
121        }
122
123        if (i == KeyCharacterMap.PICKER_DIALOG_INPUT) {
124            if (view != null) {
125                showCharacterPicker(view, content,
126                                    KeyCharacterMap.PICKER_DIALOG_INPUT, true, 1);
127            }
128            resetMetaState(content);
129            return true;
130        }
131
132        if (i == KeyCharacterMap.HEX_INPUT) {
133            int start;
134
135            if (selStart == selEnd) {
136                start = selEnd;
137
138                while (start > 0 && selEnd - start < 4 &&
139                       Character.digit(content.charAt(start - 1), 16) >= 0) {
140                    start--;
141                }
142            } else {
143                start = selStart;
144            }
145
146            int ch = -1;
147            try {
148                String hex = TextUtils.substring(content, start, selEnd);
149                ch = Integer.parseInt(hex, 16);
150            } catch (NumberFormatException nfe) { }
151
152            if (ch >= 0) {
153                selStart = start;
154                Selection.setSelection(content, selStart, selEnd);
155                i = ch;
156            } else {
157                i = 0;
158            }
159        }
160
161        if (i != 0) {
162            boolean dead = false;
163
164            if ((i & KeyCharacterMap.COMBINING_ACCENT) != 0) {
165                dead = true;
166                i = i & KeyCharacterMap.COMBINING_ACCENT_MASK;
167            }
168
169            if (activeStart == selStart && activeEnd == selEnd) {
170                boolean replace = false;
171
172                if (selEnd - selStart - 1 == 0) {
173                    char accent = content.charAt(selStart);
174                    int composed = event.getDeadChar(accent, i);
175
176                    if (composed != 0) {
177                        i = composed;
178                        replace = true;
179                    }
180                }
181
182                if (!replace) {
183                    Selection.setSelection(content, selEnd);
184                    content.removeSpan(TextKeyListener.ACTIVE);
185                    selStart = selEnd;
186                }
187            }
188
189            if ((pref & TextKeyListener.AUTO_CAP) != 0 &&
190                Character.isLowerCase(i) &&
191                TextKeyListener.shouldCap(mAutoCap, content, selStart)) {
192                int where = content.getSpanEnd(TextKeyListener.CAPPED);
193                int flags = content.getSpanFlags(TextKeyListener.CAPPED);
194
195                if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) {
196                    content.removeSpan(TextKeyListener.CAPPED);
197                } else {
198                    flags = i << 16;
199                    i = Character.toUpperCase(i);
200
201                    if (selStart == 0)
202                        content.setSpan(TextKeyListener.CAPPED, 0, 0,
203                                        Spannable.SPAN_MARK_MARK | flags);
204                    else
205                        content.setSpan(TextKeyListener.CAPPED,
206                                        selStart - 1, selStart,
207                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
208                                        flags);
209                }
210            }
211
212            if (selStart != selEnd) {
213                Selection.setSelection(content, selEnd);
214            }
215            content.setSpan(OLD_SEL_START, selStart, selStart,
216                            Spannable.SPAN_MARK_MARK);
217
218            content.replace(selStart, selEnd, String.valueOf((char) i));
219
220            int oldStart = content.getSpanStart(OLD_SEL_START);
221            selEnd = Selection.getSelectionEnd(content);
222
223            if (oldStart < selEnd) {
224                content.setSpan(TextKeyListener.LAST_TYPED,
225                                oldStart, selEnd,
226                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
227
228                if (dead) {
229                    Selection.setSelection(content, oldStart, selEnd);
230                    content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd,
231                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
232                }
233            }
234
235            adjustMetaAfterKeypress(content);
236
237            // potentially do autotext replacement if the character
238            // that was typed was an autotext terminator
239
240            if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText &&
241                (i == ' ' || i == '\t' || i == '\n' ||
242                 i == ',' || i == '.' || i == '!' || i == '?' ||
243                 i == '"' || Character.getType(i) == Character.END_PUNCTUATION) &&
244                 content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT)
245                     != oldStart) {
246                int x;
247
248                for (x = oldStart; x > 0; x--) {
249                    char c = content.charAt(x - 1);
250                    if (c != '\'' && !Character.isLetter(c)) {
251                        break;
252                    }
253                }
254
255                String rep = getReplacement(content, x, oldStart, view);
256
257                if (rep != null) {
258                    Replaced[] repl = content.getSpans(0, content.length(),
259                                                     Replaced.class);
260                    for (int a = 0; a < repl.length; a++)
261                        content.removeSpan(repl[a]);
262
263                    char[] orig = new char[oldStart - x];
264                    TextUtils.getChars(content, x, oldStart, orig, 0);
265
266                    content.setSpan(new Replaced(orig), x, oldStart,
267                                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
268                    content.replace(x, oldStart, rep);
269                }
270            }
271
272            // Replace two spaces by a period and a space.
273
274            if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) {
275                selEnd = Selection.getSelectionEnd(content);
276                if (selEnd - 3 >= 0) {
277                    if (content.charAt(selEnd - 1) == ' ' &&
278                        content.charAt(selEnd - 2) == ' ') {
279                        char c = content.charAt(selEnd - 3);
280
281                        for (int j = selEnd - 3; j > 0; j--) {
282                            if (c == '"' ||
283                                Character.getType(c) == Character.END_PUNCTUATION) {
284                                c = content.charAt(j - 1);
285                            } else {
286                                break;
287                            }
288                        }
289
290                        if (Character.isLetter(c) || Character.isDigit(c)) {
291                            content.replace(selEnd - 2, selEnd - 1, ".");
292                        }
293                    }
294                }
295            }
296
297            return true;
298        } else if (keyCode == KeyEvent.KEYCODE_DEL
299                && (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_ALT_ON))
300                && selStart == selEnd) {
301            // special backspace case for undoing autotext
302
303            int consider = 1;
304
305            // if backspacing over the last typed character,
306            // it undoes the autotext prior to that character
307            // (unless the character typed was newline, in which
308            // case this behavior would be confusing)
309
310            if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) {
311                if (content.charAt(selStart - 1) != '\n')
312                    consider = 2;
313            }
314
315            Replaced[] repl = content.getSpans(selStart - consider, selStart,
316                                             Replaced.class);
317
318            if (repl.length > 0) {
319                int st = content.getSpanStart(repl[0]);
320                int en = content.getSpanEnd(repl[0]);
321                String old = new String(repl[0].mText);
322
323                content.removeSpan(repl[0]);
324
325                // only cancel the autocomplete if the cursor is at the end of
326                // the replaced span (or after it, because the user is
327                // backspacing over the space after the word, not the word
328                // itself).
329                if (selStart >= en) {
330                    content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
331                                    en, en, Spannable.SPAN_POINT_POINT);
332                    content.replace(st, en, old);
333
334                    en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT);
335                    if (en - 1 >= 0) {
336                        content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
337                                        en - 1, en,
338                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
339                    } else {
340                        content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT);
341                    }
342                    adjustMetaAfterKeypress(content);
343                } else {
344                    adjustMetaAfterKeypress(content);
345                    return super.onKeyDown(view, content, keyCode, event);
346                }
347
348                return true;
349            }
350        }
351
352        return super.onKeyDown(view, content, keyCode, event);
353    }
354
355    private String getReplacement(CharSequence src, int start, int end,
356                                  View view) {
357        int len = end - start;
358        boolean changecase = false;
359
360        String replacement = AutoText.get(src, start, end, view);
361
362        if (replacement == null) {
363            String key = TextUtils.substring(src, start, end).toLowerCase();
364            replacement = AutoText.get(key, 0, end - start, view);
365            changecase = true;
366
367            if (replacement == null)
368                return null;
369        }
370
371        int caps = 0;
372
373        if (changecase) {
374            for (int j = start; j < end; j++) {
375                if (Character.isUpperCase(src.charAt(j)))
376                    caps++;
377            }
378        }
379
380        String out;
381
382        if (caps == 0)
383            out = replacement;
384        else if (caps == 1)
385            out = toTitleCase(replacement);
386        else if (caps == len)
387            out = replacement.toUpperCase();
388        else
389            out = toTitleCase(replacement);
390
391        if (out.length() == len &&
392            TextUtils.regionMatches(src, start, out, 0, len))
393            return null;
394
395        return out;
396    }
397
398    /**
399     * Marks the specified region of <code>content</code> as having
400     * contained <code>original</code> prior to AutoText replacement.
401     * Call this method when you have done or are about to do an
402     * AutoText-style replacement on a region of text and want to let
403     * the same mechanism (the user pressing DEL immediately after the
404     * change) undo the replacement.
405     *
406     * @param content the Editable text where the replacement was made
407     * @param start the start of the replaced region
408     * @param end the end of the replaced region; the location of the cursor
409     * @param original the text to be restored if the user presses DEL
410     */
411    public static void markAsReplaced(Spannable content, int start, int end,
412                                      String original) {
413        Replaced[] repl = content.getSpans(0, content.length(), Replaced.class);
414        for (int a = 0; a < repl.length; a++) {
415            content.removeSpan(repl[a]);
416        }
417
418        int len = original.length();
419        char[] orig = new char[len];
420        original.getChars(0, len, orig, 0);
421
422        content.setSpan(new Replaced(orig), start, end,
423                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
424    }
425
426    private static SparseArray<String> PICKER_SETS =
427                        new SparseArray<String>();
428    static {
429        PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5\u0104\u0100");
430        PICKER_SETS.put('C', "\u00C7\u0106\u010C");
431        PICKER_SETS.put('D', "\u010E");
432        PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB\u0118\u011A\u0112");
433        PICKER_SETS.put('G', "\u011E");
434        PICKER_SETS.put('L', "\u0141");
435        PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A\u0130");
436        PICKER_SETS.put('N', "\u00D1\u0143\u0147");
437        PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6\u014C");
438        PICKER_SETS.put('R', "\u0158");
439        PICKER_SETS.put('S', "\u015A\u0160\u015E");
440        PICKER_SETS.put('T', "\u0164");
441        PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC\u016E\u016A");
442        PICKER_SETS.put('Y', "\u00DD\u0178");
443        PICKER_SETS.put('Z', "\u0179\u017B\u017D");
444        PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5\u0105\u0101");
445        PICKER_SETS.put('c', "\u00E7\u0107\u010D");
446        PICKER_SETS.put('d', "\u010F");
447        PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB\u0119\u011B\u0113");
448        PICKER_SETS.put('g', "\u011F");
449        PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B\u0131");
450        PICKER_SETS.put('l', "\u0142");
451        PICKER_SETS.put('n', "\u00F1\u0144\u0148");
452        PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6\u014D");
453        PICKER_SETS.put('r', "\u0159");
454        PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161\u015F");
455        PICKER_SETS.put('t', "\u0165");
456        PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC\u016F\u016B");
457        PICKER_SETS.put('y', "\u00FD\u00FF");
458        PICKER_SETS.put('z', "\u017A\u017C\u017E");
459        PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT,
460                             "\u2026\u00A5\u2022\u00AE\u00A9\u00B1[]{}\\|");
461        PICKER_SETS.put('/', "\\");
462
463        // From packages/inputmethods/LatinIME/res/xml/kbd_symbols.xml
464
465        PICKER_SETS.put('1', "\u00b9\u00bd\u2153\u00bc\u215b");
466        PICKER_SETS.put('2', "\u00b2\u2154");
467        PICKER_SETS.put('3', "\u00b3\u00be\u215c");
468        PICKER_SETS.put('4', "\u2074");
469        PICKER_SETS.put('5', "\u215d");
470        PICKER_SETS.put('7', "\u215e");
471        PICKER_SETS.put('0', "\u207f\u2205");
472        PICKER_SETS.put('$', "\u00a2\u00a3\u20ac\u00a5\u20a3\u20a4\u20b1");
473        PICKER_SETS.put('%', "\u2030");
474        PICKER_SETS.put('*', "\u2020\u2021");
475        PICKER_SETS.put('-', "\u2013\u2014");
476        PICKER_SETS.put('+', "\u00b1");
477        PICKER_SETS.put('(', "[{<");
478        PICKER_SETS.put(')', "]}>");
479        PICKER_SETS.put('!', "\u00a1");
480        PICKER_SETS.put('"', "\u201c\u201d\u00ab\u00bb\u02dd");
481        PICKER_SETS.put('?', "\u00bf");
482        PICKER_SETS.put(',', "\u201a\u201e");
483
484        // From packages/inputmethods/LatinIME/res/xml/kbd_symbols_shift.xml
485
486        PICKER_SETS.put('=', "\u2260\u2248\u221e");
487        PICKER_SETS.put('<', "\u2264\u00ab\u2039");
488        PICKER_SETS.put('>', "\u2265\u00bb\u203a");
489    };
490
491    private boolean showCharacterPicker(View view, Editable content, char c,
492                                        boolean insert, int count) {
493        String set = PICKER_SETS.get(c);
494        if (set == null) {
495            return false;
496        }
497
498        if (count == 1) {
499            new CharacterPickerDialog(view.getContext(),
500                                      view, content, set, insert).show();
501        }
502
503        return true;
504    }
505
506    private static String toTitleCase(String src) {
507        return Character.toUpperCase(src.charAt(0)) + src.substring(1);
508    }
509
510    /* package */ static class Replaced implements NoCopySpan
511    {
512        public Replaced(char[] text) {
513            mText = text;
514        }
515
516        private char[] mText;
517    }
518}
519
520