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