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