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