WebTextView.java revision b5ce0e05327b4b76da49d2bd3944e66c38c8cb69
1/* 2 * Copyright (C) 2007 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.webkit; 18 19import com.android.internal.widget.EditableInputConnection; 20 21import android.content.Context; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.ColorFilter; 25import android.graphics.Paint; 26import android.graphics.PixelFormat; 27import android.graphics.Rect; 28import android.graphics.drawable.Drawable; 29import android.text.Editable; 30import android.text.InputFilter; 31import android.text.Layout; 32import android.text.Selection; 33import android.text.Spannable; 34import android.text.TextPaint; 35import android.text.TextUtils; 36import android.text.method.MovementMethod; 37import android.text.method.Touch; 38import android.util.Log; 39import android.view.Gravity; 40import android.view.KeyCharacterMap; 41import android.view.KeyEvent; 42import android.view.MotionEvent; 43import android.view.View; 44import android.view.ViewConfiguration; 45import android.view.ViewGroup; 46import android.view.inputmethod.EditorInfo; 47import android.view.inputmethod.InputMethodManager; 48import android.view.inputmethod.InputConnection; 49import android.widget.AbsoluteLayout.LayoutParams; 50import android.widget.AdapterView; 51import android.widget.ArrayAdapter; 52import android.widget.AutoCompleteTextView; 53import android.widget.TextView; 54 55import java.util.ArrayList; 56 57/** 58 * WebTextView is a specialized version of EditText used by WebView 59 * to overlay html textfields (and textareas) to use our standard 60 * text editing. 61 */ 62/* package */ class WebTextView extends AutoCompleteTextView { 63 64 static final String LOGTAG = "webtextview"; 65 66 private WebView mWebView; 67 private boolean mSingle; 68 private int mWidthSpec; 69 private int mHeightSpec; 70 private int mNodePointer; 71 // FIXME: This is a hack for blocking unmatched key ups, in particular 72 // on the enter key. The method for blocking unmatched key ups prevents 73 // the shift key from working properly. 74 private boolean mGotEnterDown; 75 private int mMaxLength; 76 // Keep track of the text before the change so we know whether we actually 77 // need to send down the DOM events. 78 private String mPreChange; 79 private Drawable mBackground; 80 // Variables for keeping track of the touch down, to send to the WebView 81 // when a drag starts 82 private float mDragStartX; 83 private float mDragStartY; 84 private long mDragStartTime; 85 private boolean mDragSent; 86 // True if the most recent drag event has caused either the TextView to 87 // scroll or the web page to scroll. Gets reset after a touch down. 88 private boolean mScrolled; 89 // Whether or not a selection change was generated from webkit. If it was, 90 // we do not need to pass the selection back to webkit. 91 private boolean mFromWebKit; 92 // Whether or not a selection change was generated from the WebTextView 93 // gaining focus. If it is, we do not want to pass it to webkit. This 94 // selection comes from the MovementMethod, but we behave differently. If 95 // WebTextView gained focus from a touch, webkit will determine the 96 // selection. 97 private boolean mFromFocusChange; 98 // Whether or not a selection change was generated from setInputType. We 99 // do not want to pass this change to webkit. 100 private boolean mFromSetInputType; 101 private boolean mGotTouchDown; 102 // Keep track of whether a long press has happened. Only meaningful after 103 // an ACTION_DOWN MotionEvent 104 private boolean mHasPerformedLongClick; 105 private boolean mInSetTextAndKeepSelection; 106 // Array to store the final character added in onTextChanged, so that its 107 // KeyEvents may be determined. 108 private char[] mCharacter = new char[1]; 109 // This is used to reset the length filter when on a textfield 110 // with no max length. 111 // FIXME: This can be replaced with TextView.NO_FILTERS if that 112 // is made public/protected. 113 private static final InputFilter[] NO_FILTERS = new InputFilter[0]; 114 // For keeping track of the fact that the delete key was pressed, so 115 // we can simply pass a delete key instead of calling deleteSelection. 116 private boolean mGotDelete; 117 private int mDelSelStart; 118 private int mDelSelEnd; 119 120 // Keep in sync with native constant in 121 // external/webkit/WebKit/android/WebCoreSupport/autofill/WebAutoFill.cpp 122 /* package */ static final int FORM_NOT_AUTOFILLABLE = -1; 123 124 private boolean mAutoFillable; // Is this textview part of an autofillable form? 125 private int mQueryId; 126 127 // Types used with setType. Keep in sync with CachedInput.h 128 private static final int NORMAL_TEXT_FIELD = 0; 129 private static final int TEXT_AREA = 1; 130 private static final int PASSWORD = 2; 131 private static final int SEARCH = 3; 132 private static final int EMAIL = 4; 133 private static final int NUMBER = 5; 134 private static final int TELEPHONE = 6; 135 private static final int URL = 7; 136 137 /** 138 * Create a new WebTextView. 139 * @param context The Context for this WebTextView. 140 * @param webView The WebView that created this. 141 */ 142 /* package */ WebTextView(Context context, WebView webView, int autoFillQueryId) { 143 super(context, null, com.android.internal.R.attr.webTextViewStyle); 144 mWebView = webView; 145 mMaxLength = -1; 146 setAutoFillable(autoFillQueryId); 147 } 148 149 public void setAutoFillable(int queryId) { 150 mAutoFillable = mWebView.getSettings().getAutoFillEnabled() 151 && (queryId != FORM_NOT_AUTOFILLABLE); 152 mQueryId = queryId; 153 } 154 155 @Override 156 public boolean dispatchKeyEvent(KeyEvent event) { 157 if (event.isSystem()) { 158 return super.dispatchKeyEvent(event); 159 } 160 // Treat ACTION_DOWN and ACTION MULTIPLE the same 161 boolean down = event.getAction() != KeyEvent.ACTION_UP; 162 int keyCode = event.getKeyCode(); 163 164 boolean isArrowKey = false; 165 switch(keyCode) { 166 case KeyEvent.KEYCODE_DPAD_LEFT: 167 case KeyEvent.KEYCODE_DPAD_RIGHT: 168 case KeyEvent.KEYCODE_DPAD_UP: 169 case KeyEvent.KEYCODE_DPAD_DOWN: 170 if (!mWebView.nativeCursorMatchesFocus()) { 171 return down ? mWebView.onKeyDown(keyCode, event) : mWebView 172 .onKeyUp(keyCode, event); 173 174 } 175 isArrowKey = true; 176 break; 177 } 178 179 if (KeyEvent.KEYCODE_TAB == keyCode) { 180 if (down) { 181 onEditorAction(EditorInfo.IME_ACTION_NEXT); 182 } 183 return true; 184 } 185 Spannable text = (Spannable) getText(); 186 int oldStart = Selection.getSelectionStart(text); 187 int oldEnd = Selection.getSelectionEnd(text); 188 // Normally the delete key's dom events are sent via onTextChanged. 189 // However, if the cursor is at the beginning of the field, which 190 // includes the case where it has zero length, then the text is not 191 // changed, so send the events immediately. 192 if (KeyEvent.KEYCODE_DEL == keyCode) { 193 if (oldStart == 0 && oldEnd == 0) { 194 sendDomEvent(event); 195 return true; 196 } 197 if (down) { 198 mGotDelete = true; 199 mDelSelStart = oldStart; 200 mDelSelEnd = oldEnd; 201 } 202 } 203 204 if ((mSingle && KeyEvent.KEYCODE_ENTER == keyCode)) { 205 if (isPopupShowing()) { 206 return super.dispatchKeyEvent(event); 207 } 208 if (!down) { 209 // Hide the keyboard, since the user has just submitted this 210 // form. The submission happens thanks to the two calls 211 // to sendDomEvent. 212 InputMethodManager.getInstance(mContext) 213 .hideSoftInputFromWindow(getWindowToken(), 0); 214 sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); 215 sendDomEvent(event); 216 } 217 return super.dispatchKeyEvent(event); 218 } else if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { 219 // Note that this handles center key and trackball. 220 if (isPopupShowing()) { 221 return super.dispatchKeyEvent(event); 222 } 223 if (!mWebView.nativeCursorMatchesFocus()) { 224 return down ? mWebView.onKeyDown(keyCode, event) : mWebView 225 .onKeyUp(keyCode, event); 226 } 227 // Center key should be passed to a potential onClick 228 if (!down) { 229 mWebView.centerKeyPressOnTextField(); 230 } 231 // Pass to super to handle longpress. 232 return super.dispatchKeyEvent(event); 233 } 234 235 // Ensure there is a layout so arrow keys are handled properly. 236 if (getLayout() == null) { 237 measure(mWidthSpec, mHeightSpec); 238 } 239 240 int oldLength = text.length(); 241 boolean maxedOut = mMaxLength != -1 && oldLength == mMaxLength; 242 // If we are at max length, and there is a selection rather than a 243 // cursor, we need to store the text to compare later, since the key 244 // may have changed the string. 245 String oldText; 246 if (maxedOut && oldEnd != oldStart) { 247 oldText = text.toString(); 248 } else { 249 oldText = ""; 250 } 251 if (super.dispatchKeyEvent(event)) { 252 // If the WebTextView handled the key it was either an alphanumeric 253 // key, a delete, or a movement within the text. All of those are 254 // ok to pass to javascript. 255 256 // UNLESS there is a max length determined by the html. In that 257 // case, if the string was already at the max length, an 258 // alphanumeric key will be erased by the LengthFilter, 259 // so do not pass down to javascript, and instead 260 // return true. If it is an arrow key or a delete key, we can go 261 // ahead and pass it down. 262 if (KeyEvent.KEYCODE_ENTER == keyCode) { 263 // For multi-line text boxes, newlines will 264 // trigger onTextChanged for key down (which will send both 265 // key up and key down) but not key up. 266 mGotEnterDown = true; 267 } 268 if (maxedOut && !isArrowKey && keyCode != KeyEvent.KEYCODE_DEL) { 269 if (oldEnd == oldStart) { 270 // Return true so the key gets dropped. 271 return true; 272 } else if (!oldText.equals(getText().toString())) { 273 // FIXME: This makes the text work properly, but it 274 // does not pass down the key event, so it may not 275 // work for a textfield that has the type of 276 // behavior of GoogleSuggest. That said, it is 277 // unlikely that a site would combine the two in 278 // one textfield. 279 Spannable span = (Spannable) getText(); 280 int newStart = Selection.getSelectionStart(span); 281 int newEnd = Selection.getSelectionEnd(span); 282 mWebView.replaceTextfieldText(0, oldLength, span.toString(), 283 newStart, newEnd); 284 return true; 285 } 286 } 287 /* FIXME: 288 * In theory, we would like to send the events for the arrow keys. 289 * However, the TextView can arbitrarily change the selection (i.e. 290 * long press followed by using the trackball). Therefore, we keep 291 * in sync with the TextView via onSelectionChanged. If we also 292 * send the DOM event, we lose the correct selection. 293 if (isArrowKey) { 294 // Arrow key does not change the text, but we still want to send 295 // the DOM events. 296 sendDomEvent(event); 297 } 298 */ 299 return true; 300 } 301 // Ignore the key up event for newlines. This prevents 302 // multiple newlines in the native textarea. 303 if (mGotEnterDown && !down) { 304 return true; 305 } 306 // if it is a navigation key, pass it to WebView 307 if (isArrowKey) { 308 // WebView check the trackballtime in onKeyDown to avoid calling 309 // native from both trackball and key handling. As this is called 310 // from WebTextView, we always want WebView to check with native. 311 // Reset trackballtime to ensure it. 312 mWebView.resetTrackballTime(); 313 return down ? mWebView.onKeyDown(keyCode, event) : mWebView 314 .onKeyUp(keyCode, event); 315 } 316 return false; 317 } 318 319 /** 320 * Determine whether this WebTextView currently represents the node 321 * represented by ptr. 322 * @param ptr Pointer to a node to compare to. 323 * @return boolean Whether this WebTextView already represents the node 324 * pointed to by ptr. 325 */ 326 /* package */ boolean isSameTextField(int ptr) { 327 return ptr == mNodePointer; 328 } 329 330 /** 331 * Ensure that the underlying textfield is lined up with the WebTextView. 332 */ 333 private void lineUpScroll() { 334 Layout layout = getLayout(); 335 if (mWebView != null && layout != null) { 336 float maxScrollX = Touch.getMaxScrollX(this, layout, mScrollY); 337 if (DebugFlags.WEB_TEXT_VIEW) { 338 Log.v(LOGTAG, "onTouchEvent x=" + mScrollX + " y=" 339 + mScrollY + " maxX=" + maxScrollX); 340 } 341 mWebView.scrollFocusedTextInput(maxScrollX > 0 ? 342 mScrollX / maxScrollX : 0, mScrollY); 343 } 344 } 345 346 @Override public InputConnection onCreateInputConnection( 347 EditorInfo outAttrs) { 348 InputConnection connection = super.onCreateInputConnection(outAttrs); 349 if (mWebView != null) { 350 // Use the name of the textfield + the url. Use backslash as an 351 // arbitrary separator. 352 outAttrs.fieldName = mWebView.nativeFocusCandidateName() + "\\" 353 + mWebView.getUrl(); 354 } 355 return connection; 356 } 357 358 /** 359 * In general, TextView makes a call to InputMethodManager.updateSelection 360 * in onDraw. However, in the general case of WebTextView, we do not draw. 361 * This method is called by WebView.onDraw to take care of the part that 362 * needs to be called. 363 */ 364 /* package */ void onDrawSubstitute() { 365 if (!willNotDraw()) { 366 // If the WebTextView is set to draw, such as in the case of a 367 // password, onDraw calls updateSelection(), so this code path is 368 // unnecessary. 369 return; 370 } 371 // This code is copied from TextView.onDraw(). That code does not get 372 // executed, however, because the WebTextView does not draw, allowing 373 // webkit's drawing to show through. 374 InputMethodManager imm = InputMethodManager.peekInstance(); 375 if (imm != null && imm.isActive(this)) { 376 Spannable sp = (Spannable) getText(); 377 int selStart = Selection.getSelectionStart(sp); 378 int selEnd = Selection.getSelectionEnd(sp); 379 int candStart = EditableInputConnection.getComposingSpanStart(sp); 380 int candEnd = EditableInputConnection.getComposingSpanEnd(sp); 381 imm.updateSelection(this, selStart, selEnd, candStart, candEnd); 382 } 383 updateCursorControllerPositions(); 384 } 385 386 @Override 387 protected void onDraw(Canvas canvas) { 388 // onDraw should only be called for password fields. If WebTextView is 389 // still drawing, but is no longer corresponding to a password field, 390 // remove it. 391 if (mWebView == null || !mWebView.nativeFocusCandidateIsPassword() 392 || !isSameTextField(mWebView.nativeFocusCandidatePointer())) { 393 // Although calling remove() would seem to make more sense here, 394 // changing it to not be a password field will make it not draw. 395 // Other code will make sure that it is removed completely, but this 396 // way the user will not see it. 397 setInPassword(false); 398 } else { 399 super.onDraw(canvas); 400 } 401 } 402 403 @Override 404 public void onEditorAction(int actionCode) { 405 switch (actionCode) { 406 case EditorInfo.IME_ACTION_NEXT: 407 if (mWebView.nativeMoveCursorToNextTextInput()) { 408 // Preemptively rebuild the WebTextView, so that the action will 409 // be set properly. 410 mWebView.rebuildWebTextView(); 411 setDefaultSelection(); 412 mWebView.invalidate(); 413 } 414 break; 415 case EditorInfo.IME_ACTION_DONE: 416 super.onEditorAction(actionCode); 417 break; 418 case EditorInfo.IME_ACTION_GO: 419 case EditorInfo.IME_ACTION_SEARCH: 420 // Send an enter and hide the soft keyboard 421 InputMethodManager.getInstance(mContext) 422 .hideSoftInputFromWindow(getWindowToken(), 0); 423 sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, 424 KeyEvent.KEYCODE_ENTER)); 425 sendDomEvent(new KeyEvent(KeyEvent.ACTION_UP, 426 KeyEvent.KEYCODE_ENTER)); 427 428 default: 429 break; 430 } 431 } 432 433 @Override 434 protected void onFocusChanged(boolean focused, int direction, 435 Rect previouslyFocusedRect) { 436 mFromFocusChange = true; 437 super.onFocusChanged(focused, direction, previouslyFocusedRect); 438 mFromFocusChange = false; 439 } 440 441 @Override 442 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 443 super.onScrollChanged(l, t, oldl, oldt); 444 lineUpScroll(); 445 } 446 447 @Override 448 protected void onSelectionChanged(int selStart, int selEnd) { 449 if (!mFromWebKit && !mFromFocusChange && !mFromSetInputType 450 && mWebView != null && !mInSetTextAndKeepSelection) { 451 if (DebugFlags.WEB_TEXT_VIEW) { 452 Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart 453 + " selEnd=" + selEnd); 454 } 455 mWebView.setSelection(selStart, selEnd); 456 lineUpScroll(); 457 } 458 } 459 460 @Override 461 protected void onTextChanged(CharSequence s,int start,int before,int count){ 462 super.onTextChanged(s, start, before, count); 463 String postChange = s.toString(); 464 // Prevent calls to setText from invoking onTextChanged (since this will 465 // mean we are on a different textfield). Also prevent the change when 466 // going from a textfield with a string of text to one with a smaller 467 // limit on text length from registering the onTextChanged event. 468 if (mPreChange == null || mPreChange.equals(postChange) || 469 (mMaxLength > -1 && mPreChange.length() > mMaxLength && 470 mPreChange.substring(0, mMaxLength).equals(postChange))) { 471 return; 472 } 473 mPreChange = postChange; 474 if (0 == count) { 475 if (before > 0) { 476 // For this and all changes to the text, update our cache 477 updateCachedTextfield(); 478 if (mGotDelete) { 479 mGotDelete = false; 480 int oldEnd = start + before; 481 if (mDelSelEnd == oldEnd 482 && (mDelSelStart == start 483 || (mDelSelStart == oldEnd && before == 1))) { 484 // If the selection is set up properly before the 485 // delete, send the DOM events. 486 sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, 487 KeyEvent.KEYCODE_DEL)); 488 sendDomEvent(new KeyEvent(KeyEvent.ACTION_UP, 489 KeyEvent.KEYCODE_DEL)); 490 return; 491 } 492 } 493 // This was simply a delete or a cut, so just delete the 494 // selection. 495 mWebView.deleteSelection(start, start + before); 496 } 497 mGotDelete = false; 498 // before should never be negative, so whether it was a cut 499 // (handled above), or before is 0, in which case nothing has 500 // changed, we should return. 501 return; 502 } 503 // Ensure that this flag gets cleared, since with autocorrect on, a 504 // delete key press may have a more complex result than deleting one 505 // character or the existing selection, so it will not get cleared 506 // above. 507 mGotDelete = false; 508 // Find the last character being replaced. If it can be represented by 509 // events, we will pass them to native (after replacing the beginning 510 // of the changed text), so we can see javascript events. 511 // Otherwise, replace the text being changed (including the last 512 // character) in the textfield. 513 TextUtils.getChars(s, start + count - 1, start + count, mCharacter, 0); 514 KeyCharacterMap kmap = 515 KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); 516 KeyEvent[] events = kmap.getEvents(mCharacter); 517 boolean cannotUseKeyEvents = null == events; 518 int charactersFromKeyEvents = cannotUseKeyEvents ? 0 : 1; 519 if (count > 1 || cannotUseKeyEvents) { 520 String replace = s.subSequence(start, 521 start + count - charactersFromKeyEvents).toString(); 522 mWebView.replaceTextfieldText(start, start + before, replace, 523 start + count - charactersFromKeyEvents, 524 start + count - charactersFromKeyEvents); 525 } else { 526 // This corrects the selection which may have been affected by the 527 // trackball or auto-correct. 528 if (DebugFlags.WEB_TEXT_VIEW) { 529 Log.v(LOGTAG, "onTextChanged start=" + start 530 + " start + before=" + (start + before)); 531 } 532 if (!mInSetTextAndKeepSelection) { 533 mWebView.setSelection(start, start + before); 534 } 535 } 536 if (!cannotUseKeyEvents) { 537 int length = events.length; 538 for (int i = 0; i < length; i++) { 539 // We never send modifier keys to native code so don't send them 540 // here either. 541 if (!KeyEvent.isModifierKey(events[i].getKeyCode())) { 542 sendDomEvent(events[i]); 543 } 544 } 545 } 546 updateCachedTextfield(); 547 } 548 549 @Override 550 public boolean onTouchEvent(MotionEvent event) { 551 switch (event.getAction()) { 552 case MotionEvent.ACTION_DOWN: 553 super.onTouchEvent(event); 554 // This event may be the start of a drag, so store it to pass to the 555 // WebView if it is. 556 mDragStartX = event.getX(); 557 mDragStartY = event.getY(); 558 mDragStartTime = event.getEventTime(); 559 mDragSent = false; 560 mScrolled = false; 561 mGotTouchDown = true; 562 mHasPerformedLongClick = false; 563 break; 564 case MotionEvent.ACTION_MOVE: 565 if (mHasPerformedLongClick) { 566 mGotTouchDown = false; 567 return false; 568 } 569 int slop = ViewConfiguration.get(mContext).getScaledTouchSlop(); 570 Spannable buffer = getText(); 571 int initialScrollX = Touch.getInitialScrollX(this, buffer); 572 int initialScrollY = Touch.getInitialScrollY(this, buffer); 573 super.onTouchEvent(event); 574 int dx = Math.abs(mScrollX - initialScrollX); 575 int dy = Math.abs(mScrollY - initialScrollY); 576 // Use a smaller slop when checking to see if we've moved far enough 577 // to scroll the text, because experimentally, slop has shown to be 578 // to big for the case of a small textfield. 579 int smallerSlop = slop/2; 580 if (dx > smallerSlop || dy > smallerSlop) { 581 // Scrolling is handled in onScrollChanged. 582 mScrolled = true; 583 cancelLongPress(); 584 return true; 585 } 586 if (Math.abs((int) event.getX() - mDragStartX) < slop 587 && Math.abs((int) event.getY() - mDragStartY) < slop) { 588 // If the user has not scrolled further than slop, we should not 589 // send the drag. Instead, do nothing, and when the user lifts 590 // their finger, we will change the selection. 591 return true; 592 } 593 if (mWebView != null) { 594 // Only want to set the initial state once. 595 if (!mDragSent) { 596 mWebView.initiateTextFieldDrag(mDragStartX, mDragStartY, 597 mDragStartTime); 598 mDragSent = true; 599 } 600 boolean scrolled = mWebView.textFieldDrag(event); 601 if (scrolled) { 602 mScrolled = true; 603 cancelLongPress(); 604 return true; 605 } 606 } 607 return false; 608 case MotionEvent.ACTION_UP: 609 case MotionEvent.ACTION_CANCEL: 610 super.onTouchEvent(event); 611 if (mHasPerformedLongClick) { 612 mGotTouchDown = false; 613 return false; 614 } 615 if (!mScrolled) { 616 // If the page scrolled, or the TextView scrolled, we do not 617 // want to change the selection 618 cancelLongPress(); 619 if (mGotTouchDown && mWebView != null) { 620 mWebView.touchUpOnTextField(event); 621 } 622 } 623 // Necessary for the WebView to reset its state 624 if (mWebView != null && mDragSent) { 625 mWebView.onTouchEvent(event); 626 } 627 mGotTouchDown = false; 628 break; 629 default: 630 break; 631 } 632 return true; 633 } 634 635 @Override 636 public boolean onTrackballEvent(MotionEvent event) { 637 if (isPopupShowing()) { 638 return super.onTrackballEvent(event); 639 } 640 if (event.getAction() != MotionEvent.ACTION_MOVE) { 641 return false; 642 } 643 // If the Cursor is not on the text input, webview should handle the 644 // trackball 645 if (!mWebView.nativeCursorMatchesFocus()) { 646 return mWebView.onTrackballEvent(event); 647 } 648 Spannable text = (Spannable) getText(); 649 MovementMethod move = getMovementMethod(); 650 if (move != null && getLayout() != null && 651 move.onTrackballEvent(this, text, event)) { 652 // Selection is changed in onSelectionChanged 653 return true; 654 } 655 return false; 656 } 657 658 @Override 659 public boolean performLongClick() { 660 mHasPerformedLongClick = true; 661 return super.performLongClick(); 662 } 663 664 /** 665 * Remove this WebTextView from its host WebView, and return 666 * focus to the host. 667 */ 668 /* package */ void remove() { 669 // hide the soft keyboard when the edit text is out of focus 670 InputMethodManager imm = InputMethodManager.getInstance(mContext); 671 if (imm.isActive(this)) { 672 imm.hideSoftInputFromWindow(getWindowToken(), 0); 673 } 674 mWebView.removeView(this); 675 mWebView.requestFocus(); 676 } 677 678 /** 679 * Move the caret/selection into view. 680 */ 681 /* package */ void bringIntoView() { 682 bringPointIntoView(Selection.getSelectionEnd(getText())); 683 } 684 685 @Override 686 public boolean bringPointIntoView(int offset) { 687 if (mWebView == null) return false; 688 if (mWebView.nativeFocusCandidateIsPassword()) { 689 return getLayout() != null && super.bringPointIntoView(offset); 690 } 691 // For non password text input, tell webkit to move the caret/selection 692 // on screen, since webkit draws them. 693 mWebView.revealSelection(); 694 return true; 695 } 696 697 /** 698 * Send the DOM events for the specified event. 699 * @param event KeyEvent to be translated into a DOM event. 700 */ 701 private void sendDomEvent(KeyEvent event) { 702 mWebView.passToJavaScript(getText().toString(), event); 703 } 704 705 /** 706 * Always use this instead of setAdapter, as this has features specific to 707 * the WebTextView. 708 */ 709 public void setAdapterCustom(AutoCompleteAdapter adapter) { 710 if (adapter != null) { 711 setInputType(getInputType() 712 | EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE); 713 adapter.setTextView(this); 714 } 715 super.setAdapter(adapter); 716 if (mAutoFillable) { 717 setOnItemClickListener(new AdapterView.OnItemClickListener() { 718 @Override 719 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 720 if (id == 0 && position == 0) { 721 // Blank out the text box while we wait for WebCore to fill the form. 722 replaceText(""); 723 // Call a webview method to tell WebCore to autofill the form. 724 mWebView.autoFillForm(mQueryId); 725 } 726 } 727 }); 728 } else { 729 setOnItemClickListener(null); 730 } 731 showDropDown(); 732 } 733 734 /** 735 * This is a special version of ArrayAdapter which changes its text size 736 * to match the text size of its host TextView. 737 */ 738 public static class AutoCompleteAdapter extends ArrayAdapter<String> { 739 private TextView mTextView; 740 741 public AutoCompleteAdapter(Context context, ArrayList<String> entries) { 742 super(context, com.android.internal.R.layout 743 .search_dropdown_item_1line, entries); 744 } 745 746 /** 747 * {@inheritDoc} 748 */ 749 public View getView(int position, View convertView, ViewGroup parent) { 750 TextView tv = 751 (TextView) super.getView(position, convertView, parent); 752 if (tv != null && mTextView != null) { 753 tv.setTextSize(mTextView.getTextSize()); 754 } 755 return tv; 756 } 757 758 /** 759 * Set the TextView so we can match its text size. 760 */ 761 private void setTextView(TextView tv) { 762 mTextView = tv; 763 } 764 } 765 766 /** 767 * Sets the selection when the user clicks on a textfield or textarea with 768 * the trackball or center key, or starts typing into it without clicking on 769 * it. 770 */ 771 /* package */ void setDefaultSelection() { 772 Spannable text = (Spannable) getText(); 773 int selection = mSingle ? text.length() : 0; 774 if (Selection.getSelectionStart(text) == selection 775 && Selection.getSelectionEnd(text) == selection) { 776 // The selection of the UI copy is set correctly, but the 777 // WebTextView still needs to inform the webkit thread to set the 778 // selection. Normally that is done in onSelectionChanged, but 779 // onSelectionChanged will not be called because the UI copy is not 780 // changing. (This can happen when the WebTextView takes focus. 781 // That onSelectionChanged was blocked because the selection set 782 // when focusing is not necessarily the desirable selection for 783 // WebTextView.) 784 if (mWebView != null) { 785 mWebView.setSelection(selection, selection); 786 } 787 } else { 788 Selection.setSelection(text, selection, selection); 789 } 790 if (mWebView != null) mWebView.incrementTextGeneration(); 791 } 792 793 /** 794 * Determine whether to use the system-wide password disguising method, 795 * or to use none. 796 * @param inPassword True if the textfield is a password field. 797 */ 798 /* package */ void setInPassword(boolean inPassword) { 799 if (inPassword) { 800 setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo. 801 TYPE_TEXT_VARIATION_WEB_PASSWORD); 802 createBackground(); 803 } 804 // For password fields, draw the WebTextView. For others, just show 805 // webkit's drawing. 806 setWillNotDraw(!inPassword); 807 setBackgroundDrawable(inPassword ? mBackground : null); 808 } 809 810 /** 811 * Private class used for the background of a password textfield. 812 */ 813 private static class OutlineDrawable extends Drawable { 814 public void draw(Canvas canvas) { 815 Rect bounds = getBounds(); 816 Paint paint = new Paint(); 817 paint.setAntiAlias(true); 818 // Draw the background. 819 paint.setColor(Color.WHITE); 820 canvas.drawRect(bounds, paint); 821 // Draw the outline. 822 paint.setStyle(Paint.Style.STROKE); 823 paint.setColor(Color.BLACK); 824 canvas.drawRect(bounds, paint); 825 } 826 // Always want it to be opaque. 827 public int getOpacity() { 828 return PixelFormat.OPAQUE; 829 } 830 // These are needed because they are abstract in Drawable. 831 public void setAlpha(int alpha) { } 832 public void setColorFilter(ColorFilter cf) { } 833 } 834 835 /** 836 * Create a background for the WebTextView and set up the paint for drawing 837 * the text. This way, we can see the password transformation of the 838 * system, which (optionally) shows the actual text before changing to dots. 839 * The background is necessary to hide the webkit-drawn text beneath. 840 */ 841 private void createBackground() { 842 if (mBackground != null) { 843 return; 844 } 845 mBackground = new OutlineDrawable(); 846 847 setGravity(Gravity.CENTER_VERTICAL); 848 // Turn on subpixel text, and turn off kerning, so it better matches 849 // the text in webkit. 850 TextPaint paint = getPaint(); 851 int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG | 852 Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG; 853 paint.setFlags(flags); 854 // Set the text color to black, regardless of the theme. This ensures 855 // that other applications that use embedded WebViews will properly 856 // display the text in password textfields. 857 setTextColor(Color.BLACK); 858 } 859 860 @Override 861 public void setInputType(int type) { 862 mFromSetInputType = true; 863 super.setInputType(type); 864 mFromSetInputType = false; 865 } 866 867 private void setMaxLength(int maxLength) { 868 mMaxLength = maxLength; 869 if (-1 == maxLength) { 870 setFilters(NO_FILTERS); 871 } else { 872 setFilters(new InputFilter[] { 873 new InputFilter.LengthFilter(maxLength) }); 874 } 875 } 876 877 /** 878 * Set the pointer for this node so it can be determined which node this 879 * WebTextView represents. 880 * @param ptr Integer representing the pointer to the node which this 881 * WebTextView represents. 882 */ 883 /* package */ void setNodePointer(int ptr) { 884 mNodePointer = ptr; 885 } 886 887 /** 888 * Determine the position and size of WebTextView, and add it to the 889 * WebView's view heirarchy. All parameters are presumed to be in 890 * view coordinates. Also requests Focus and sets the cursor to not 891 * request to be in view. 892 * @param x x-position of the textfield. 893 * @param y y-position of the textfield. 894 * @param width width of the textfield. 895 * @param height height of the textfield. 896 */ 897 /* package */ void setRect(int x, int y, int width, int height) { 898 LayoutParams lp = (LayoutParams) getLayoutParams(); 899 if (null == lp) { 900 lp = new LayoutParams(width, height, x, y); 901 } else { 902 lp.x = x; 903 lp.y = y; 904 lp.width = width; 905 lp.height = height; 906 } 907 if (getParent() == null) { 908 mWebView.addView(this, lp); 909 } else { 910 setLayoutParams(lp); 911 } 912 // Set up a measure spec so a layout can always be recreated. 913 mWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); 914 mHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 915 } 916 917 /** 918 * Set the selection, and disable our onSelectionChanged action. 919 */ 920 /* package */ void setSelectionFromWebKit(int start, int end) { 921 if (start < 0 || end < 0) return; 922 Spannable text = (Spannable) getText(); 923 int length = text.length(); 924 if (start > length || end > length) return; 925 mFromWebKit = true; 926 Selection.setSelection(text, start, end); 927 mFromWebKit = false; 928 } 929 930 /** 931 * Set the text to the new string, but use the old selection, making sure 932 * to keep it within the new string. 933 * @param text The new text to place in the textfield. 934 */ 935 /* package */ void setTextAndKeepSelection(String text) { 936 mPreChange = text.toString(); 937 Editable edit = (Editable) getText(); 938 int selStart = Selection.getSelectionStart(edit); 939 int selEnd = Selection.getSelectionEnd(edit); 940 mInSetTextAndKeepSelection = true; 941 edit.replace(0, edit.length(), text); 942 int newLength = edit.length(); 943 if (selStart > newLength) selStart = newLength; 944 if (selEnd > newLength) selEnd = newLength; 945 Selection.setSelection(edit, selStart, selEnd); 946 mInSetTextAndKeepSelection = false; 947 updateCachedTextfield(); 948 } 949 950 /** 951 * Called by WebView.rebuildWebTextView(). Based on the type of the <input> 952 * element, set up the WebTextView, its InputType, and IME Options properly. 953 * @param type int corresponding to enum "Type" defined in CachedInput.h. 954 * Does not correspond to HTMLInputElement::InputType so this 955 * is unaffected if that changes, and also because that has no 956 * type corresponding to textarea (which is its own tag). 957 */ 958 /* package */ void setType(int type) { 959 if (mWebView == null) return; 960 boolean single = true; 961 boolean inPassword = false; 962 int maxLength = -1; 963 int inputType = EditorInfo.TYPE_CLASS_TEXT 964 | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; 965 int imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI 966 | EditorInfo.IME_FLAG_NO_FULLSCREEN; 967 if (TEXT_AREA != type 968 && mWebView.nativeFocusCandidateHasNextTextfield()) { 969 imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT; 970 } 971 switch (type) { 972 case NORMAL_TEXT_FIELD: 973 imeOptions |= EditorInfo.IME_ACTION_GO; 974 break; 975 case TEXT_AREA: 976 single = false; 977 inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE 978 | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES 979 | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; 980 imeOptions |= EditorInfo.IME_ACTION_NONE; 981 break; 982 case PASSWORD: 983 inPassword = true; 984 imeOptions |= EditorInfo.IME_ACTION_GO; 985 break; 986 case SEARCH: 987 imeOptions |= EditorInfo.IME_ACTION_SEARCH; 988 break; 989 case EMAIL: 990 inputType = EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; 991 imeOptions |= EditorInfo.IME_ACTION_GO; 992 break; 993 case NUMBER: 994 inputType |= EditorInfo.TYPE_CLASS_NUMBER; 995 // Number and telephone do not have both a Tab key and an 996 // action, so set the action to NEXT 997 imeOptions |= EditorInfo.IME_ACTION_NEXT; 998 break; 999 case TELEPHONE: 1000 inputType |= EditorInfo.TYPE_CLASS_PHONE; 1001 imeOptions |= EditorInfo.IME_ACTION_NEXT; 1002 break; 1003 case URL: 1004 // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so 1005 // exclude it for now. 1006 imeOptions |= EditorInfo.IME_ACTION_GO; 1007 break; 1008 default: 1009 imeOptions |= EditorInfo.IME_ACTION_GO; 1010 break; 1011 } 1012 setHint(null); 1013 if (single) { 1014 mWebView.requestLabel(mWebView.nativeFocusCandidateFramePointer(), 1015 mNodePointer); 1016 maxLength = mWebView.nativeFocusCandidateMaxLength(); 1017 if (type != PASSWORD) { 1018 String name = mWebView.nativeFocusCandidateName(); 1019 if (name != null && name.length() > 0) { 1020 mWebView.requestFormData(name, mNodePointer, mAutoFillable); 1021 } 1022 } 1023 } 1024 mSingle = single; 1025 setMaxLength(maxLength); 1026 setHorizontallyScrolling(single); 1027 setInputType(inputType); 1028 setImeOptions(imeOptions); 1029 setInPassword(inPassword); 1030 AutoCompleteAdapter adapter = null; 1031 setAdapterCustom(adapter); 1032 } 1033 1034 /** 1035 * Update the cache to reflect the current text. 1036 */ 1037 /* package */ void updateCachedTextfield() { 1038 mWebView.updateCachedTextfield(getText().toString()); 1039 } 1040} 1041