autofill_popup_controller_impl.cc revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h" 6 7#include <algorithm> 8#include <utility> 9 10#include "base/logging.h" 11#include "base/strings/utf_string_conversions.h" 12#include "chrome/browser/ui/autofill/autofill_popup_view.h" 13#include "chrome/browser/ui/autofill/popup_constants.h" 14#include "components/autofill/core/browser/autofill_popup_delegate.h" 15#include "content/public/browser/native_web_keyboard_event.h" 16#include "grit/webkit_resources.h" 17#include "third_party/WebKit/public/web/WebAutofillClient.h" 18#include "ui/base/resource/resource_bundle.h" 19#include "ui/events/event.h" 20#include "ui/gfx/rect_conversions.h" 21#include "ui/gfx/screen.h" 22#include "ui/gfx/text_elider.h" 23#include "ui/gfx/text_utils.h" 24#include "ui/gfx/vector2d.h" 25 26using base::WeakPtr; 27using blink::WebAutofillClient; 28 29namespace autofill { 30namespace { 31 32// Used to indicate that no line is currently selected by the user. 33const int kNoSelection = -1; 34 35// The vertical height of each row in pixels. 36const size_t kRowHeight = 24; 37 38// The vertical height of a separator in pixels. 39const size_t kSeparatorHeight = 1; 40 41#if !defined(OS_ANDROID) 42// Size difference between name and subtext in pixels. 43const int kLabelFontSizeDelta = -2; 44 45const size_t kNamePadding = AutofillPopupView::kNamePadding; 46const size_t kIconPadding = AutofillPopupView::kIconPadding; 47const size_t kEndPadding = AutofillPopupView::kEndPadding; 48#endif 49 50struct DataResource { 51 const char* name; 52 int id; 53}; 54 55const DataResource kDataResources[] = { 56 { "americanExpressCC", IDR_AUTOFILL_CC_AMEX }, 57 { "dinersCC", IDR_AUTOFILL_CC_DINERS }, 58 { "discoverCC", IDR_AUTOFILL_CC_DISCOVER }, 59 { "genericCC", IDR_AUTOFILL_CC_GENERIC }, 60 { "jcbCC", IDR_AUTOFILL_CC_JCB }, 61 { "masterCardCC", IDR_AUTOFILL_CC_MASTERCARD }, 62 { "visaCC", IDR_AUTOFILL_CC_VISA }, 63}; 64 65} // namespace 66 67// static 68WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetOrCreate( 69 WeakPtr<AutofillPopupControllerImpl> previous, 70 WeakPtr<AutofillPopupDelegate> delegate, 71 content::WebContents* web_contents, 72 gfx::NativeView container_view, 73 const gfx::RectF& element_bounds, 74 base::i18n::TextDirection text_direction) { 75 DCHECK(!previous.get() || previous->delegate_.get() == delegate.get()); 76 77 if (previous.get() && previous->web_contents() == web_contents && 78 previous->container_view() == container_view && 79 previous->element_bounds() == element_bounds) { 80 previous->ClearState(); 81 return previous; 82 } 83 84 if (previous.get()) 85 previous->Hide(); 86 87 AutofillPopupControllerImpl* controller = 88 new AutofillPopupControllerImpl( 89 delegate, web_contents, container_view, element_bounds, 90 text_direction); 91 return controller->GetWeakPtr(); 92} 93 94AutofillPopupControllerImpl::AutofillPopupControllerImpl( 95 base::WeakPtr<AutofillPopupDelegate> delegate, 96 content::WebContents* web_contents, 97 gfx::NativeView container_view, 98 const gfx::RectF& element_bounds, 99 base::i18n::TextDirection text_direction) 100 : controller_common_(new PopupControllerCommon(element_bounds, 101 container_view, 102 web_contents)), 103 view_(NULL), 104 delegate_(delegate), 105 text_direction_(text_direction), 106 hide_on_outside_click_(false), 107 weak_ptr_factory_(this) { 108 ClearState(); 109 controller_common_->SetKeyPressCallback( 110 base::Bind(&AutofillPopupControllerImpl::HandleKeyPressEvent, 111 base::Unretained(this))); 112#if !defined(OS_ANDROID) 113 subtext_font_list_ = name_font_list_.DeriveWithSizeDelta(kLabelFontSizeDelta); 114#if defined(OS_MACOSX) 115 // There is no italic version of the system font. 116 warning_font_list_ = name_font_list_; 117#else 118 warning_font_list_ = name_font_list_.DeriveWithStyle(gfx::Font::ITALIC); 119#endif 120#endif 121} 122 123AutofillPopupControllerImpl::~AutofillPopupControllerImpl() {} 124 125void AutofillPopupControllerImpl::Show( 126 const std::vector<base::string16>& names, 127 const std::vector<base::string16>& subtexts, 128 const std::vector<base::string16>& icons, 129 const std::vector<int>& identifiers) { 130 SetValues(names, subtexts, icons, identifiers); 131 132#if !defined(OS_ANDROID) 133 // Android displays the long text with ellipsis using the view attributes. 134 135 UpdatePopupBounds(); 136 int popup_width = popup_bounds().width(); 137 138 // Elide the name and subtext strings so that the popup fits in the available 139 // space. 140 for (size_t i = 0; i < names_.size(); ++i) { 141 int name_width = gfx::GetStringWidth(names_[i], GetNameFontListForRow(i)); 142 int subtext_width = gfx::GetStringWidth(subtexts_[i], subtext_font_list()); 143 int total_text_length = name_width + subtext_width; 144 145 // The line can have no strings if it represents a UI element, such as 146 // a separator line. 147 if (total_text_length == 0) 148 continue; 149 150 int available_width = popup_width - RowWidthWithoutText(i); 151 152 // Each field recieves space in proportion to its length. 153 int name_size = available_width * name_width / total_text_length; 154 names_[i] = gfx::ElideText(names_[i], 155 GetNameFontListForRow(i), 156 name_size, 157 gfx::ELIDE_AT_END); 158 159 int subtext_size = available_width * subtext_width / total_text_length; 160 subtexts_[i] = gfx::ElideText(subtexts_[i], 161 subtext_font_list(), 162 subtext_size, 163 gfx::ELIDE_AT_END); 164 } 165#endif 166 167 if (!view_) { 168 view_ = AutofillPopupView::Create(this); 169 170 // It is possible to fail to create the popup, in this case 171 // treat the popup as hiding right away. 172 if (!view_) { 173 Hide(); 174 return; 175 } 176 177 ShowView(); 178 } else { 179 UpdateBoundsAndRedrawPopup(); 180 } 181 182 delegate_->OnPopupShown(); 183 controller_common_->RegisterKeyPressCallback(); 184} 185 186void AutofillPopupControllerImpl::UpdateDataListValues( 187 const std::vector<base::string16>& values, 188 const std::vector<base::string16>& labels) { 189 // Remove all the old data list values, which should always be at the top of 190 // the list if they are present. 191 while (!identifiers_.empty() && 192 identifiers_[0] == WebAutofillClient::MenuItemIDDataListEntry) { 193 names_.erase(names_.begin()); 194 subtexts_.erase(subtexts_.begin()); 195 icons_.erase(icons_.begin()); 196 identifiers_.erase(identifiers_.begin()); 197 } 198 199 // If there are no new data list values, exit (clearing the separator if there 200 // is one). 201 if (values.empty()) { 202 if (!identifiers_.empty() && 203 identifiers_[0] == WebAutofillClient::MenuItemIDSeparator) { 204 names_.erase(names_.begin()); 205 subtexts_.erase(subtexts_.begin()); 206 icons_.erase(icons_.begin()); 207 identifiers_.erase(identifiers_.begin()); 208 } 209 210 // The popup contents have changed, so either update the bounds or hide it. 211 if (HasSuggestions()) 212 UpdateBoundsAndRedrawPopup(); 213 else 214 Hide(); 215 216 return; 217 } 218 219 // Add a separator if there are any other values. 220 if (!identifiers_.empty() && 221 identifiers_[0] != WebAutofillClient::MenuItemIDSeparator) { 222 names_.insert(names_.begin(), base::string16()); 223 subtexts_.insert(subtexts_.begin(), base::string16()); 224 icons_.insert(icons_.begin(), base::string16()); 225 identifiers_.insert(identifiers_.begin(), 226 WebAutofillClient::MenuItemIDSeparator); 227 } 228 229 230 names_.insert(names_.begin(), values.begin(), values.end()); 231 subtexts_.insert(subtexts_.begin(), labels.begin(), labels.end()); 232 233 // Add the values that are the same for all data list elements. 234 icons_.insert(icons_.begin(), values.size(), base::string16()); 235 identifiers_.insert(identifiers_.begin(), 236 values.size(), 237 WebAutofillClient::MenuItemIDDataListEntry); 238 239 UpdateBoundsAndRedrawPopup(); 240} 241 242void AutofillPopupControllerImpl::Hide() { 243 controller_common_->RemoveKeyPressCallback(); 244 if (delegate_.get()) 245 delegate_->OnPopupHidden(); 246 247 if (view_) 248 view_->Hide(); 249 250 delete this; 251} 252 253void AutofillPopupControllerImpl::ViewDestroyed() { 254 // The view has already been destroyed so clear the reference to it. 255 view_ = NULL; 256 257 Hide(); 258} 259 260bool AutofillPopupControllerImpl::HandleKeyPressEvent( 261 const content::NativeWebKeyboardEvent& event) { 262 switch (event.windowsKeyCode) { 263 case ui::VKEY_UP: 264 SelectPreviousLine(); 265 return true; 266 case ui::VKEY_DOWN: 267 SelectNextLine(); 268 return true; 269 case ui::VKEY_PRIOR: // Page up. 270 SetSelectedLine(0); 271 return true; 272 case ui::VKEY_NEXT: // Page down. 273 SetSelectedLine(names().size() - 1); 274 return true; 275 case ui::VKEY_ESCAPE: 276 Hide(); 277 return true; 278 case ui::VKEY_DELETE: 279 return (event.modifiers & content::NativeWebKeyboardEvent::ShiftKey) && 280 RemoveSelectedLine(); 281 case ui::VKEY_TAB: 282 // A tab press should cause the selected line to be accepted, but still 283 // return false so the tab key press propagates and changes the cursor 284 // location. 285 AcceptSelectedLine(); 286 return false; 287 case ui::VKEY_RETURN: 288 return AcceptSelectedLine(); 289 default: 290 return false; 291 } 292} 293 294void AutofillPopupControllerImpl::UpdateBoundsAndRedrawPopup() { 295#if !defined(OS_ANDROID) 296 // TODO(csharp): Since UpdatePopupBounds can change the position of the popup, 297 // the popup could end up jumping from above the element to below it. 298 // It is unclear if it is better to keep the popup where it was, or if it 299 // should try and move to its desired position. 300 UpdatePopupBounds(); 301#endif 302 303 view_->UpdateBoundsAndRedrawPopup(); 304} 305 306void AutofillPopupControllerImpl::SetSelectionAtPoint(const gfx::Point& point) { 307 SetSelectedLine(LineFromY(point.y())); 308} 309 310void AutofillPopupControllerImpl::AcceptSelectionAtPoint( 311 const gfx::Point& point) { 312 SetSelectionAtPoint(point); 313 AcceptSelectedLine(); 314} 315 316void AutofillPopupControllerImpl::SelectionCleared() { 317 SetSelectedLine(kNoSelection); 318} 319 320bool AutofillPopupControllerImpl::ShouldRepostEvent( 321 const ui::MouseEvent& event) { 322 return delegate_->ShouldRepostEvent(event); 323} 324 325bool AutofillPopupControllerImpl::ShouldHideOnOutsideClick() const { 326 return hide_on_outside_click_; 327} 328 329void AutofillPopupControllerImpl::AcceptSuggestion(size_t index) { 330 delegate_->DidAcceptSuggestion(full_names_[index], identifiers_[index]); 331} 332 333int AutofillPopupControllerImpl::GetIconResourceID( 334 const base::string16& resource_name) const { 335 for (size_t i = 0; i < arraysize(kDataResources); ++i) { 336 if (resource_name == base::ASCIIToUTF16(kDataResources[i].name)) 337 return kDataResources[i].id; 338 } 339 340 return -1; 341} 342 343bool AutofillPopupControllerImpl::CanDelete(size_t index) const { 344 // TODO(isherman): Native AddressBook suggestions on Mac and Android should 345 // not be considered to be deleteable. 346 int id = identifiers_[index]; 347 return id > 0 || 348 id == WebAutofillClient::MenuItemIDAutocompleteEntry || 349 id == WebAutofillClient::MenuItemIDPasswordEntry; 350} 351 352bool AutofillPopupControllerImpl::IsWarning(size_t index) const { 353 return identifiers_[index] == WebAutofillClient::MenuItemIDWarningMessage; 354} 355 356gfx::Rect AutofillPopupControllerImpl::GetRowBounds(size_t index) { 357 int top = kPopupBorderThickness; 358 for (size_t i = 0; i < index; ++i) { 359 top += GetRowHeightFromId(identifiers()[i]); 360 } 361 362 return gfx::Rect( 363 kPopupBorderThickness, 364 top, 365 popup_bounds_.width() - 2 * kPopupBorderThickness, 366 GetRowHeightFromId(identifiers()[index])); 367} 368 369void AutofillPopupControllerImpl::SetPopupBounds(const gfx::Rect& bounds) { 370 popup_bounds_ = bounds; 371 UpdateBoundsAndRedrawPopup(); 372} 373 374const gfx::Rect& AutofillPopupControllerImpl::popup_bounds() const { 375 return popup_bounds_; 376} 377 378content::WebContents* AutofillPopupControllerImpl::web_contents() { 379 return controller_common_->web_contents(); 380} 381 382gfx::NativeView AutofillPopupControllerImpl::container_view() { 383 return controller_common_->container_view(); 384} 385 386const gfx::RectF& AutofillPopupControllerImpl::element_bounds() const { 387 return controller_common_->element_bounds(); 388} 389 390bool AutofillPopupControllerImpl::IsRTL() const { 391 return text_direction_ == base::i18n::RIGHT_TO_LEFT; 392} 393 394const std::vector<base::string16>& AutofillPopupControllerImpl::names() const { 395 return names_; 396} 397 398const std::vector<base::string16>& AutofillPopupControllerImpl::subtexts() 399 const { 400 return subtexts_; 401} 402 403const std::vector<base::string16>& AutofillPopupControllerImpl::icons() const { 404 return icons_; 405} 406 407const std::vector<int>& AutofillPopupControllerImpl::identifiers() const { 408 return identifiers_; 409} 410 411#if !defined(OS_ANDROID) 412const gfx::FontList& AutofillPopupControllerImpl::GetNameFontListForRow( 413 size_t index) const { 414 if (identifiers_[index] == WebAutofillClient::MenuItemIDWarningMessage) 415 return warning_font_list_; 416 417 return name_font_list_; 418} 419 420const gfx::FontList& AutofillPopupControllerImpl::subtext_font_list() const { 421 return subtext_font_list_; 422} 423#endif 424 425int AutofillPopupControllerImpl::selected_line() const { 426 return selected_line_; 427} 428 429void AutofillPopupControllerImpl::set_hide_on_outside_click( 430 bool hide_on_outside_click) { 431 hide_on_outside_click_ = hide_on_outside_click; 432} 433 434void AutofillPopupControllerImpl::SetSelectedLine(int selected_line) { 435 if (selected_line_ == selected_line) 436 return; 437 438 if (selected_line_ != kNoSelection && 439 static_cast<size_t>(selected_line_) < identifiers_.size()) 440 InvalidateRow(selected_line_); 441 442 if (selected_line != kNoSelection) 443 InvalidateRow(selected_line); 444 445 selected_line_ = selected_line; 446 447 if (selected_line_ != kNoSelection) 448 delegate_->DidSelectSuggestion(identifiers_[selected_line_]); 449 else 450 delegate_->ClearPreviewedForm(); 451} 452 453void AutofillPopupControllerImpl::SelectNextLine() { 454 int new_selected_line = selected_line_ + 1; 455 456 // Skip over any lines that can't be selected. 457 while (static_cast<size_t>(new_selected_line) < names_.size() && 458 !CanAccept(identifiers()[new_selected_line])) { 459 ++new_selected_line; 460 } 461 462 if (new_selected_line >= static_cast<int>(names_.size())) 463 new_selected_line = 0; 464 465 SetSelectedLine(new_selected_line); 466} 467 468void AutofillPopupControllerImpl::SelectPreviousLine() { 469 int new_selected_line = selected_line_ - 1; 470 471 // Skip over any lines that can't be selected. 472 while (new_selected_line > kNoSelection && 473 !CanAccept(identifiers()[new_selected_line])) { 474 --new_selected_line; 475 } 476 477 if (new_selected_line <= kNoSelection) 478 new_selected_line = names_.size() - 1; 479 480 SetSelectedLine(new_selected_line); 481} 482 483bool AutofillPopupControllerImpl::AcceptSelectedLine() { 484 if (selected_line_ == kNoSelection) 485 return false; 486 487 DCHECK_GE(selected_line_, 0); 488 DCHECK_LT(selected_line_, static_cast<int>(names_.size())); 489 490 if (!CanAccept(identifiers_[selected_line_])) 491 return false; 492 493 AcceptSuggestion(selected_line_); 494 return true; 495} 496 497bool AutofillPopupControllerImpl::RemoveSelectedLine() { 498 if (selected_line_ == kNoSelection) 499 return false; 500 501 DCHECK_GE(selected_line_, 0); 502 DCHECK_LT(selected_line_, static_cast<int>(names_.size())); 503 504 if (!CanDelete(selected_line_)) 505 return false; 506 507 delegate_->RemoveSuggestion(full_names_[selected_line_], 508 identifiers_[selected_line_]); 509 510 // Remove the deleted element. 511 names_.erase(names_.begin() + selected_line_); 512 full_names_.erase(full_names_.begin() + selected_line_); 513 subtexts_.erase(subtexts_.begin() + selected_line_); 514 icons_.erase(icons_.begin() + selected_line_); 515 identifiers_.erase(identifiers_.begin() + selected_line_); 516 517 SetSelectedLine(kNoSelection); 518 519 if (HasSuggestions()) { 520 delegate_->ClearPreviewedForm(); 521 UpdateBoundsAndRedrawPopup(); 522 } else { 523 Hide(); 524 } 525 526 return true; 527} 528 529int AutofillPopupControllerImpl::LineFromY(int y) { 530 int current_height = kPopupBorderThickness; 531 532 for (size_t i = 0; i < identifiers().size(); ++i) { 533 current_height += GetRowHeightFromId(identifiers()[i]); 534 535 if (y <= current_height) 536 return i; 537 } 538 539 // The y value goes beyond the popup so stop the selection at the last line. 540 return identifiers().size() - 1; 541} 542 543int AutofillPopupControllerImpl::GetRowHeightFromId(int identifier) const { 544 if (identifier == WebAutofillClient::MenuItemIDSeparator) 545 return kSeparatorHeight; 546 547 return kRowHeight; 548} 549 550bool AutofillPopupControllerImpl::CanAccept(int id) { 551 return id != WebAutofillClient::MenuItemIDSeparator && 552 id != WebAutofillClient::MenuItemIDWarningMessage; 553} 554 555bool AutofillPopupControllerImpl::HasSuggestions() { 556 return identifiers_.size() != 0 && 557 (identifiers_[0] > 0 || 558 identifiers_[0] == 559 WebAutofillClient::MenuItemIDAutocompleteEntry || 560 identifiers_[0] == WebAutofillClient::MenuItemIDPasswordEntry || 561 identifiers_[0] == WebAutofillClient::MenuItemIDDataListEntry); 562} 563 564void AutofillPopupControllerImpl::SetValues( 565 const std::vector<base::string16>& names, 566 const std::vector<base::string16>& subtexts, 567 const std::vector<base::string16>& icons, 568 const std::vector<int>& identifiers) { 569 names_ = names; 570 full_names_ = names; 571 subtexts_ = subtexts; 572 icons_ = icons; 573 identifiers_ = identifiers; 574} 575 576void AutofillPopupControllerImpl::ShowView() { 577 view_->Show(); 578} 579 580void AutofillPopupControllerImpl::InvalidateRow(size_t row) { 581 DCHECK(0 <= row); 582 DCHECK(row < identifiers_.size()); 583 view_->InvalidateRow(row); 584} 585 586#if !defined(OS_ANDROID) 587int AutofillPopupControllerImpl::GetDesiredPopupWidth() const { 588 int popup_width = controller_common_->RoundedElementBounds().width(); 589 DCHECK_EQ(names().size(), subtexts().size()); 590 for (size_t i = 0; i < names().size(); ++i) { 591 int row_size = 592 gfx::GetStringWidth(names()[i], name_font_list_) + 593 gfx::GetStringWidth(subtexts()[i], subtext_font_list_) + 594 RowWidthWithoutText(i); 595 596 popup_width = std::max(popup_width, row_size); 597 } 598 599 return popup_width; 600} 601 602int AutofillPopupControllerImpl::GetDesiredPopupHeight() const { 603 int popup_height = 2 * kPopupBorderThickness; 604 605 for (size_t i = 0; i < identifiers().size(); ++i) { 606 popup_height += GetRowHeightFromId(identifiers()[i]); 607 } 608 609 return popup_height; 610} 611 612int AutofillPopupControllerImpl::RowWidthWithoutText(int row) const { 613 int row_size = kEndPadding; 614 615 if (!subtexts_[row].empty()) 616 row_size += kNamePadding; 617 618 // Add the Autofill icon size, if required. 619 if (!icons_[row].empty()) { 620 int icon_width = ui::ResourceBundle::GetSharedInstance().GetImageNamed( 621 GetIconResourceID(icons_[row])).Width(); 622 row_size += icon_width + kIconPadding; 623 } 624 625 // Add the padding at the end. 626 row_size += kEndPadding; 627 628 // Add room for the popup border. 629 row_size += 2 * kPopupBorderThickness; 630 631 return row_size; 632} 633 634void AutofillPopupControllerImpl::UpdatePopupBounds() { 635 int popup_required_width = GetDesiredPopupWidth(); 636 int popup_height = GetDesiredPopupHeight(); 637 638 popup_bounds_ = controller_common_->GetPopupBounds(popup_height, 639 popup_required_width); 640} 641#endif // !defined(OS_ANDROID) 642 643WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetWeakPtr() { 644 return weak_ptr_factory_.GetWeakPtr(); 645} 646 647void AutofillPopupControllerImpl::ClearState() { 648 // Don't clear view_, because otherwise the popup will have to get regenerated 649 // and this will cause flickering. 650 651 popup_bounds_ = gfx::Rect(); 652 653 names_.clear(); 654 subtexts_.clear(); 655 icons_.clear(); 656 identifiers_.clear(); 657 full_names_.clear(); 658 659 selected_line_ = kNoSelection; 660} 661 662} // namespace autofill 663