2 * Copyright (C) 2006 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.text.method;
19 import android.graphics.Paint;
20 import android.icu.lang.UCharacter;
21 import android.icu.lang.UProperty;
22 import android.view.KeyEvent;
23 import android.view.View;
24 import android.text.*;
25 import android.text.method.TextKeyListener.Capitalize;
26 import android.text.style.ReplacementSpan;
27 import android.widget.TextView;
29 import com.android.internal.annotations.GuardedBy;
31 import java.text.BreakIterator;
32 import java.util.Arrays;
33 import java.util.Collections;
34 import java.util.HashSet;
37 * Abstract base class for key listeners.
39 * Provides a basic foundation for entering and editing text.
40 * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert
41 * characters as keys are pressed.
43 * As for all implementations of {@link KeyListener}, this class is only concerned
44 * with hardware keyboards. Software input methods have no obligation to trigger
45 * the methods in this class.
47 public abstract class BaseKeyListener extends MetaKeyKeyListener
48 implements KeyListener {
49 /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();
51 private static final int LINE_FEED = 0x0A;
52 private static final int CARRIAGE_RETURN = 0x0D;
54 private final Object mLock = new Object();
57 static Paint sCachedPaint = null;
60 * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
61 * a {@link TextView}. If there is a selection, deletes the selection; otherwise,
62 * deletes the character before the cursor, if any; ALT+DEL deletes everything on
63 * the line the cursor is on.
65 * @return true if anything was deleted; false otherwise.
67 public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
68 return backspaceOrForwardDelete(view, content, keyCode, event, false);
72 * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL}
73 * key in a {@link TextView}. If there is a selection, deletes the selection; otherwise,
74 * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on
75 * the line the cursor is on.
77 * @return true if anything was deleted; false otherwise.
79 public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) {
80 return backspaceOrForwardDelete(view, content, keyCode, event, true);
83 // Returns true if the given code point is a variation selector.
84 private static boolean isVariationSelector(int codepoint) {
85 return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR);
88 // Returns the offset of the replacement span edge if the offset is inside of the replacement
89 // span. Otherwise, does nothing and returns the input offset value.
90 private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) {
91 if (!(text instanceof Spanned)) {
95 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class);
96 for (int i = 0; i < spans.length; i++) {
97 final int start = ((Spanned) text).getSpanStart(spans[i]);
98 final int end = ((Spanned) text).getSpanEnd(spans[i]);
100 if (start < offset && end > offset) {
101 offset = moveToStart ? start : end;
107 // Returns the start offset to be deleted by a backspace key from the given offset.
108 private static int getOffsetForBackspaceKey(CharSequence text, int offset) {
114 final int STATE_START = 0;
116 // The offset is immediately before line feed.
117 final int STATE_LF = 1;
119 // The offset is immediately before a KEYCAP.
120 final int STATE_BEFORE_KEYCAP = 2;
121 // The offset is immediately before a variation selector and a KEYCAP.
122 final int STATE_BEFORE_VS_AND_KEYCAP = 3;
124 // The offset is immediately before an emoji modifier.
125 final int STATE_BEFORE_EMOJI_MODIFIER = 4;
126 // The offset is immediately before a variation selector and an emoji modifier.
127 final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5;
129 // The offset is immediately before a variation selector.
130 final int STATE_BEFORE_VS = 6;
132 // The offset is immediately before a ZWJ emoji.
133 final int STATE_BEFORE_ZWJ_EMOJI = 7;
134 // The offset is immediately before a ZWJ that were seen before a ZWJ emoji.
135 final int STATE_BEFORE_ZWJ = 8;
136 // The offset is immediately before a variation selector and a ZWJ that were seen before a
138 final int STATE_BEFORE_VS_AND_ZWJ = 9;
140 // The number of following RIS code points is odd.
141 final int STATE_ODD_NUMBERED_RIS = 10;
142 // The number of following RIS code points is even.
143 final int STATE_EVEN_NUMBERED_RIS = 11;
145 // The state machine has been stopped.
146 final int STATE_FINISHED = 12;
148 int deleteCharCount = 0; // Char count to be deleted by backspace.
149 int lastSeenVSCharCount = 0; // Char count of previous variation selector.
151 int state = STATE_START;
153 int tmpOffset = offset;
155 final int codePoint = Character.codePointBefore(text, tmpOffset);
156 tmpOffset -= Character.charCount(codePoint);
160 deleteCharCount = Character.charCount(codePoint);
161 if (codePoint == LINE_FEED) {
163 } else if (isVariationSelector(codePoint)) {
164 state = STATE_BEFORE_VS;
165 } else if (Emoji.isZwjEmoji(codePoint)) {
166 state = STATE_BEFORE_ZWJ_EMOJI;
167 } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
168 state = STATE_ODD_NUMBERED_RIS;
169 } else if (Emoji.isEmojiModifier(codePoint)) {
170 state = STATE_BEFORE_EMOJI_MODIFIER;
171 } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) {
172 state = STATE_BEFORE_KEYCAP;
174 state = STATE_FINISHED;
178 if (codePoint == CARRIAGE_RETURN) {
181 state = STATE_FINISHED;
182 case STATE_ODD_NUMBERED_RIS:
183 if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
184 deleteCharCount += 2; /* Char count of RIS */
185 state = STATE_EVEN_NUMBERED_RIS;
187 state = STATE_FINISHED;
190 case STATE_EVEN_NUMBERED_RIS:
191 if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
192 deleteCharCount -= 2; /* Char count of RIS */
193 state = STATE_ODD_NUMBERED_RIS;
195 state = STATE_FINISHED;
198 case STATE_BEFORE_KEYCAP:
199 if (isVariationSelector(codePoint)) {
200 lastSeenVSCharCount = Character.charCount(codePoint);
201 state = STATE_BEFORE_VS_AND_KEYCAP;
205 if (Emoji.isKeycapBase(codePoint)) {
206 deleteCharCount += Character.charCount(codePoint);
208 state = STATE_FINISHED;
210 case STATE_BEFORE_VS_AND_KEYCAP:
211 if (Emoji.isKeycapBase(codePoint)) {
212 deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
214 state = STATE_FINISHED;
216 case STATE_BEFORE_EMOJI_MODIFIER:
217 if (isVariationSelector(codePoint)) {
218 lastSeenVSCharCount = Character.charCount(codePoint);
219 state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER;
221 } else if (Emoji.isEmojiModifierBase(codePoint)) {
222 deleteCharCount += Character.charCount(codePoint);
224 state = STATE_FINISHED;
226 case STATE_BEFORE_VS_AND_EMOJI_MODIFIER:
227 if (Emoji.isEmojiModifierBase(codePoint)) {
228 deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
230 state = STATE_FINISHED;
232 case STATE_BEFORE_VS:
233 if (Emoji.isZwjEmoji(codePoint)) {
234 deleteCharCount += Character.charCount(codePoint);
235 state = STATE_BEFORE_ZWJ_EMOJI;
239 if (!isVariationSelector(codePoint) &&
240 UCharacter.getCombiningClass(codePoint) == 0) {
241 deleteCharCount += Character.charCount(codePoint);
243 state = STATE_FINISHED;
245 case STATE_BEFORE_ZWJ_EMOJI:
246 if (codePoint == Emoji.ZERO_WIDTH_JOINER) {
247 state = STATE_BEFORE_ZWJ;
249 state = STATE_FINISHED;
252 case STATE_BEFORE_ZWJ:
253 if (Emoji.isZwjEmoji(codePoint)) {
254 deleteCharCount += Character.charCount(codePoint) + 1; // +1 for ZWJ.
255 state = STATE_BEFORE_ZWJ_EMOJI;
256 } else if (isVariationSelector(codePoint)) {
257 lastSeenVSCharCount = Character.charCount(codePoint);
258 state = STATE_BEFORE_VS_AND_ZWJ;
260 state = STATE_FINISHED;
263 case STATE_BEFORE_VS_AND_ZWJ:
264 if (Emoji.isZwjEmoji(codePoint)) {
266 deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
267 lastSeenVSCharCount = 0;
268 state = STATE_BEFORE_ZWJ_EMOJI;
270 state = STATE_FINISHED;
274 throw new IllegalArgumentException("state " + state + " is unknown");
276 } while (tmpOffset > 0 && state != STATE_FINISHED);
278 return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
281 // Returns the end offset to be deleted by a forward delete key from the given offset.
282 private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
283 final int len = text.length();
285 if (offset >= len - 1) {
289 offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
290 offset, Paint.CURSOR_AFTER);
292 return adjustReplacementSpan(text, offset, false /* move to the end */);
295 private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
296 KeyEvent event, boolean isForwardDelete) {
297 // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
298 if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
299 & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
303 // If there is a current selection, delete it.
304 if (deleteSelection(view, content)) {
308 // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
309 boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
310 boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
311 boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
314 if (isAltActive || isShiftActive) {
315 // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
318 return deleteUntilWordBoundary(view, content, isForwardDelete);
321 // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
322 if (isAltActive && deleteLine(view, content)) {
326 // Delete a character.
327 final int start = Selection.getSelectionEnd(content);
329 if (isForwardDelete) {
331 if (view instanceof TextView) {
332 paint = ((TextView)view).getPaint();
334 synchronized (mLock) {
335 if (sCachedPaint == null) {
336 sCachedPaint = new Paint();
338 paint = sCachedPaint;
341 end = getOffsetForForwardDeleteKey(content, start, paint);
343 end = getOffsetForBackspaceKey(content, start);
346 content.delete(Math.min(start, end), Math.max(start, end));
352 private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
353 int currentCursorOffset = Selection.getSelectionStart(content);
355 // If there is a selection, do nothing.
356 if (currentCursorOffset != Selection.getSelectionEnd(content)) {
360 // Early exit if there is no contents to delete.
361 if ((!isForwardDelete && currentCursorOffset == 0) ||
362 (isForwardDelete && currentCursorOffset == content.length())) {
366 WordIterator wordIterator = null;
367 if (view instanceof TextView) {
368 wordIterator = ((TextView)view).getWordIterator();
371 if (wordIterator == null) {
372 // Default locale is used for WordIterator since the appropriate locale is not clear
374 // TODO: Use appropriate locale for WordIterator.
375 wordIterator = new WordIterator();
381 if (isForwardDelete) {
382 deleteFrom = currentCursorOffset;
383 wordIterator.setCharSequence(content, deleteFrom, content.length());
384 deleteTo = wordIterator.following(currentCursorOffset);
385 if (deleteTo == BreakIterator.DONE) {
386 deleteTo = content.length();
389 deleteTo = currentCursorOffset;
390 wordIterator.setCharSequence(content, 0, deleteTo);
391 deleteFrom = wordIterator.preceding(currentCursorOffset);
392 if (deleteFrom == BreakIterator.DONE) {
396 content.delete(deleteFrom, deleteTo);
400 private boolean deleteSelection(View view, Editable content) {
401 int selectionStart = Selection.getSelectionStart(content);
402 int selectionEnd = Selection.getSelectionEnd(content);
403 if (selectionEnd < selectionStart) {
404 int temp = selectionEnd;
405 selectionEnd = selectionStart;
406 selectionStart = temp;
408 if (selectionStart != selectionEnd) {
409 content.delete(selectionStart, selectionEnd);
415 private boolean deleteLine(View view, Editable content) {
416 if (view instanceof TextView) {
417 final Layout layout = ((TextView) view).getLayout();
418 if (layout != null) {
419 final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
420 final int start = layout.getLineStart(line);
421 final int end = layout.getLineEnd(line);
423 content.delete(start, end);
431 static int makeTextContentType(Capitalize caps, boolean autoText) {
432 int contentType = InputType.TYPE_CLASS_TEXT;
435 contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
438 contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
441 contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
445 contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
450 public boolean onKeyDown(View view, Editable content,
451 int keyCode, KeyEvent event) {
454 case KeyEvent.KEYCODE_DEL:
455 handled = backspace(view, content, keyCode, event);
457 case KeyEvent.KEYCODE_FORWARD_DEL:
458 handled = forwardDelete(view, content, keyCode, event);
466 adjustMetaAfterKeypress(content);
470 return super.onKeyDown(view, content, keyCode, event);
474 * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
475 * the event's text into the content.
477 public boolean onKeyOther(View view, Editable content, KeyEvent event) {
478 if (event.getAction() != KeyEvent.ACTION_MULTIPLE
479 || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
480 // Not something we are interested in.
484 int selectionStart = Selection.getSelectionStart(content);
485 int selectionEnd = Selection.getSelectionEnd(content);
486 if (selectionEnd < selectionStart) {
487 int temp = selectionEnd;
488 selectionEnd = selectionStart;
489 selectionStart = temp;
492 CharSequence text = event.getCharacters();
497 content.replace(selectionStart, selectionEnd, text);