BaseKeyListener.java revision bba8d97c369f02a9d1988217324724a24842079f
1/* 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.graphics.Paint; 20import android.icu.lang.UCharacter; 21import android.icu.lang.UProperty; 22import android.view.KeyEvent; 23import android.view.View; 24import android.text.*; 25import android.text.method.TextKeyListener.Capitalize; 26import android.text.style.ReplacementSpan; 27import android.widget.TextView; 28 29import com.android.internal.annotations.GuardedBy; 30 31import java.text.BreakIterator; 32import java.util.Arrays; 33import java.util.Collections; 34import java.util.HashSet; 35 36/** 37 * Abstract base class for key listeners. 38 * 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. 42 * <p></p> 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. 46 */ 47public abstract class BaseKeyListener extends MetaKeyKeyListener 48 implements KeyListener { 49 /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete(); 50 51 private static final int LINE_FEED = 0x0A; 52 private static final int CARRIAGE_RETURN = 0x0D; 53 54 private final Object mLock = new Object(); 55 56 @GuardedBy("mLock") 57 static Paint sCachedPaint = null; 58 59 /** 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. 64 * 65 * @return true if anything was deleted; false otherwise. 66 */ 67 public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) { 68 return backspaceOrForwardDelete(view, content, keyCode, event, false); 69 } 70 71 /** 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. 76 * 77 * @return true if anything was deleted; false otherwise. 78 */ 79 public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) { 80 return backspaceOrForwardDelete(view, content, keyCode, event, true); 81 } 82 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); 86 } 87 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)) { 92 return offset; 93 } 94 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]); 99 100 if (start < offset && end > offset) { 101 offset = moveToStart ? start : end; 102 } 103 } 104 return offset; 105 } 106 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) { 109 if (offset <= 1) { 110 return 0; 111 } 112 113 // Initial state 114 final int STATE_START = 0; 115 116 // The offset is immediately before line feed. 117 final int STATE_LF = 1; 118 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; 123 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; 128 129 // The offset is immediately before a variation selector. 130 final int STATE_BEFORE_VS = 6; 131 132 // The offset is immediately before an emoji. 133 final int STATE_BEFORE_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 137 // ZWJ emoji. 138 final int STATE_BEFORE_VS_AND_ZWJ = 9; 139 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; 144 145 // The state machine has been stopped. 146 final int STATE_FINISHED = 12; 147 148 int deleteCharCount = 0; // Char count to be deleted by backspace. 149 int lastSeenVSCharCount = 0; // Char count of previous variation selector. 150 151 int state = STATE_START; 152 153 int tmpOffset = offset; 154 do { 155 final int codePoint = Character.codePointBefore(text, tmpOffset); 156 tmpOffset -= Character.charCount(codePoint); 157 158 switch (state) { 159 case STATE_START: 160 deleteCharCount = Character.charCount(codePoint); 161 if (codePoint == LINE_FEED) { 162 state = STATE_LF; 163 } else if (isVariationSelector(codePoint)) { 164 state = STATE_BEFORE_VS; 165 } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) { 166 state = STATE_ODD_NUMBERED_RIS; 167 } else if (Emoji.isEmojiModifier(codePoint)) { 168 state = STATE_BEFORE_EMOJI_MODIFIER; 169 } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) { 170 state = STATE_BEFORE_KEYCAP; 171 } else if (Emoji.isEmoji(codePoint)) { 172 state = STATE_BEFORE_EMOJI; 173 } else { 174 state = STATE_FINISHED; 175 } 176 break; 177 case STATE_LF: 178 if (codePoint == CARRIAGE_RETURN) { 179 ++deleteCharCount; 180 } 181 state = STATE_FINISHED; 182 break; 183 case STATE_ODD_NUMBERED_RIS: 184 if (Emoji.isRegionalIndicatorSymbol(codePoint)) { 185 deleteCharCount += 2; /* Char count of RIS */ 186 state = STATE_EVEN_NUMBERED_RIS; 187 } else { 188 state = STATE_FINISHED; 189 } 190 break; 191 case STATE_EVEN_NUMBERED_RIS: 192 if (Emoji.isRegionalIndicatorSymbol(codePoint)) { 193 deleteCharCount -= 2; /* Char count of RIS */ 194 state = STATE_ODD_NUMBERED_RIS; 195 } else { 196 state = STATE_FINISHED; 197 } 198 break; 199 case STATE_BEFORE_KEYCAP: 200 if (isVariationSelector(codePoint)) { 201 lastSeenVSCharCount = Character.charCount(codePoint); 202 state = STATE_BEFORE_VS_AND_KEYCAP; 203 break; 204 } 205 206 if (Emoji.isKeycapBase(codePoint)) { 207 deleteCharCount += Character.charCount(codePoint); 208 } 209 state = STATE_FINISHED; 210 break; 211 case STATE_BEFORE_VS_AND_KEYCAP: 212 if (Emoji.isKeycapBase(codePoint)) { 213 deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); 214 } 215 state = STATE_FINISHED; 216 break; 217 case STATE_BEFORE_EMOJI_MODIFIER: 218 if (isVariationSelector(codePoint)) { 219 lastSeenVSCharCount = Character.charCount(codePoint); 220 state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER; 221 break; 222 } else if (Emoji.isEmojiModifierBase(codePoint)) { 223 deleteCharCount += Character.charCount(codePoint); 224 } 225 state = STATE_FINISHED; 226 break; 227 case STATE_BEFORE_VS_AND_EMOJI_MODIFIER: 228 if (Emoji.isEmojiModifierBase(codePoint)) { 229 deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); 230 } 231 state = STATE_FINISHED; 232 break; 233 case STATE_BEFORE_VS: 234 if (Emoji.isEmoji(codePoint)) { 235 deleteCharCount += Character.charCount(codePoint); 236 state = STATE_BEFORE_EMOJI; 237 break; 238 } 239 240 if (!isVariationSelector(codePoint) && 241 UCharacter.getCombiningClass(codePoint) == 0) { 242 deleteCharCount += Character.charCount(codePoint); 243 } 244 state = STATE_FINISHED; 245 break; 246 case STATE_BEFORE_EMOJI: 247 if (codePoint == Emoji.ZERO_WIDTH_JOINER) { 248 state = STATE_BEFORE_ZWJ; 249 } else { 250 state = STATE_FINISHED; 251 } 252 break; 253 case STATE_BEFORE_ZWJ: 254 if (Emoji.isEmoji(codePoint)) { 255 deleteCharCount += Character.charCount(codePoint) + 1; // +1 for ZWJ. 256 state = Emoji.isEmojiModifier(codePoint) ? 257 STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI; 258 } else if (isVariationSelector(codePoint)) { 259 lastSeenVSCharCount = Character.charCount(codePoint); 260 state = STATE_BEFORE_VS_AND_ZWJ; 261 } else { 262 state = STATE_FINISHED; 263 } 264 break; 265 case STATE_BEFORE_VS_AND_ZWJ: 266 if (Emoji.isEmoji(codePoint)) { 267 // +1 for ZWJ. 268 deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint); 269 lastSeenVSCharCount = 0; 270 state = STATE_BEFORE_EMOJI; 271 } else { 272 state = STATE_FINISHED; 273 } 274 break; 275 default: 276 throw new IllegalArgumentException("state " + state + " is unknown"); 277 } 278 } while (tmpOffset > 0 && state != STATE_FINISHED); 279 280 return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */); 281 } 282 283 // Returns the end offset to be deleted by a forward delete key from the given offset. 284 private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) { 285 final int len = text.length(); 286 287 if (offset >= len - 1) { 288 return len; 289 } 290 291 offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */, 292 offset, Paint.CURSOR_AFTER); 293 294 return adjustReplacementSpan(text, offset, false /* move to the end */); 295 } 296 297 private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode, 298 KeyEvent event, boolean isForwardDelete) { 299 // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL. 300 if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState() 301 & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) { 302 return false; 303 } 304 305 // If there is a current selection, delete it. 306 if (deleteSelection(view, content)) { 307 return true; 308 } 309 310 // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead. 311 boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0); 312 boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1); 313 boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1); 314 315 if (isCtrlActive) { 316 if (isAltActive || isShiftActive) { 317 // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters. 318 return false; 319 } 320 return deleteUntilWordBoundary(view, content, isForwardDelete); 321 } 322 323 // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible. 324 if (isAltActive && deleteLine(view, content)) { 325 return true; 326 } 327 328 // Delete a character. 329 final int start = Selection.getSelectionEnd(content); 330 final int end; 331 if (isForwardDelete) { 332 final Paint paint; 333 if (view instanceof TextView) { 334 paint = ((TextView)view).getPaint(); 335 } else { 336 synchronized (mLock) { 337 if (sCachedPaint == null) { 338 sCachedPaint = new Paint(); 339 } 340 paint = sCachedPaint; 341 } 342 } 343 end = getOffsetForForwardDeleteKey(content, start, paint); 344 } else { 345 end = getOffsetForBackspaceKey(content, start); 346 } 347 if (start != end) { 348 content.delete(Math.min(start, end), Math.max(start, end)); 349 return true; 350 } 351 return false; 352 } 353 354 private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) { 355 int currentCursorOffset = Selection.getSelectionStart(content); 356 357 // If there is a selection, do nothing. 358 if (currentCursorOffset != Selection.getSelectionEnd(content)) { 359 return false; 360 } 361 362 // Early exit if there is no contents to delete. 363 if ((!isForwardDelete && currentCursorOffset == 0) || 364 (isForwardDelete && currentCursorOffset == content.length())) { 365 return false; 366 } 367 368 WordIterator wordIterator = null; 369 if (view instanceof TextView) { 370 wordIterator = ((TextView)view).getWordIterator(); 371 } 372 373 if (wordIterator == null) { 374 // Default locale is used for WordIterator since the appropriate locale is not clear 375 // here. 376 // TODO: Use appropriate locale for WordIterator. 377 wordIterator = new WordIterator(); 378 } 379 380 int deleteFrom; 381 int deleteTo; 382 383 if (isForwardDelete) { 384 deleteFrom = currentCursorOffset; 385 wordIterator.setCharSequence(content, deleteFrom, content.length()); 386 deleteTo = wordIterator.following(currentCursorOffset); 387 if (deleteTo == BreakIterator.DONE) { 388 deleteTo = content.length(); 389 } 390 } else { 391 deleteTo = currentCursorOffset; 392 wordIterator.setCharSequence(content, 0, deleteTo); 393 deleteFrom = wordIterator.preceding(currentCursorOffset); 394 if (deleteFrom == BreakIterator.DONE) { 395 deleteFrom = 0; 396 } 397 } 398 content.delete(deleteFrom, deleteTo); 399 return true; 400 } 401 402 private boolean deleteSelection(View view, Editable content) { 403 int selectionStart = Selection.getSelectionStart(content); 404 int selectionEnd = Selection.getSelectionEnd(content); 405 if (selectionEnd < selectionStart) { 406 int temp = selectionEnd; 407 selectionEnd = selectionStart; 408 selectionStart = temp; 409 } 410 if (selectionStart != selectionEnd) { 411 content.delete(selectionStart, selectionEnd); 412 return true; 413 } 414 return false; 415 } 416 417 private boolean deleteLine(View view, Editable content) { 418 if (view instanceof TextView) { 419 final Layout layout = ((TextView) view).getLayout(); 420 if (layout != null) { 421 final int line = layout.getLineForOffset(Selection.getSelectionStart(content)); 422 final int start = layout.getLineStart(line); 423 final int end = layout.getLineEnd(line); 424 if (end != start) { 425 content.delete(start, end); 426 return true; 427 } 428 } 429 } 430 return false; 431 } 432 433 static int makeTextContentType(Capitalize caps, boolean autoText) { 434 int contentType = InputType.TYPE_CLASS_TEXT; 435 switch (caps) { 436 case CHARACTERS: 437 contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; 438 break; 439 case WORDS: 440 contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; 441 break; 442 case SENTENCES: 443 contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; 444 break; 445 } 446 if (autoText) { 447 contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; 448 } 449 return contentType; 450 } 451 452 public boolean onKeyDown(View view, Editable content, 453 int keyCode, KeyEvent event) { 454 boolean handled; 455 switch (keyCode) { 456 case KeyEvent.KEYCODE_DEL: 457 handled = backspace(view, content, keyCode, event); 458 break; 459 case KeyEvent.KEYCODE_FORWARD_DEL: 460 handled = forwardDelete(view, content, keyCode, event); 461 break; 462 default: 463 handled = false; 464 break; 465 } 466 467 if (handled) { 468 adjustMetaAfterKeypress(content); 469 return true; 470 } 471 472 return super.onKeyDown(view, content, keyCode, event); 473 } 474 475 /** 476 * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting 477 * the event's text into the content. 478 */ 479 public boolean onKeyOther(View view, Editable content, KeyEvent event) { 480 if (event.getAction() != KeyEvent.ACTION_MULTIPLE 481 || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) { 482 // Not something we are interested in. 483 return false; 484 } 485 486 int selectionStart = Selection.getSelectionStart(content); 487 int selectionEnd = Selection.getSelectionEnd(content); 488 if (selectionEnd < selectionStart) { 489 int temp = selectionEnd; 490 selectionEnd = selectionStart; 491 selectionStart = temp; 492 } 493 494 CharSequence text = event.getCharacters(); 495 if (text == null) { 496 return false; 497 } 498 499 content.replace(selectionStart, selectionEnd, text); 500 return true; 501 } 502} 503