blob: 98316ae58da60e2da789347aa38b7e26d411c4f4 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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.
Jean Chalard405bc512012-05-29 19:12:34 +090030 * <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.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034 */
35public class QwertyKeyListener extends BaseKeyListener {
36 private static QwertyKeyListener[] sInstance =
37 new QwertyKeyListener[Capitalize.values().length * 2];
Jeff Brown47e6b1b2010-11-29 17:37:49 -080038 private static QwertyKeyListener sFullKeyboardInstance;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039
Jeff Brown47e6b1b2010-11-29 17:37:49 -080040 private Capitalize mAutoCap;
41 private boolean mAutoText;
42 private boolean mFullKeyboard;
43
44 private QwertyKeyListener(Capitalize cap, boolean autoText, boolean fullKeyboard) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080045 mAutoCap = cap;
Jeff Brown47e6b1b2010-11-29 17:37:49 -080046 mAutoText = autoText;
47 mFullKeyboard = fullKeyboard;
48 }
49
50 public QwertyKeyListener(Capitalize cap, boolean autoText) {
51 this(cap, autoText, false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052 }
53
54 /**
55 * Returns a new or existing instance with the specified capitalization
56 * and correction properties.
57 */
Jeff Brown47e6b1b2010-11-29 17:37:49 -080058 public static QwertyKeyListener getInstance(boolean autoText, Capitalize cap) {
59 int off = cap.ordinal() * 2 + (autoText ? 1 : 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060
61 if (sInstance[off] == null) {
Jeff Brown47e6b1b2010-11-29 17:37:49 -080062 sInstance[off] = new QwertyKeyListener(cap, autoText);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063 }
64
65 return sInstance[off];
66 }
67
Jeff Brown47e6b1b2010-11-29 17:37:49 -080068 /**
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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080080 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
Jeff Brown6b53e8d2010-11-10 16:03:06 -0800111 int i = event.getUnicodeChar(event.getMetaState() | getMetaState(content));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800112
Jeff Brown47e6b1b2010-11-29 17:37:49 -0800113 if (!mFullKeyboard) {
114 int count = event.getRepeatCount();
115 if (count > 0 && selStart == selEnd && selStart > 0) {
116 char c = content.charAt(selStart - 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800117
Jeff Brown47e6b1b2010-11-29 17:37:49 -0800118 if (c == i || c == Character.toUpperCase(i) && view != null) {
119 if (showCharacterPicker(view, content, c, false, count)) {
120 resetMetaState(content);
121 return true;
122 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800123 }
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;
Jean Chalard54865502013-02-25 19:56:32 -0800183 dead = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800184 }
185 }
186
187 if (!replace) {
188 Selection.setSelection(content, selEnd);
189 content.removeSpan(TextKeyListener.ACTIVE);
190 selStart = selEnd;
191 }
192 }
193
194 if ((pref & TextKeyListener.AUTO_CAP) != 0 &&
195 Character.isLowerCase(i) &&
196 TextKeyListener.shouldCap(mAutoCap, content, selStart)) {
197 int where = content.getSpanEnd(TextKeyListener.CAPPED);
198 int flags = content.getSpanFlags(TextKeyListener.CAPPED);
199
200 if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) {
201 content.removeSpan(TextKeyListener.CAPPED);
202 } else {
203 flags = i << 16;
204 i = Character.toUpperCase(i);
205
206 if (selStart == 0)
207 content.setSpan(TextKeyListener.CAPPED, 0, 0,
208 Spannable.SPAN_MARK_MARK | flags);
209 else
210 content.setSpan(TextKeyListener.CAPPED,
211 selStart - 1, selStart,
212 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
213 flags);
214 }
215 }
216
217 if (selStart != selEnd) {
218 Selection.setSelection(content, selEnd);
219 }
220 content.setSpan(OLD_SEL_START, selStart, selStart,
221 Spannable.SPAN_MARK_MARK);
222
223 content.replace(selStart, selEnd, String.valueOf((char) i));
224
225 int oldStart = content.getSpanStart(OLD_SEL_START);
226 selEnd = Selection.getSelectionEnd(content);
227
228 if (oldStart < selEnd) {
229 content.setSpan(TextKeyListener.LAST_TYPED,
230 oldStart, selEnd,
231 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
232
233 if (dead) {
234 Selection.setSelection(content, oldStart, selEnd);
235 content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd,
236 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
237 }
238 }
239
240 adjustMetaAfterKeypress(content);
241
242 // potentially do autotext replacement if the character
243 // that was typed was an autotext terminator
244
245 if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText &&
246 (i == ' ' || i == '\t' || i == '\n' ||
247 i == ',' || i == '.' || i == '!' || i == '?' ||
248 i == '"' || Character.getType(i) == Character.END_PUNCTUATION) &&
249 content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT)
250 != oldStart) {
251 int x;
252
253 for (x = oldStart; x > 0; x--) {
254 char c = content.charAt(x - 1);
255 if (c != '\'' && !Character.isLetter(c)) {
256 break;
257 }
258 }
259
260 String rep = getReplacement(content, x, oldStart, view);
261
262 if (rep != null) {
263 Replaced[] repl = content.getSpans(0, content.length(),
264 Replaced.class);
265 for (int a = 0; a < repl.length; a++)
266 content.removeSpan(repl[a]);
267
268 char[] orig = new char[oldStart - x];
269 TextUtils.getChars(content, x, oldStart, orig, 0);
270
271 content.setSpan(new Replaced(orig), x, oldStart,
272 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
273 content.replace(x, oldStart, rep);
274 }
275 }
276
277 // Replace two spaces by a period and a space.
278
279 if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) {
280 selEnd = Selection.getSelectionEnd(content);
281 if (selEnd - 3 >= 0) {
282 if (content.charAt(selEnd - 1) == ' ' &&
283 content.charAt(selEnd - 2) == ' ') {
284 char c = content.charAt(selEnd - 3);
285
286 for (int j = selEnd - 3; j > 0; j--) {
287 if (c == '"' ||
288 Character.getType(c) == Character.END_PUNCTUATION) {
289 c = content.charAt(j - 1);
290 } else {
291 break;
292 }
293 }
294
295 if (Character.isLetter(c) || Character.isDigit(c)) {
296 content.replace(selEnd - 2, selEnd - 1, ".");
297 }
298 }
299 }
300 }
301
302 return true;
Jeff Brown14d0ca12010-12-21 16:06:44 -0800303 } else if (keyCode == KeyEvent.KEYCODE_DEL
304 && (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_ALT_ON))
305 && selStart == selEnd) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800306 // special backspace case for undoing autotext
307
308 int consider = 1;
309
310 // if backspacing over the last typed character,
311 // it undoes the autotext prior to that character
312 // (unless the character typed was newline, in which
313 // case this behavior would be confusing)
314
315 if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) {
316 if (content.charAt(selStart - 1) != '\n')
317 consider = 2;
318 }
319
320 Replaced[] repl = content.getSpans(selStart - consider, selStart,
321 Replaced.class);
322
323 if (repl.length > 0) {
324 int st = content.getSpanStart(repl[0]);
325 int en = content.getSpanEnd(repl[0]);
326 String old = new String(repl[0].mText);
327
328 content.removeSpan(repl[0]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800329
The Android Open Source Project4df24232009-03-05 14:34:35 -0800330 // only cancel the autocomplete if the cursor is at the end of
The Android Open Source Projectc39a6e02009-03-11 12:11:56 -0700331 // the replaced span (or after it, because the user is
332 // backspacing over the space after the word, not the word
333 // itself).
334 if (selStart >= en) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800335 content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
The Android Open Source Project4df24232009-03-05 14:34:35 -0800336 en, en, Spannable.SPAN_POINT_POINT);
337 content.replace(st, en, old);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800338
The Android Open Source Project4df24232009-03-05 14:34:35 -0800339 en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT);
340 if (en - 1 >= 0) {
341 content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
342 en - 1, en,
343 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
344 } else {
345 content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT);
346 }
347 adjustMetaAfterKeypress(content);
348 } else {
349 adjustMetaAfterKeypress(content);
350 return super.onKeyDown(view, content, keyCode, event);
351 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800352
353 return true;
354 }
355 }
356
357 return super.onKeyDown(view, content, keyCode, event);
358 }
359
360 private String getReplacement(CharSequence src, int start, int end,
361 View view) {
362 int len = end - start;
363 boolean changecase = false;
364
365 String replacement = AutoText.get(src, start, end, view);
366
367 if (replacement == null) {
368 String key = TextUtils.substring(src, start, end).toLowerCase();
369 replacement = AutoText.get(key, 0, end - start, view);
370 changecase = true;
371
372 if (replacement == null)
373 return null;
374 }
375
376 int caps = 0;
377
378 if (changecase) {
379 for (int j = start; j < end; j++) {
380 if (Character.isUpperCase(src.charAt(j)))
381 caps++;
382 }
383 }
384
385 String out;
386
387 if (caps == 0)
388 out = replacement;
389 else if (caps == 1)
390 out = toTitleCase(replacement);
391 else if (caps == len)
392 out = replacement.toUpperCase();
393 else
394 out = toTitleCase(replacement);
395
396 if (out.length() == len &&
397 TextUtils.regionMatches(src, start, out, 0, len))
398 return null;
399
400 return out;
401 }
402
403 /**
404 * Marks the specified region of <code>content</code> as having
405 * contained <code>original</code> prior to AutoText replacement.
406 * Call this method when you have done or are about to do an
407 * AutoText-style replacement on a region of text and want to let
408 * the same mechanism (the user pressing DEL immediately after the
409 * change) undo the replacement.
410 *
411 * @param content the Editable text where the replacement was made
412 * @param start the start of the replaced region
413 * @param end the end of the replaced region; the location of the cursor
414 * @param original the text to be restored if the user presses DEL
415 */
416 public static void markAsReplaced(Spannable content, int start, int end,
417 String original) {
418 Replaced[] repl = content.getSpans(0, content.length(), Replaced.class);
419 for (int a = 0; a < repl.length; a++) {
420 content.removeSpan(repl[a]);
421 }
422
423 int len = original.length();
424 char[] orig = new char[len];
425 original.getChars(0, len, orig, 0);
426
427 content.setSpan(new Replaced(orig), start, end,
428 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
429 }
430
431 private static SparseArray<String> PICKER_SETS =
432 new SparseArray<String>();
433 static {
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700434 PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5\u0104\u0100");
435 PICKER_SETS.put('C', "\u00C7\u0106\u010C");
436 PICKER_SETS.put('D', "\u010E");
437 PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB\u0118\u011A\u0112");
Eric Fischerec1f8a22009-09-30 18:03:47 -0700438 PICKER_SETS.put('G', "\u011E");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700439 PICKER_SETS.put('L', "\u0141");
Eric Fischer4ef29952009-09-15 16:02:47 -0700440 PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A\u0130");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700441 PICKER_SETS.put('N', "\u00D1\u0143\u0147");
442 PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6\u014C");
443 PICKER_SETS.put('R', "\u0158");
Eric Fischerec1f8a22009-09-30 18:03:47 -0700444 PICKER_SETS.put('S', "\u015A\u0160\u015E");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700445 PICKER_SETS.put('T', "\u0164");
446 PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC\u016E\u016A");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800447 PICKER_SETS.put('Y', "\u00DD\u0178");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700448 PICKER_SETS.put('Z', "\u0179\u017B\u017D");
449 PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5\u0105\u0101");
450 PICKER_SETS.put('c', "\u00E7\u0107\u010D");
451 PICKER_SETS.put('d', "\u010F");
452 PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB\u0119\u011B\u0113");
Eric Fischerec1f8a22009-09-30 18:03:47 -0700453 PICKER_SETS.put('g', "\u011F");
Eric Fischer4ef29952009-09-15 16:02:47 -0700454 PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B\u0131");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700455 PICKER_SETS.put('l', "\u0142");
456 PICKER_SETS.put('n', "\u00F1\u0144\u0148");
457 PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6\u014D");
458 PICKER_SETS.put('r', "\u0159");
Eric Fischerec1f8a22009-09-30 18:03:47 -0700459 PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161\u015F");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700460 PICKER_SETS.put('t', "\u0165");
461 PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC\u016F\u016B");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800462 PICKER_SETS.put('y', "\u00FD\u00FF");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700463 PICKER_SETS.put('z', "\u017A\u017C\u017E");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800464 PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT,
Eric Fischerd7c432b2009-11-17 16:28:30 -0800465 "\u2026\u00A5\u2022\u00AE\u00A9\u00B1[]{}\\|");
Eric Fischercebe3472009-09-16 16:26:48 -0700466 PICKER_SETS.put('/', "\\");
Eric Fischer4ef29952009-09-15 16:02:47 -0700467
468 // From packages/inputmethods/LatinIME/res/xml/kbd_symbols.xml
469
470 PICKER_SETS.put('1', "\u00b9\u00bd\u2153\u00bc\u215b");
471 PICKER_SETS.put('2', "\u00b2\u2154");
472 PICKER_SETS.put('3', "\u00b3\u00be\u215c");
473 PICKER_SETS.put('4', "\u2074");
474 PICKER_SETS.put('5', "\u215d");
475 PICKER_SETS.put('7', "\u215e");
476 PICKER_SETS.put('0', "\u207f\u2205");
477 PICKER_SETS.put('$', "\u00a2\u00a3\u20ac\u00a5\u20a3\u20a4\u20b1");
478 PICKER_SETS.put('%', "\u2030");
479 PICKER_SETS.put('*', "\u2020\u2021");
480 PICKER_SETS.put('-', "\u2013\u2014");
481 PICKER_SETS.put('+', "\u00b1");
482 PICKER_SETS.put('(', "[{<");
483 PICKER_SETS.put(')', "]}>");
484 PICKER_SETS.put('!', "\u00a1");
485 PICKER_SETS.put('"', "\u201c\u201d\u00ab\u00bb\u02dd");
486 PICKER_SETS.put('?', "\u00bf");
487 PICKER_SETS.put(',', "\u201a\u201e");
488
489 // From packages/inputmethods/LatinIME/res/xml/kbd_symbols_shift.xml
490
491 PICKER_SETS.put('=', "\u2260\u2248\u221e");
492 PICKER_SETS.put('<', "\u2264\u00ab\u2039");
493 PICKER_SETS.put('>', "\u2265\u00bb\u203a");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800494 };
495
496 private boolean showCharacterPicker(View view, Editable content, char c,
497 boolean insert, int count) {
498 String set = PICKER_SETS.get(c);
499 if (set == null) {
500 return false;
501 }
502
503 if (count == 1) {
504 new CharacterPickerDialog(view.getContext(),
505 view, content, set, insert).show();
506 }
507
508 return true;
509 }
510
511 private static String toTitleCase(String src) {
512 return Character.toUpperCase(src.charAt(0)) + src.substring(1);
513 }
514
515 /* package */ static class Replaced implements NoCopySpan
516 {
517 public Replaced(char[] text) {
518 mText = text;
519 }
520
521 private char[] mText;
522 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800523}
524