QwertyKeyListener.java revision 4df2423a947bcd3f024cc3d3a1a315a8dc428598
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 300 // only cancel the autocomplete if the cursor is at the end of 301 // the replaced span 302 if (selStart == en) { 303 content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, 304 en, en, Spannable.SPAN_POINT_POINT); 305 content.replace(st, en, old); 306 307 en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT); 308 if (en - 1 >= 0) { 309 content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, 310 en - 1, en, 311 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 312 } else { 313 content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT); 314 } 315 adjustMetaAfterKeypress(content); 316 } else { 317 adjustMetaAfterKeypress(content); 318 return super.onKeyDown(view, content, keyCode, event); 319 } 320 321 return true; 322 } 323 } 324 325 return super.onKeyDown(view, content, keyCode, event); 326 } 327 328 private String getReplacement(CharSequence src, int start, int end, 329 View view) { 330 int len = end - start; 331 boolean changecase = false; 332 333 String replacement = AutoText.get(src, start, end, view); 334 335 if (replacement == null) { 336 String key = TextUtils.substring(src, start, end).toLowerCase(); 337 replacement = AutoText.get(key, 0, end - start, view); 338 changecase = true; 339 340 if (replacement == null) 341 return null; 342 } 343 344 int caps = 0; 345 346 if (changecase) { 347 for (int j = start; j < end; j++) { 348 if (Character.isUpperCase(src.charAt(j))) 349 caps++; 350 } 351 } 352 353 String out; 354 355 if (caps == 0) 356 out = replacement; 357 else if (caps == 1) 358 out = toTitleCase(replacement); 359 else if (caps == len) 360 out = replacement.toUpperCase(); 361 else 362 out = toTitleCase(replacement); 363 364 if (out.length() == len && 365 TextUtils.regionMatches(src, start, out, 0, len)) 366 return null; 367 368 return out; 369 } 370 371 /** 372 * Marks the specified region of <code>content</code> as having 373 * contained <code>original</code> prior to AutoText replacement. 374 * Call this method when you have done or are about to do an 375 * AutoText-style replacement on a region of text and want to let 376 * the same mechanism (the user pressing DEL immediately after the 377 * change) undo the replacement. 378 * 379 * @param content the Editable text where the replacement was made 380 * @param start the start of the replaced region 381 * @param end the end of the replaced region; the location of the cursor 382 * @param original the text to be restored if the user presses DEL 383 */ 384 public static void markAsReplaced(Spannable content, int start, int end, 385 String original) { 386 Replaced[] repl = content.getSpans(0, content.length(), Replaced.class); 387 for (int a = 0; a < repl.length; a++) { 388 content.removeSpan(repl[a]); 389 } 390 391 int len = original.length(); 392 char[] orig = new char[len]; 393 original.getChars(0, len, orig, 0); 394 395 content.setSpan(new Replaced(orig), start, end, 396 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 397 } 398 399 private static SparseArray<String> PICKER_SETS = 400 new SparseArray<String>(); 401 static { 402 PICKER_SETS.put('!', "\u00A1"); 403 PICKER_SETS.put('<', "\u00AB"); 404 PICKER_SETS.put('>', "\u00BB"); 405 PICKER_SETS.put('?', "\u00BF"); 406 PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5"); 407 PICKER_SETS.put('C', "\u00C7"); 408 PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB"); 409 PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF"); 410 PICKER_SETS.put('N', "\u00D1"); 411 PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6"); 412 PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC"); 413 PICKER_SETS.put('Y', "\u00DD\u0178"); 414 PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5"); 415 PICKER_SETS.put('c', "\u00E7"); 416 PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB"); 417 PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF"); 418 PICKER_SETS.put('n', "\u00F1"); 419 PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6"); 420 PICKER_SETS.put('s', "\u00A7\u00DF"); 421 PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC"); 422 PICKER_SETS.put('y', "\u00FD\u00FF"); 423 PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT, 424 "\u2026\u00A5\u2022\u00AE\u00A9\u00B1"); 425 }; 426 427 private boolean showCharacterPicker(View view, Editable content, char c, 428 boolean insert, int count) { 429 String set = PICKER_SETS.get(c); 430 if (set == null) { 431 return false; 432 } 433 434 if (count == 1) { 435 new CharacterPickerDialog(view.getContext(), 436 view, content, set, insert).show(); 437 } 438 439 return true; 440 } 441 442 private static String toTitleCase(String src) { 443 return Character.toUpperCase(src.charAt(0)) + src.substring(1); 444 } 445 446 /* package */ static class Replaced implements NoCopySpan 447 { 448 public Replaced(char[] text) { 449 mText = text; 450 } 451 452 private char[] mText; 453 } 454 455 private Capitalize mAutoCap; 456 private boolean mAutoText; 457} 458 459