Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / autofill / autofill_popup_controller_impl.cc
blob857811f3111d7d0ad38e388e9466d3169a09d59d
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.
5 #include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
7 #include <algorithm>
8 #include <utility>
10 #include "base/command_line.h"
11 #include "base/logging.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/ui/autofill/autofill_popup_view.h"
14 #include "chrome/browser/ui/autofill/popup_constants.h"
15 #include "components/autofill/core/browser/autofill_popup_delegate.h"
16 #include "components/autofill/core/browser/popup_item_ids.h"
17 #include "components/autofill/core/browser/suggestion.h"
18 #include "components/autofill/core/common/autofill_util.h"
19 #include "content/public/browser/native_web_keyboard_event.h"
20 #include "grit/components_scaled_resources.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/events/event.h"
23 #include "ui/gfx/geometry/rect_conversions.h"
24 #include "ui/gfx/geometry/vector2d.h"
25 #include "ui/gfx/screen.h"
26 #include "ui/gfx/text_elider.h"
27 #include "ui/gfx/text_utils.h"
29 using base::WeakPtr;
31 namespace autofill {
32 namespace {
34 // Used to indicate that no line is currently selected by the user.
35 const int kNoSelection = -1;
37 // The vertical height of each row in pixels.
38 const size_t kRowHeight = 24;
40 // The vertical height of a separator in pixels.
41 const size_t kSeparatorHeight = 1;
43 #if !defined(OS_ANDROID)
44 // Size difference between name and label in pixels.
45 const int kLabelFontSizeDelta = -2;
47 const size_t kNamePadding = AutofillPopupView::kNamePadding;
48 const size_t kIconPadding = AutofillPopupView::kIconPadding;
49 const size_t kEndPadding = AutofillPopupView::kEndPadding;
50 #endif
52 struct DataResource {
53 const char* name;
54 int id;
57 const DataResource kDataResources[] = {
58 { "americanExpressCC", IDR_AUTOFILL_CC_AMEX },
59 { "dinersCC", IDR_AUTOFILL_CC_GENERIC },
60 { "discoverCC", IDR_AUTOFILL_CC_DISCOVER },
61 { "genericCC", IDR_AUTOFILL_CC_GENERIC },
62 { "jcbCC", IDR_AUTOFILL_CC_GENERIC },
63 { "masterCardCC", IDR_AUTOFILL_CC_MASTERCARD },
64 { "visaCC", IDR_AUTOFILL_CC_VISA },
65 #if defined(OS_ANDROID)
66 { "scanCreditCardIcon", IDR_AUTOFILL_CC_SCAN_NEW },
67 { "settings", IDR_AUTOFILL_SETTINGS },
68 #endif
71 } // namespace
73 // static
74 WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetOrCreate(
75 WeakPtr<AutofillPopupControllerImpl> previous,
76 WeakPtr<AutofillPopupDelegate> delegate,
77 content::WebContents* web_contents,
78 gfx::NativeView container_view,
79 const gfx::RectF& element_bounds,
80 base::i18n::TextDirection text_direction) {
81 if (previous.get() && previous->web_contents() == web_contents &&
82 previous->delegate_.get() == delegate.get() &&
83 previous->container_view() == container_view &&
84 previous->element_bounds() == element_bounds) {
85 previous->ClearState();
86 return previous;
89 if (previous.get())
90 previous->Hide();
92 AutofillPopupControllerImpl* controller =
93 new AutofillPopupControllerImpl(
94 delegate, web_contents, container_view, element_bounds,
95 text_direction);
96 return controller->GetWeakPtr();
99 AutofillPopupControllerImpl::AutofillPopupControllerImpl(
100 base::WeakPtr<AutofillPopupDelegate> delegate,
101 content::WebContents* web_contents,
102 gfx::NativeView container_view,
103 const gfx::RectF& element_bounds,
104 base::i18n::TextDirection text_direction)
105 : controller_common_(new PopupControllerCommon(element_bounds,
106 text_direction,
107 container_view,
108 web_contents)),
109 view_(NULL),
110 delegate_(delegate),
111 weak_ptr_factory_(this) {
112 ClearState();
113 controller_common_->SetKeyPressCallback(
114 base::Bind(&AutofillPopupControllerImpl::HandleKeyPressEvent,
115 base::Unretained(this)));
116 #if !defined(OS_ANDROID)
117 label_font_list_ = value_font_list_.DeriveWithSizeDelta(kLabelFontSizeDelta);
118 title_font_list_ = value_font_list_.DeriveWithStyle(gfx::Font::BOLD);
119 #if defined(OS_MACOSX)
120 // There is no italic version of the system font.
121 warning_font_list_ = value_font_list_;
122 #else
123 warning_font_list_ = value_font_list_.DeriveWithStyle(gfx::Font::ITALIC);
124 #endif
125 #endif
128 AutofillPopupControllerImpl::~AutofillPopupControllerImpl() {}
130 void AutofillPopupControllerImpl::Show(
131 const std::vector<autofill::Suggestion>& suggestions) {
132 SetValues(suggestions);
133 DCHECK_EQ(suggestions_.size(), elided_values_.size());
134 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
136 #if !defined(OS_ANDROID)
137 // Android displays the long text with ellipsis using the view attributes.
139 UpdatePopupBounds();
140 int popup_width = popup_bounds().width();
142 // Elide the name and label strings so that the popup fits in the available
143 // space.
144 for (size_t i = 0; i < suggestions_.size(); ++i) {
145 int value_width =
146 gfx::GetStringWidth(suggestions_[i].value, GetValueFontListForRow(i));
147 int label_width =
148 gfx::GetStringWidth(suggestions_[i].label, GetLabelFontList());
149 int total_text_length = value_width + label_width;
151 // The line can have no strings if it represents a UI element, such as
152 // a separator line.
153 if (total_text_length == 0)
154 continue;
156 int available_width = popup_width - RowWidthWithoutText(i);
158 // Each field receives space in proportion to its length.
159 int value_size = available_width * value_width / total_text_length;
160 elided_values_[i] = gfx::ElideText(suggestions_[i].value,
161 GetValueFontListForRow(i),
162 value_size, gfx::ELIDE_TAIL);
164 int label_size = available_width * label_width / total_text_length;
165 elided_labels_[i] = gfx::ElideText(suggestions_[i].label,
166 GetLabelFontList(),
167 label_size, gfx::ELIDE_TAIL);
169 #endif
171 if (!view_) {
172 view_ = AutofillPopupView::Create(this);
174 // It is possible to fail to create the popup, in this case
175 // treat the popup as hiding right away.
176 if (!view_) {
177 Hide();
178 return;
181 ShowView();
182 } else {
183 UpdateBoundsAndRedrawPopup();
186 controller_common_->RegisterKeyPressCallback();
187 delegate_->OnPopupShown();
189 DCHECK_EQ(suggestions_.size(), elided_values_.size());
190 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
193 void AutofillPopupControllerImpl::UpdateDataListValues(
194 const std::vector<base::string16>& values,
195 const std::vector<base::string16>& labels) {
196 DCHECK_EQ(suggestions_.size(), elided_values_.size());
197 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
199 // Remove all the old data list values, which should always be at the top of
200 // the list if they are present.
201 while (!suggestions_.empty() &&
202 suggestions_[0].frontend_id == POPUP_ITEM_ID_DATALIST_ENTRY) {
203 suggestions_.erase(suggestions_.begin());
204 elided_values_.erase(elided_values_.begin());
205 elided_labels_.erase(elided_labels_.begin());
208 // If there are no new data list values, exit (clearing the separator if there
209 // is one).
210 if (values.empty()) {
211 if (!suggestions_.empty() &&
212 suggestions_[0].frontend_id == POPUP_ITEM_ID_SEPARATOR) {
213 suggestions_.erase(suggestions_.begin());
214 elided_values_.erase(elided_values_.begin());
215 elided_labels_.erase(elided_labels_.begin());
218 // The popup contents have changed, so either update the bounds or hide it.
219 if (HasSuggestions())
220 UpdateBoundsAndRedrawPopup();
221 else
222 Hide();
224 return;
227 // Add a separator if there are any other values.
228 if (!suggestions_.empty() &&
229 suggestions_[0].frontend_id != POPUP_ITEM_ID_SEPARATOR) {
230 suggestions_.insert(suggestions_.begin(), autofill::Suggestion());
231 suggestions_[0].frontend_id = POPUP_ITEM_ID_SEPARATOR;
232 elided_values_.insert(elided_values_.begin(), base::string16());
233 elided_labels_.insert(elided_labels_.begin(), base::string16());
236 // Prepend the parameters to the suggestions we already have.
237 suggestions_.insert(suggestions_.begin(), values.size(), Suggestion());
238 elided_values_.insert(elided_values_.begin(), values.size(),
239 base::string16());
240 elided_labels_.insert(elided_labels_.begin(), values.size(),
241 base::string16());
242 for (size_t i = 0; i < values.size(); i++) {
243 suggestions_[i].value = values[i];
244 suggestions_[i].label = labels[i];
245 suggestions_[i].frontend_id = POPUP_ITEM_ID_DATALIST_ENTRY;
247 // TODO(brettw) it looks like these should be elided.
248 elided_values_[i] = values[i];
249 elided_labels_[i] = labels[i];
252 UpdateBoundsAndRedrawPopup();
253 DCHECK_EQ(suggestions_.size(), elided_values_.size());
254 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
257 void AutofillPopupControllerImpl::Hide() {
258 controller_common_->RemoveKeyPressCallback();
259 if (delegate_)
260 delegate_->OnPopupHidden();
262 if (view_)
263 view_->Hide();
265 delete this;
268 void AutofillPopupControllerImpl::ViewDestroyed() {
269 // The view has already been destroyed so clear the reference to it.
270 view_ = NULL;
272 Hide();
275 bool AutofillPopupControllerImpl::HandleKeyPressEvent(
276 const content::NativeWebKeyboardEvent& event) {
277 switch (event.windowsKeyCode) {
278 case ui::VKEY_UP:
279 SelectPreviousLine();
280 return true;
281 case ui::VKEY_DOWN:
282 SelectNextLine();
283 return true;
284 case ui::VKEY_PRIOR: // Page up.
285 // Set no line and then select the next line in case the first line is not
286 // selectable.
287 SetSelectedLine(kNoSelection);
288 SelectNextLine();
289 return true;
290 case ui::VKEY_NEXT: // Page down.
291 SetSelectedLine(GetLineCount() - 1);
292 return true;
293 case ui::VKEY_ESCAPE:
294 Hide();
295 return true;
296 case ui::VKEY_DELETE:
297 return (event.modifiers & content::NativeWebKeyboardEvent::ShiftKey) &&
298 RemoveSelectedLine();
299 case ui::VKEY_TAB:
300 // A tab press should cause the selected line to be accepted, but still
301 // return false so the tab key press propagates and changes the cursor
302 // location.
303 AcceptSelectedLine();
304 return false;
305 case ui::VKEY_RETURN:
306 return AcceptSelectedLine();
307 default:
308 return false;
312 void AutofillPopupControllerImpl::UpdateBoundsAndRedrawPopup() {
313 #if !defined(OS_ANDROID)
314 // TODO(csharp): Since UpdatePopupBounds can change the position of the popup,
315 // the popup could end up jumping from above the element to below it.
316 // It is unclear if it is better to keep the popup where it was, or if it
317 // should try and move to its desired position.
318 UpdatePopupBounds();
319 #endif
321 view_->UpdateBoundsAndRedrawPopup();
324 void AutofillPopupControllerImpl::SetSelectionAtPoint(const gfx::Point& point) {
325 SetSelectedLine(LineFromY(point.y()));
328 bool AutofillPopupControllerImpl::AcceptSelectedLine() {
329 if (selected_line_ == kNoSelection)
330 return false;
332 DCHECK_GE(selected_line_, 0);
333 DCHECK_LT(selected_line_, static_cast<int>(GetLineCount()));
335 if (!CanAccept(suggestions_[selected_line_].frontend_id))
336 return false;
338 AcceptSuggestion(selected_line_);
339 return true;
342 void AutofillPopupControllerImpl::SelectionCleared() {
343 SetSelectedLine(kNoSelection);
346 void AutofillPopupControllerImpl::AcceptSuggestion(size_t index) {
347 const autofill::Suggestion& suggestion = suggestions_[index];
348 delegate_->DidAcceptSuggestion(suggestion.value, suggestion.frontend_id,
349 index);
352 int AutofillPopupControllerImpl::GetIconResourceID(
353 const base::string16& resource_name) const {
354 int result = -1;
355 for (size_t i = 0; i < arraysize(kDataResources); ++i) {
356 if (resource_name == base::ASCIIToUTF16(kDataResources[i].name)) {
357 result = kDataResources[i].id;
358 break;
362 #if defined(OS_ANDROID)
363 if (result == IDR_AUTOFILL_CC_SCAN_NEW && IsKeyboardAccessoryEnabled())
364 result = IDR_AUTOFILL_CC_SCAN_NEW_KEYBOARD_ACCESSORY;
365 #endif // OS_ANDROID
367 return result;
370 bool AutofillPopupControllerImpl::IsWarning(size_t index) const {
371 return suggestions_[index].frontend_id == POPUP_ITEM_ID_WARNING_MESSAGE;
374 gfx::Rect AutofillPopupControllerImpl::GetRowBounds(size_t index) {
375 int top = kPopupBorderThickness;
376 for (size_t i = 0; i < index; ++i) {
377 top += GetRowHeightFromId(suggestions_[i].frontend_id);
380 return gfx::Rect(
381 kPopupBorderThickness,
382 top,
383 popup_bounds_.width() - 2 * kPopupBorderThickness,
384 GetRowHeightFromId(suggestions_[index].frontend_id));
387 void AutofillPopupControllerImpl::SetPopupBounds(const gfx::Rect& bounds) {
388 popup_bounds_ = bounds;
389 UpdateBoundsAndRedrawPopup();
392 const gfx::Rect& AutofillPopupControllerImpl::popup_bounds() const {
393 return popup_bounds_;
396 content::WebContents* AutofillPopupControllerImpl::web_contents() {
397 return controller_common_->web_contents();
400 gfx::NativeView AutofillPopupControllerImpl::container_view() {
401 return controller_common_->container_view();
404 const gfx::RectF& AutofillPopupControllerImpl::element_bounds() const {
405 return controller_common_->element_bounds();
408 bool AutofillPopupControllerImpl::IsRTL() const {
409 return controller_common_->is_rtl();
412 size_t AutofillPopupControllerImpl::GetLineCount() const {
413 return suggestions_.size();
416 const autofill::Suggestion& AutofillPopupControllerImpl::GetSuggestionAt(
417 size_t row) const {
418 return suggestions_[row];
421 const base::string16& AutofillPopupControllerImpl::GetElidedValueAt(
422 size_t row) const {
423 return elided_values_[row];
426 const base::string16& AutofillPopupControllerImpl::GetElidedLabelAt(
427 size_t row) const {
428 return elided_labels_[row];
431 bool AutofillPopupControllerImpl::GetRemovalConfirmationText(
432 int list_index,
433 base::string16* title,
434 base::string16* body) {
435 return delegate_->GetDeletionConfirmationText(
436 suggestions_[list_index].value, suggestions_[list_index].frontend_id,
437 title, body);
440 bool AutofillPopupControllerImpl::RemoveSuggestion(int list_index) {
441 if (!delegate_->RemoveSuggestion(suggestions_[list_index].value,
442 suggestions_[list_index].frontend_id)) {
443 return false;
446 // Remove the deleted element.
447 suggestions_.erase(suggestions_.begin() + list_index);
448 elided_values_.erase(elided_values_.begin() + list_index);
449 elided_labels_.erase(elided_labels_.begin() + list_index);
451 SetSelectedLine(kNoSelection);
453 if (HasSuggestions()) {
454 delegate_->ClearPreviewedForm();
455 UpdateBoundsAndRedrawPopup();
456 } else {
457 Hide();
460 return true;
463 #if !defined(OS_ANDROID)
464 const gfx::FontList& AutofillPopupControllerImpl::GetValueFontListForRow(
465 size_t index) const {
466 if (suggestions_[index].frontend_id == POPUP_ITEM_ID_WARNING_MESSAGE)
467 return warning_font_list_;
469 if (suggestions_[index].frontend_id == POPUP_ITEM_ID_TITLE)
470 return title_font_list_;
472 return value_font_list_;
475 const gfx::FontList& AutofillPopupControllerImpl::GetLabelFontList() const {
476 return label_font_list_;
478 #endif
480 int AutofillPopupControllerImpl::selected_line() const {
481 return selected_line_;
484 void AutofillPopupControllerImpl::SetSelectedLine(int selected_line) {
485 if (selected_line_ == selected_line)
486 return;
488 if (selected_line_ != kNoSelection &&
489 static_cast<size_t>(selected_line_) < suggestions_.size())
490 InvalidateRow(selected_line_);
492 if (selected_line != kNoSelection) {
493 InvalidateRow(selected_line);
495 if (!CanAccept(suggestions_[selected_line].frontend_id))
496 selected_line = kNoSelection;
499 selected_line_ = selected_line;
501 if (selected_line_ != kNoSelection) {
502 delegate_->DidSelectSuggestion(suggestions_[selected_line_].value,
503 suggestions_[selected_line_].frontend_id);
504 } else {
505 delegate_->ClearPreviewedForm();
509 void AutofillPopupControllerImpl::SelectNextLine() {
510 int new_selected_line = selected_line_ + 1;
512 // Skip over any lines that can't be selected.
513 while (static_cast<size_t>(new_selected_line) < GetLineCount() &&
514 !CanAccept(suggestions_[new_selected_line].frontend_id)) {
515 ++new_selected_line;
518 if (new_selected_line >= static_cast<int>(GetLineCount()))
519 new_selected_line = 0;
521 SetSelectedLine(new_selected_line);
524 void AutofillPopupControllerImpl::SelectPreviousLine() {
525 int new_selected_line = selected_line_ - 1;
527 // Skip over any lines that can't be selected.
528 while (new_selected_line > kNoSelection &&
529 !CanAccept(GetSuggestionAt(new_selected_line).frontend_id)) {
530 --new_selected_line;
533 if (new_selected_line <= kNoSelection)
534 new_selected_line = GetLineCount() - 1;
536 SetSelectedLine(new_selected_line);
539 bool AutofillPopupControllerImpl::RemoveSelectedLine() {
540 if (selected_line_ == kNoSelection)
541 return false;
543 DCHECK_GE(selected_line_, 0);
544 DCHECK_LT(selected_line_, static_cast<int>(GetLineCount()));
545 return RemoveSuggestion(selected_line_);
548 int AutofillPopupControllerImpl::LineFromY(int y) {
549 int current_height = kPopupBorderThickness;
551 for (size_t i = 0; i < suggestions_.size(); ++i) {
552 current_height += GetRowHeightFromId(suggestions_[i].frontend_id);
554 if (y <= current_height)
555 return i;
558 // The y value goes beyond the popup so stop the selection at the last line.
559 return GetLineCount() - 1;
562 int AutofillPopupControllerImpl::GetRowHeightFromId(int identifier) const {
563 if (identifier == POPUP_ITEM_ID_SEPARATOR)
564 return kSeparatorHeight;
566 return kRowHeight;
569 bool AutofillPopupControllerImpl::CanAccept(int id) {
570 return id != POPUP_ITEM_ID_SEPARATOR && id != POPUP_ITEM_ID_WARNING_MESSAGE &&
571 id != POPUP_ITEM_ID_TITLE;
574 bool AutofillPopupControllerImpl::HasSuggestions() {
575 if (suggestions_.empty())
576 return false;
577 int id = suggestions_[0].frontend_id;
578 return id > 0 ||
579 id == POPUP_ITEM_ID_AUTOCOMPLETE_ENTRY ||
580 id == POPUP_ITEM_ID_PASSWORD_ENTRY ||
581 id == POPUP_ITEM_ID_DATALIST_ENTRY ||
582 id == POPUP_ITEM_ID_SCAN_CREDIT_CARD;
585 void AutofillPopupControllerImpl::SetValues(
586 const std::vector<autofill::Suggestion>& suggestions) {
587 suggestions_ = suggestions;
588 elided_values_.resize(suggestions.size());
589 elided_labels_.resize(suggestions.size());
590 for (size_t i = 0; i < suggestions.size(); i++) {
591 elided_values_[i] = suggestions[i].value;
592 elided_labels_[i] = suggestions[i].label;
596 void AutofillPopupControllerImpl::ShowView() {
597 view_->Show();
600 void AutofillPopupControllerImpl::InvalidateRow(size_t row) {
601 DCHECK(0 <= row);
602 DCHECK(row < suggestions_.size());
603 view_->InvalidateRow(row);
606 #if !defined(OS_ANDROID)
607 int AutofillPopupControllerImpl::GetDesiredPopupWidth() const {
608 int popup_width = controller_common_->RoundedElementBounds().width();
609 for (size_t i = 0; i < GetLineCount(); ++i) {
610 int row_size =
611 gfx::GetStringWidth(GetElidedValueAt(i), value_font_list_) +
612 gfx::GetStringWidth(GetElidedLabelAt(i), label_font_list_) +
613 RowWidthWithoutText(i);
615 popup_width = std::max(popup_width, row_size);
618 return popup_width;
621 int AutofillPopupControllerImpl::GetDesiredPopupHeight() const {
622 int popup_height = 2 * kPopupBorderThickness;
624 for (size_t i = 0; i < suggestions_.size(); ++i) {
625 popup_height += GetRowHeightFromId(suggestions_[i].frontend_id);
628 return popup_height;
631 int AutofillPopupControllerImpl::RowWidthWithoutText(int row) const {
632 int row_size = kEndPadding;
634 if (!elided_labels_[row].empty())
635 row_size += kNamePadding;
637 // Add the Autofill icon size, if required.
638 const base::string16& icon = suggestions_[row].icon;
639 if (!icon.empty()) {
640 int icon_width = ui::ResourceBundle::GetSharedInstance().GetImageNamed(
641 GetIconResourceID(icon)).Width();
642 row_size += icon_width + kIconPadding;
645 // Add the padding at the end.
646 row_size += kEndPadding;
648 // Add room for the popup border.
649 row_size += 2 * kPopupBorderThickness;
651 return row_size;
654 void AutofillPopupControllerImpl::UpdatePopupBounds() {
655 int popup_width = GetDesiredPopupWidth();
656 int popup_height = GetDesiredPopupHeight();
658 popup_bounds_ = controller_common_->GetPopupBounds(popup_width, popup_height);
660 #endif // !defined(OS_ANDROID)
662 WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetWeakPtr() {
663 return weak_ptr_factory_.GetWeakPtr();
666 void AutofillPopupControllerImpl::ClearState() {
667 // Don't clear view_, because otherwise the popup will have to get regenerated
668 // and this will cause flickering.
670 popup_bounds_ = gfx::Rect();
672 suggestions_.clear();
673 elided_values_.clear();
674 elided_labels_.clear();
676 selected_line_ = kNoSelection;
679 } // namespace autofill