| /* |
| * 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.text.Editable; |
| import android.text.Emoji; |
| import android.text.InputType; |
| import android.text.Layout; |
| import android.text.NoCopySpan; |
| import android.text.Selection; |
| import android.text.Spanned; |
| import android.text.method.TextKeyListener.Capitalize; |
| import android.text.style.ReplacementSpan; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.widget.TextView; |
| |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.text.BreakIterator; |
| |
| /** |
| * 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. |
| * <p></p> |
| * 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 an emoji. |
| final int STATE_BEFORE_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 offset is in emoji tag sequence. |
| final int STATE_IN_TAG_SEQUENCE = 12; |
| |
| // The state machine has been stopped. |
| final int STATE_FINISHED = 13; |
| |
| 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_EMOJI; |
| } else if (codePoint == Emoji.CANCEL_TAG) { |
| state = STATE_IN_TAG_SEQUENCE; |
| } else { |
| state = STATE_FINISHED; |
| } |
| break; |
| case STATE_LF: |
| if (codePoint == CARRIAGE_RETURN) { |
| ++deleteCharCount; |
| } |
| state = STATE_FINISHED; |
| break; |
| 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_EMOJI; |
| break; |
| } |
| |
| if (!isVariationSelector(codePoint) && |
| UCharacter.getCombiningClass(codePoint) == 0) { |
| deleteCharCount += Character.charCount(codePoint); |
| } |
| state = STATE_FINISHED; |
| break; |
| case STATE_BEFORE_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 = Emoji.isEmojiModifier(codePoint) ? |
| STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_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_EMOJI; |
| } else { |
| state = STATE_FINISHED; |
| } |
| break; |
| case STATE_IN_TAG_SEQUENCE: |
| if (Emoji.isTagSpecChar(codePoint)) { |
| deleteCharCount += 2; /* Char count of emoji tag spec character. */ |
| // Keep the same state. |
| } else if (Emoji.isEmoji(codePoint)) { |
| deleteCharCount += Character.charCount(codePoint); |
| state = STATE_FINISHED; |
| } else { |
| // Couldn't find tag_base character. Delete the last tag_term character. |
| deleteCharCount = 2; // for U+E007F |
| state = STATE_FINISHED; |
| } |
| // TODO: Need handle emoji variation selectors. Issue 35224297 |
| 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; |
| } |
| } |