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