blob: f736f85273ceef7c2617da5724ee5931c3521ea0 [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.
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]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800299
The Android Open Source Project4df24232009-03-05 14:34:35 -0800300 // only cancel the autocomplete if the cursor is at the end of
The Android Open Source Projectc39a6e02009-03-11 12:11:56 -0700301 // the replaced span (or after it, because the user is
302 // backspacing over the space after the word, not the word
303 // itself).
304 if (selStart >= en) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800305 content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
The Android Open Source Project4df24232009-03-05 14:34:35 -0800306 en, en, Spannable.SPAN_POINT_POINT);
307 content.replace(st, en, old);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800308
The Android Open Source Project4df24232009-03-05 14:34:35 -0800309 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 adjustMetaAfterKeypress(content);
318 } else {
319 adjustMetaAfterKeypress(content);
320 return super.onKeyDown(view, content, keyCode, event);
321 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800322
323 return true;
324 }
325 }
326
327 return super.onKeyDown(view, content, keyCode, event);
328 }
329
330 private String getReplacement(CharSequence src, int start, int end,
331 View view) {
332 int len = end - start;
333 boolean changecase = false;
334
335 String replacement = AutoText.get(src, start, end, view);
336
337 if (replacement == null) {
338 String key = TextUtils.substring(src, start, end).toLowerCase();
339 replacement = AutoText.get(key, 0, end - start, view);
340 changecase = true;
341
342 if (replacement == null)
343 return null;
344 }
345
346 int caps = 0;
347
348 if (changecase) {
349 for (int j = start; j < end; j++) {
350 if (Character.isUpperCase(src.charAt(j)))
351 caps++;
352 }
353 }
354
355 String out;
356
357 if (caps == 0)
358 out = replacement;
359 else if (caps == 1)
360 out = toTitleCase(replacement);
361 else if (caps == len)
362 out = replacement.toUpperCase();
363 else
364 out = toTitleCase(replacement);
365
366 if (out.length() == len &&
367 TextUtils.regionMatches(src, start, out, 0, len))
368 return null;
369
370 return out;
371 }
372
373 /**
374 * Marks the specified region of <code>content</code> as having
375 * contained <code>original</code> prior to AutoText replacement.
376 * Call this method when you have done or are about to do an
377 * AutoText-style replacement on a region of text and want to let
378 * the same mechanism (the user pressing DEL immediately after the
379 * change) undo the replacement.
380 *
381 * @param content the Editable text where the replacement was made
382 * @param start the start of the replaced region
383 * @param end the end of the replaced region; the location of the cursor
384 * @param original the text to be restored if the user presses DEL
385 */
386 public static void markAsReplaced(Spannable content, int start, int end,
387 String original) {
388 Replaced[] repl = content.getSpans(0, content.length(), Replaced.class);
389 for (int a = 0; a < repl.length; a++) {
390 content.removeSpan(repl[a]);
391 }
392
393 int len = original.length();
394 char[] orig = new char[len];
395 original.getChars(0, len, orig, 0);
396
397 content.setSpan(new Replaced(orig), start, end,
398 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
399 }
400
401 private static SparseArray<String> PICKER_SETS =
402 new SparseArray<String>();
403 static {
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700404 PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5\u0104\u0100");
405 PICKER_SETS.put('C', "\u00C7\u0106\u010C");
406 PICKER_SETS.put('D', "\u010E");
407 PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB\u0118\u011A\u0112");
408 PICKER_SETS.put('L', "\u0141");
Eric Fischer4ef29952009-09-15 16:02:47 -0700409 PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A\u0130");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700410 PICKER_SETS.put('N', "\u00D1\u0143\u0147");
411 PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6\u014C");
412 PICKER_SETS.put('R', "\u0158");
413 PICKER_SETS.put('S', "\u015A\u0160");
414 PICKER_SETS.put('T', "\u0164");
415 PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC\u016E\u016A");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800416 PICKER_SETS.put('Y', "\u00DD\u0178");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700417 PICKER_SETS.put('Z', "\u0179\u017B\u017D");
418 PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5\u0105\u0101");
419 PICKER_SETS.put('c', "\u00E7\u0107\u010D");
420 PICKER_SETS.put('d', "\u010F");
421 PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB\u0119\u011B\u0113");
Eric Fischer4ef29952009-09-15 16:02:47 -0700422 PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B\u0131");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700423 PICKER_SETS.put('l', "\u0142");
424 PICKER_SETS.put('n', "\u00F1\u0144\u0148");
425 PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6\u014D");
426 PICKER_SETS.put('r', "\u0159");
427 PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161");
428 PICKER_SETS.put('t', "\u0165");
429 PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC\u016F\u016B");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800430 PICKER_SETS.put('y', "\u00FD\u00FF");
Eric Fischera3ea3ae2009-03-31 14:17:10 -0700431 PICKER_SETS.put('z', "\u017A\u017C\u017E");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800432 PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT,
Eric Fischerf16da452009-08-13 13:27:23 -0700433 "\u2026\u00A5\u2022\u00AE\u00A9\u00B1[]{}\\");
Eric Fischercebe3472009-09-16 16:26:48 -0700434 PICKER_SETS.put('/', "\\");
Eric Fischer4ef29952009-09-15 16:02:47 -0700435
436 // From packages/inputmethods/LatinIME/res/xml/kbd_symbols.xml
437
438 PICKER_SETS.put('1', "\u00b9\u00bd\u2153\u00bc\u215b");
439 PICKER_SETS.put('2', "\u00b2\u2154");
440 PICKER_SETS.put('3', "\u00b3\u00be\u215c");
441 PICKER_SETS.put('4', "\u2074");
442 PICKER_SETS.put('5', "\u215d");
443 PICKER_SETS.put('7', "\u215e");
444 PICKER_SETS.put('0', "\u207f\u2205");
445 PICKER_SETS.put('$', "\u00a2\u00a3\u20ac\u00a5\u20a3\u20a4\u20b1");
446 PICKER_SETS.put('%', "\u2030");
447 PICKER_SETS.put('*', "\u2020\u2021");
448 PICKER_SETS.put('-', "\u2013\u2014");
449 PICKER_SETS.put('+', "\u00b1");
450 PICKER_SETS.put('(', "[{<");
451 PICKER_SETS.put(')', "]}>");
452 PICKER_SETS.put('!', "\u00a1");
453 PICKER_SETS.put('"', "\u201c\u201d\u00ab\u00bb\u02dd");
454 PICKER_SETS.put('?', "\u00bf");
455 PICKER_SETS.put(',', "\u201a\u201e");
456
457 // From packages/inputmethods/LatinIME/res/xml/kbd_symbols_shift.xml
458
459 PICKER_SETS.put('=', "\u2260\u2248\u221e");
460 PICKER_SETS.put('<', "\u2264\u00ab\u2039");
461 PICKER_SETS.put('>', "\u2265\u00bb\u203a");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800462 };
463
464 private boolean showCharacterPicker(View view, Editable content, char c,
465 boolean insert, int count) {
466 String set = PICKER_SETS.get(c);
467 if (set == null) {
468 return false;
469 }
470
471 if (count == 1) {
472 new CharacterPickerDialog(view.getContext(),
473 view, content, set, insert).show();
474 }
475
476 return true;
477 }
478
479 private static String toTitleCase(String src) {
480 return Character.toUpperCase(src.charAt(0)) + src.substring(1);
481 }
482
483 /* package */ static class Replaced implements NoCopySpan
484 {
485 public Replaced(char[] text) {
486 mText = text;
487 }
488
489 private char[] mText;
490 }
491
492 private Capitalize mAutoCap;
493 private boolean mAutoText;
494}
495