/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.text.method; import android.graphics.Paint; import android.icu.lang.UCharacter; import android.icu.lang.UProperty; import android.view.KeyEvent; import android.view.View; import android.text.*; import android.text.method.TextKeyListener.Capitalize; import android.text.style.ReplacementSpan; import android.widget.TextView; import com.android.internal.annotations.GuardedBy; import java.text.BreakIterator; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; /** * Abstract base class for key listeners. * * Provides a basic foundation for entering and editing text. * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert * characters as keys are pressed. *

* As for all implementations of {@link KeyListener}, this class is only concerned * with hardware keyboards. Software input methods have no obligation to trigger * the methods in this class. */ public abstract class BaseKeyListener extends MetaKeyKeyListener implements KeyListener { /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete(); private static final int LINE_FEED = 0x0A; private static final int CARRIAGE_RETURN = 0x0D; private final Object mLock = new Object(); @GuardedBy("mLock") static Paint sCachedPaint = null; /** * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in * a {@link TextView}. If there is a selection, deletes the selection; otherwise, * deletes the character before the cursor, if any; ALT+DEL deletes everything on * the line the cursor is on. * * @return true if anything was deleted; false otherwise. */ public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) { return backspaceOrForwardDelete(view, content, keyCode, event, false); } /** * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL} * key in a {@link TextView}. If there is a selection, deletes the selection; otherwise, * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on * the line the cursor is on. * * @return true if anything was deleted; false otherwise. */ public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) { return backspaceOrForwardDelete(view, content, keyCode, event, true); } // Returns true if the given code point is a variation selector. private static boolean isVariationSelector(int codepoint) { return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR); } // Returns the offset of the replacement span edge if the offset is inside of the replacement // span. Otherwise, does nothing and returns the input offset value. private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) { if (!(text instanceof Spanned)) { return offset; } ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class); for (int i = 0; i < spans.length; i++) { final int start = ((Spanned) text).getSpanStart(spans[i]); final int end = ((Spanned) text).getSpanEnd(spans[i]); if (start < offset && end > offset) { offset = moveToStart ? start : end; } } return offset; } // Returns the start offset to be deleted by a backspace key from the given offset. private static int getOffsetForBackspaceKey(CharSequence text, int offset) { if (offset <= 1) { return 0; } // Initial state final int STATE_START = 0; // The offset is immediately before line feed. final int STATE_LF = 1; // The offset is immediately before a KEYCAP. final int STATE_BEFORE_KEYCAP = 2; // The offset is immediately before a variation selector and a KEYCAP. final int STATE_BEFORE_VS_AND_KEYCAP = 3; // The offset is immediately before an emoji modifier. final int STATE_BEFORE_EMOJI_MODIFIER = 4; // The offset is immediately before a variation selector and an emoji modifier. final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5; // The offset is immediately before a variation selector. final int STATE_BEFORE_VS = 6; // The offset is immediately before a ZWJ emoji. final int STATE_BEFORE_ZWJ_EMOJI = 7; // The offset is immediately before a ZWJ that were seen before a ZWJ emoji. final int STATE_BEFORE_ZWJ = 8; // The offset is immediately before a variation selector and a ZWJ that were seen before a // ZWJ emoji. final int STATE_BEFORE_VS_AND_ZWJ = 9; // The number of following RIS code points is odd. final int STATE_ODD_NUMBERED_RIS = 10; // The number of following RIS code points is even. final int STATE_EVEN_NUMBERED_RIS = 11; // The state machine has been stopped. final int STATE_FINISHED = 12; int deleteCharCount = 0; // Char count to be deleted by backspace. int lastSeenVSCharCount = 0; // Char count of previous variation selector. int state = STATE_START; int tmpOffset = offset; do { final int codePoint = Character.codePointBefore(text, tmpOffset); tmpOffset -= Character.charCount(codePoint); switch (state) { case STATE_START: deleteCharCount = Character.charCount(codePoint); if (codePoint == LINE_FEED) { state = STATE_LF; } else if (isVariationSelector(codePoint)) { state = STATE_BEFORE_VS; } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) { state = STATE_ODD_NUMBERED_RIS; } else if (Emoji.isEmojiModifier(codePoint)) { state = STATE_BEFORE_EMOJI_MODIFIER; } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) { state = STATE_BEFORE_KEYCAP; } else if (Emoji.isEmoji(codePoint)) { state = STATE_BEFORE_ZWJ_EMOJI; } else { state = STATE_FINISHED; } break; case STATE_LF: if (codePoint == CARRIAGE_RETURN) { ++deleteCharCount; } state = STATE_FINISHED; case STATE_ODD_NUMBERED_RIS: if (Emoji.isRegionalIndicatorSymbol(codePoint)) { deleteCharCount += 2; /* Char count of RIS */ state = STATE_EVEN_NUMBERED_RIS; } else { state = STATE_FINISHED; } break; case STATE_EVEN_NUMBERED_RIS: if (Emoji.isRegionalIndicatorSymbol(codePoint)) { deleteCharCount -= 2; /* Char count of RIS */ state = STATE_ODD_NUMBERED_RIS; } else { state = STATE_FINISHED; } break; case STATE_BEFORE_KEYCAP: if (isVariationSelector(codePoint)) { lastSeenVSCharCount = Character.charCount(codePoint); state = STATE_BEFORE_VS_AND_KEYCAP; break; } if (Emoji.isKeycapBase(codePoint)) { deleteCharCount += Character.charCount(codePoint); } state = STATE_FINISHED; break; case STATE_BEFORE_VS_AND_KEYCAP: if (Emoji.isKeycapBase(codePoint)) { deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); } state = STATE_FINISHED; break; case STATE_BEFORE_EMOJI_MODIFIER: if (isVariationSelector(codePoint)) { lastSeenVSCharCount = Character.charCount(codePoint); state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER; break; } else if (Emoji.isEmojiModifierBase(codePoint)) { deleteCharCount += Character.charCount(codePoint); } state = STATE_FINISHED; break; case STATE_BEFORE_VS_AND_EMOJI_MODIFIER: if (Emoji.isEmojiModifierBase(codePoint)) { deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); } state = STATE_FINISHED; break; case STATE_BEFORE_VS: if (Emoji.isEmoji(codePoint)) { deleteCharCount += Character.charCount(codePoint); state = STATE_BEFORE_ZWJ_EMOJI; break; } if (!isVariationSelector(codePoint) && UCharacter.getCombiningClass(codePoint) == 0) { deleteCharCount += Character.charCount(codePoint); } state = STATE_FINISHED; break; case STATE_BEFORE_ZWJ_EMOJI: if (codePoint == Emoji.ZERO_WIDTH_JOINER) { state = STATE_BEFORE_ZWJ; } else { state = STATE_FINISHED; } break; case STATE_BEFORE_ZWJ: if (Emoji.isEmoji(codePoint)) { deleteCharCount += Character.charCount(codePoint) + 1; // +1 for ZWJ. state = STATE_BEFORE_ZWJ_EMOJI; } else if (isVariationSelector(codePoint)) { lastSeenVSCharCount = Character.charCount(codePoint); state = STATE_BEFORE_VS_AND_ZWJ; } else { state = STATE_FINISHED; } break; case STATE_BEFORE_VS_AND_ZWJ: if (Emoji.isEmoji(codePoint)) { // +1 for ZWJ. deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint); lastSeenVSCharCount = 0; state = STATE_BEFORE_ZWJ_EMOJI; } else { state = STATE_FINISHED; } break; default: throw new IllegalArgumentException("state " + state + " is unknown"); } } while (tmpOffset > 0 && state != STATE_FINISHED); return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */); } // Returns the end offset to be deleted by a forward delete key from the given offset. private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) { final int len = text.length(); if (offset >= len - 1) { return len; } offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */, offset, Paint.CURSOR_AFTER); return adjustReplacementSpan(text, offset, false /* move to the end */); } private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode, KeyEvent event, boolean isForwardDelete) { // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL. if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState() & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) { return false; } // If there is a current selection, delete it. if (deleteSelection(view, content)) { return true; } // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead. boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0); boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1); boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1); if (isCtrlActive) { if (isAltActive || isShiftActive) { // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters. return false; } return deleteUntilWordBoundary(view, content, isForwardDelete); } // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible. if (isAltActive && deleteLine(view, content)) { return true; } // Delete a character. final int start = Selection.getSelectionEnd(content); final int end; if (isForwardDelete) { final Paint paint; if (view instanceof TextView) { paint = ((TextView)view).getPaint(); } else { synchronized (mLock) { if (sCachedPaint == null) { sCachedPaint = new Paint(); } paint = sCachedPaint; } } end = getOffsetForForwardDeleteKey(content, start, paint); } else { end = getOffsetForBackspaceKey(content, start); } if (start != end) { content.delete(Math.min(start, end), Math.max(start, end)); return true; } return false; } private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) { int currentCursorOffset = Selection.getSelectionStart(content); // If there is a selection, do nothing. if (currentCursorOffset != Selection.getSelectionEnd(content)) { return false; } // Early exit if there is no contents to delete. if ((!isForwardDelete && currentCursorOffset == 0) || (isForwardDelete && currentCursorOffset == content.length())) { return false; } WordIterator wordIterator = null; if (view instanceof TextView) { wordIterator = ((TextView)view).getWordIterator(); } if (wordIterator == null) { // Default locale is used for WordIterator since the appropriate locale is not clear // here. // TODO: Use appropriate locale for WordIterator. wordIterator = new WordIterator(); } int deleteFrom; int deleteTo; if (isForwardDelete) { deleteFrom = currentCursorOffset; wordIterator.setCharSequence(content, deleteFrom, content.length()); deleteTo = wordIterator.following(currentCursorOffset); if (deleteTo == BreakIterator.DONE) { deleteTo = content.length(); } } else { deleteTo = currentCursorOffset; wordIterator.setCharSequence(content, 0, deleteTo); deleteFrom = wordIterator.preceding(currentCursorOffset); if (deleteFrom == BreakIterator.DONE) { deleteFrom = 0; } } content.delete(deleteFrom, deleteTo); return true; } private boolean deleteSelection(View view, Editable content) { int selectionStart = Selection.getSelectionStart(content); int selectionEnd = Selection.getSelectionEnd(content); if (selectionEnd < selectionStart) { int temp = selectionEnd; selectionEnd = selectionStart; selectionStart = temp; } if (selectionStart != selectionEnd) { content.delete(selectionStart, selectionEnd); return true; } return false; } private boolean deleteLine(View view, Editable content) { if (view instanceof TextView) { final Layout layout = ((TextView) view).getLayout(); if (layout != null) { final int line = layout.getLineForOffset(Selection.getSelectionStart(content)); final int start = layout.getLineStart(line); final int end = layout.getLineEnd(line); if (end != start) { content.delete(start, end); return true; } } } return false; } static int makeTextContentType(Capitalize caps, boolean autoText) { int contentType = InputType.TYPE_CLASS_TEXT; switch (caps) { case CHARACTERS: contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; break; case WORDS: contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; break; case SENTENCES: contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; break; } if (autoText) { contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; } return contentType; } public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { boolean handled; switch (keyCode) { case KeyEvent.KEYCODE_DEL: handled = backspace(view, content, keyCode, event); break; case KeyEvent.KEYCODE_FORWARD_DEL: handled = forwardDelete(view, content, keyCode, event); break; default: handled = false; break; } if (handled) { adjustMetaAfterKeypress(content); return true; } return super.onKeyDown(view, content, keyCode, event); } /** * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting * the event's text into the content. */ public boolean onKeyOther(View view, Editable content, KeyEvent event) { if (event.getAction() != KeyEvent.ACTION_MULTIPLE || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) { // Not something we are interested in. return false; } int selectionStart = Selection.getSelectionStart(content); int selectionEnd = Selection.getSelectionEnd(content); if (selectionEnd < selectionStart) { int temp = selectionEnd; selectionEnd = selectionStart; selectionStart = temp; } CharSequence text = event.getCharacters(); if (text == null) { return false; } content.replace(selectionStart, selectionEnd, text); return true; } }