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