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