QwertyKeyListener.java revision 9066cfe9886ac131c34d59ed0e2d287b0e3c0087
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
35    public QwertyKeyListener(Capitalize cap, boolean autotext) {
36        mAutoCap = cap;
37        mAutoText = autotext;
38    }
39
40    /**
41     * Returns a new or existing instance with the specified capitalization
42     * and correction properties.
43     */
44    public static QwertyKeyListener getInstance(boolean autotext,
45                                              Capitalize cap) {
46        int off = cap.ordinal() * 2 + (autotext ? 1 : 0);
47
48        if (sInstance[off] == null) {
49            sInstance[off] = new QwertyKeyListener(cap, autotext);
50        }
51
52        return sInstance[off];
53    }
54
55    public int getInputType() {
56        return makeTextContentType(mAutoCap, mAutoText);
57    }
58
59    public boolean onKeyDown(View view, Editable content,
60                             int keyCode, KeyEvent event) {
61        int selStart, selEnd;
62        int pref = 0;
63
64        if (view != null) {
65            pref = TextKeyListener.getInstance().getPrefs(view.getContext());
66        }
67
68        {
69            int a = Selection.getSelectionStart(content);
70            int b = Selection.getSelectionEnd(content);
71
72            selStart = Math.min(a, b);
73            selEnd = Math.max(a, b);
74
75            if (selStart < 0 || selEnd < 0) {
76                selStart = selEnd = 0;
77                Selection.setSelection(content, 0, 0);
78            }
79        }
80
81        int activeStart = content.getSpanStart(TextKeyListener.ACTIVE);
82        int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE);
83
84        // QWERTY keyboard normal case
85
86        int i = event.getUnicodeChar(getMetaState(content));
87
88        int count = event.getRepeatCount();
89        if (count > 0 && selStart == selEnd && selStart > 0) {
90            char c = content.charAt(selStart - 1);
91
92            if (c == i || c == Character.toUpperCase(i) && view != null) {
93                if (showCharacterPicker(view, content, c, false, count)) {
94                    resetMetaState(content);
95                    return true;
96                }
97            }
98        }
99
100        if (i == KeyCharacterMap.PICKER_DIALOG_INPUT) {
101            if (view != null) {
102                showCharacterPicker(view, content,
103                                    KeyCharacterMap.PICKER_DIALOG_INPUT, true, 1);
104            }
105            resetMetaState(content);
106            return true;
107        }
108
109        if (i == KeyCharacterMap.HEX_INPUT) {
110            int start;
111
112            if (selStart == selEnd) {
113                start = selEnd;
114
115                while (start > 0 && selEnd - start < 4 &&
116                       Character.digit(content.charAt(start - 1), 16) >= 0) {
117                    start--;
118                }
119            } else {
120                start = selStart;
121            }
122
123            int ch = -1;
124            try {
125                String hex = TextUtils.substring(content, start, selEnd);
126                ch = Integer.parseInt(hex, 16);
127            } catch (NumberFormatException nfe) { }
128
129            if (ch >= 0) {
130                selStart = start;
131                Selection.setSelection(content, selStart, selEnd);
132                i = ch;
133            } else {
134                i = 0;
135            }
136        }
137
138        if (i != 0) {
139            boolean dead = false;
140
141            if ((i & KeyCharacterMap.COMBINING_ACCENT) != 0) {
142                dead = true;
143                i = i & KeyCharacterMap.COMBINING_ACCENT_MASK;
144            }
145
146            if (activeStart == selStart && activeEnd == selEnd) {
147                boolean replace = false;
148
149                if (selEnd - selStart - 1 == 0) {
150                    char accent = content.charAt(selStart);
151                    int composed = event.getDeadChar(accent, i);
152
153                    if (composed != 0) {
154                        i = composed;
155                        replace = true;
156                    }
157                }
158
159                if (!replace) {
160                    Selection.setSelection(content, selEnd);
161                    content.removeSpan(TextKeyListener.ACTIVE);
162                    selStart = selEnd;
163                }
164            }
165
166            if ((pref & TextKeyListener.AUTO_CAP) != 0 &&
167                Character.isLowerCase(i) &&
168                TextKeyListener.shouldCap(mAutoCap, content, selStart)) {
169                int where = content.getSpanEnd(TextKeyListener.CAPPED);
170                int flags = content.getSpanFlags(TextKeyListener.CAPPED);
171
172                if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) {
173                    content.removeSpan(TextKeyListener.CAPPED);
174                } else {
175                    flags = i << 16;
176                    i = Character.toUpperCase(i);
177
178                    if (selStart == 0)
179                        content.setSpan(TextKeyListener.CAPPED, 0, 0,
180                                        Spannable.SPAN_MARK_MARK | flags);
181                    else
182                        content.setSpan(TextKeyListener.CAPPED,
183                                        selStart - 1, selStart,
184                                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
185                                        flags);
186                }
187            }
188
189            if (selStart != selEnd) {
190                Selection.setSelection(content, selEnd);
191            }
192            content.setSpan(OLD_SEL_START, selStart, selStart,
193                            Spannable.SPAN_MARK_MARK);
194
195            content.replace(selStart, selEnd, String.valueOf((char) i));
196
197            int oldStart = content.getSpanStart(OLD_SEL_START);
198            selEnd = Selection.getSelectionEnd(content);
199
200            if (oldStart < selEnd) {
201                content.setSpan(TextKeyListener.LAST_TYPED,
202                                oldStart, selEnd,
203                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
204
205                if (dead) {
206                    Selection.setSelection(content, oldStart, selEnd);
207                    content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd,
208                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
209                }
210            }
211
212            adjustMetaAfterKeypress(content);
213
214            // potentially do autotext replacement if the character
215            // that was typed was an autotext terminator
216
217            if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText &&
218                (i == ' ' || i == '\t' || i == '\n' ||
219                 i == ',' || i == '.' || i == '!' || i == '?' ||
220                 i == '"' || Character.getType(i) == Character.END_PUNCTUATION) &&
221                 content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT)
222                     != oldStart) {
223                int x;
224
225                for (x = oldStart; x > 0; x--) {
226                    char c = content.charAt(x - 1);
227                    if (c != '\'' && !Character.isLetter(c)) {
228                        break;
229                    }
230                }
231
232                String rep = getReplacement(content, x, oldStart, view);
233
234                if (rep != null) {
235                    Replaced[] repl = content.getSpans(0, content.length(),
236                                                     Replaced.class);
237                    for (int a = 0; a < repl.length; a++)
238                        content.removeSpan(repl[a]);
239
240                    char[] orig = new char[oldStart - x];
241                    TextUtils.getChars(content, x, oldStart, orig, 0);
242
243                    content.setSpan(new Replaced(orig), x, oldStart,
244                                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
245                    content.replace(x, oldStart, rep);
246                }
247            }
248
249            // Replace two spaces by a period and a space.
250
251            if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) {
252                selEnd = Selection.getSelectionEnd(content);
253                if (selEnd - 3 >= 0) {
254                    if (content.charAt(selEnd - 1) == ' ' &&
255                        content.charAt(selEnd - 2) == ' ') {
256                        char c = content.charAt(selEnd - 3);
257
258                        for (int j = selEnd - 3; j > 0; j--) {
259                            if (c == '"' ||
260                                Character.getType(c) == Character.END_PUNCTUATION) {
261                                c = content.charAt(j - 1);
262                            } else {
263                                break;
264                            }
265                        }
266
267                        if (Character.isLetter(c) || Character.isDigit(c)) {
268                            content.replace(selEnd - 2, selEnd - 1, ".");
269                        }
270                    }
271                }
272            }
273
274            return true;
275        } else if (keyCode == KeyEvent.KEYCODE_DEL && selStart == selEnd) {
276            // special backspace case for undoing autotext
277
278            int consider = 1;
279
280            // if backspacing over the last typed character,
281            // it undoes the autotext prior to that character
282            // (unless the character typed was newline, in which
283            // case this behavior would be confusing)
284
285            if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) {
286                if (content.charAt(selStart - 1) != '\n')
287                    consider = 2;
288            }
289
290            Replaced[] repl = content.getSpans(selStart - consider, selStart,
291                                             Replaced.class);
292
293            if (repl.length > 0) {
294                int st = content.getSpanStart(repl[0]);
295                int en = content.getSpanEnd(repl[0]);
296                String old = new String(repl[0].mText);
297
298                content.removeSpan(repl[0]);
299                content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
300                                en, en, Spannable.SPAN_POINT_POINT);
301                content.replace(st, en, old);
302
303                en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT);
304                if (en - 1 >= 0) {
305                    content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
306                                    en - 1, en,
307                                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
308                } else {
309                    content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT);
310                }
311
312                adjustMetaAfterKeypress(content);
313
314                return true;
315            }
316        }
317
318        return super.onKeyDown(view, content, keyCode, event);
319    }
320
321    private String getReplacement(CharSequence src, int start, int end,
322                                  View view) {
323        int len = end - start;
324        boolean changecase = false;
325
326        String replacement = AutoText.get(src, start, end, view);
327
328        if (replacement == null) {
329            String key = TextUtils.substring(src, start, end).toLowerCase();
330            replacement = AutoText.get(key, 0, end - start, view);
331            changecase = true;
332
333            if (replacement == null)
334                return null;
335        }
336
337        int caps = 0;
338
339        if (changecase) {
340            for (int j = start; j < end; j++) {
341                if (Character.isUpperCase(src.charAt(j)))
342                    caps++;
343            }
344        }
345
346        String out;
347
348        if (caps == 0)
349            out = replacement;
350        else if (caps == 1)
351            out = toTitleCase(replacement);
352        else if (caps == len)
353            out = replacement.toUpperCase();
354        else
355            out = toTitleCase(replacement);
356
357        if (out.length() == len &&
358            TextUtils.regionMatches(src, start, out, 0, len))
359            return null;
360
361        return out;
362    }
363
364    /**
365     * Marks the specified region of <code>content</code> as having
366     * contained <code>original</code> prior to AutoText replacement.
367     * Call this method when you have done or are about to do an
368     * AutoText-style replacement on a region of text and want to let
369     * the same mechanism (the user pressing DEL immediately after the
370     * change) undo the replacement.
371     *
372     * @param content the Editable text where the replacement was made
373     * @param start the start of the replaced region
374     * @param end the end of the replaced region; the location of the cursor
375     * @param original the text to be restored if the user presses DEL
376     */
377    public static void markAsReplaced(Spannable content, int start, int end,
378                                      String original) {
379        Replaced[] repl = content.getSpans(0, content.length(), Replaced.class);
380        for (int a = 0; a < repl.length; a++) {
381            content.removeSpan(repl[a]);
382        }
383
384        int len = original.length();
385        char[] orig = new char[len];
386        original.getChars(0, len, orig, 0);
387
388        content.setSpan(new Replaced(orig), start, end,
389                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
390    }
391
392    private static SparseArray<String> PICKER_SETS =
393                        new SparseArray<String>();
394    static {
395        PICKER_SETS.put('!', "\u00A1");
396        PICKER_SETS.put('<', "\u00AB");
397        PICKER_SETS.put('>', "\u00BB");
398        PICKER_SETS.put('?', "\u00BF");
399        PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5");
400        PICKER_SETS.put('C', "\u00C7");
401        PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB");
402        PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF");
403        PICKER_SETS.put('N', "\u00D1");
404        PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6");
405        PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC");
406        PICKER_SETS.put('Y', "\u00DD\u0178");
407        PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5");
408        PICKER_SETS.put('c', "\u00E7");
409        PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB");
410        PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF");
411        PICKER_SETS.put('n', "\u00F1");
412        PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6");
413        PICKER_SETS.put('s', "\u00A7\u00DF");
414        PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC");
415        PICKER_SETS.put('y', "\u00FD\u00FF");
416        PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT,
417                             "\u2026\u00A5\u2022\u00AE\u00A9\u00B1");
418    };
419
420    private boolean showCharacterPicker(View view, Editable content, char c,
421                                        boolean insert, int count) {
422        String set = PICKER_SETS.get(c);
423        if (set == null) {
424            return false;
425        }
426
427        if (count == 1) {
428            new CharacterPickerDialog(view.getContext(),
429                                      view, content, set, insert).show();
430        }
431
432        return true;
433    }
434
435    private static String toTitleCase(String src) {
436        return Character.toUpperCase(src.charAt(0)) + src.substring(1);
437    }
438
439    /* package */ static class Replaced implements NoCopySpan
440    {
441        public Replaced(char[] text) {
442            mText = text;
443        }
444
445        private char[] mText;
446    }
447
448    private Capitalize mAutoCap;
449    private boolean mAutoText;
450}
451
452