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