ExtensionSyncService: listen for relevant changes instead of being explicitly called...
[chromium-blink-merge.git] / chrome / browser / ui / autofill / autofill_popup_controller_impl.cc
blob5fa06d745301fe60d4aa744d2f9200e646a8bdee
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 #elif defined(OS_MACOSX) && !defined(OS_IOS)
69 { "macContactsIcon", IDR_AUTOFILL_MAC_CONTACTS_ICON },
70 #endif
73 } // namespace
75 // static
76 WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetOrCreate(
77 WeakPtr<AutofillPopupControllerImpl> previous,
78 WeakPtr<AutofillPopupDelegate> delegate,
79 content::WebContents* web_contents,
80 gfx::NativeView container_view,
81 const gfx::RectF& element_bounds,
82 base::i18n::TextDirection text_direction) {
83 if (previous.get() && previous->web_contents() == web_contents &&
84 previous->delegate_.get() == delegate.get() &&
85 previous->container_view() == container_view &&
86 previous->element_bounds() == element_bounds) {
87 previous->ClearState();
88 return previous;
91 if (previous.get())
92 previous->Hide();
94 AutofillPopupControllerImpl* controller =
95 new AutofillPopupControllerImpl(
96 delegate, web_contents, container_view, element_bounds,
97 text_direction);
98 return controller->GetWeakPtr();
101 AutofillPopupControllerImpl::AutofillPopupControllerImpl(
102 base::WeakPtr<AutofillPopupDelegate> delegate,
103 content::WebContents* web_contents,
104 gfx::NativeView container_view,
105 const gfx::RectF& element_bounds,
106 base::i18n::TextDirection text_direction)
107 : controller_common_(new PopupControllerCommon(element_bounds,
108 text_direction,
109 container_view,
110 web_contents)),
111 view_(NULL),
112 delegate_(delegate),
113 weak_ptr_factory_(this) {
114 ClearState();
115 controller_common_->SetKeyPressCallback(
116 base::Bind(&AutofillPopupControllerImpl::HandleKeyPressEvent,
117 base::Unretained(this)));
118 #if !defined(OS_ANDROID)
119 label_font_list_ = value_font_list_.DeriveWithSizeDelta(kLabelFontSizeDelta);
120 title_font_list_ = value_font_list_.DeriveWithStyle(gfx::Font::BOLD);
121 #if defined(OS_MACOSX)
122 // There is no italic version of the system font.
123 warning_font_list_ = value_font_list_;
124 #else
125 warning_font_list_ = value_font_list_.DeriveWithStyle(gfx::Font::ITALIC);
126 #endif
127 #endif
130 AutofillPopupControllerImpl::~AutofillPopupControllerImpl() {}
132 void AutofillPopupControllerImpl::Show(
133 const std::vector<autofill::Suggestion>& suggestions) {
134 SetValues(suggestions);
135 DCHECK_EQ(suggestions_.size(), elided_values_.size());
136 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
138 #if !defined(OS_ANDROID)
139 // Android displays the long text with ellipsis using the view attributes.
141 UpdatePopupBounds();
142 int popup_width = popup_bounds().width();
144 // Elide the name and label strings so that the popup fits in the available
145 // space.
146 for (size_t i = 0; i < suggestions_.size(); ++i) {
147 int value_width =
148 gfx::GetStringWidth(suggestions_[i].value, GetValueFontListForRow(i));
149 int label_width =
150 gfx::GetStringWidth(suggestions_[i].label, GetLabelFontList());
151 int total_text_length = value_width + label_width;
153 // The line can have no strings if it represents a UI element, such as
154 // a separator line.
155 if (total_text_length == 0)
156 continue;
158 int available_width = popup_width - RowWidthWithoutText(i);
160 // Each field receives space in proportion to its length.
161 int value_size = available_width * value_width / total_text_length;
162 elided_values_[i] = gfx::ElideText(suggestions_[i].value,
163 GetValueFontListForRow(i),
164 value_size, gfx::ELIDE_TAIL);
166 int label_size = available_width * label_width / total_text_length;
167 elided_labels_[i] = gfx::ElideText(suggestions_[i].label,
168 GetLabelFontList(),
169 label_size, gfx::ELIDE_TAIL);
171 #endif
173 if (!view_) {
174 view_ = AutofillPopupView::Create(this);
176 // It is possible to fail to create the popup, in this case
177 // treat the popup as hiding right away.
178 if (!view_) {
179 Hide();
180 return;
183 ShowView();
184 } else {
185 UpdateBoundsAndRedrawPopup();
188 controller_common_->RegisterKeyPressCallback();
189 delegate_->OnPopupShown();
191 DCHECK_EQ(suggestions_.size(), elided_values_.size());
192 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
195 void AutofillPopupControllerImpl::UpdateDataListValues(
196 const std::vector<base::string16>& values,
197 const std::vector<base::string16>& labels) {
198 DCHECK_EQ(suggestions_.size(), elided_values_.size());
199 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
201 // Remove all the old data list values, which should always be at the top of
202 // the list if they are present.
203 while (!suggestions_.empty() &&
204 suggestions_[0].frontend_id == POPUP_ITEM_ID_DATALIST_ENTRY) {
205 suggestions_.erase(suggestions_.begin());
206 elided_values_.erase(elided_values_.begin());
207 elided_labels_.erase(elided_labels_.begin());
210 // If there are no new data list values, exit (clearing the separator if there
211 // is one).
212 if (values.empty()) {
213 if (!suggestions_.empty() &&
214 suggestions_[0].frontend_id == POPUP_ITEM_ID_SEPARATOR) {
215 suggestions_.erase(suggestions_.begin());
216 elided_values_.erase(elided_values_.begin());
217 elided_labels_.erase(elided_labels_.begin());
220 // The popup contents have changed, so either update the bounds or hide it.
221 if (HasSuggestions())
222 UpdateBoundsAndRedrawPopup();
223 else
224 Hide();
226 return;
229 // Add a separator if there are any other values.
230 if (!suggestions_.empty() &&
231 suggestions_[0].frontend_id != POPUP_ITEM_ID_SEPARATOR) {
232 suggestions_.insert(suggestions_.begin(), autofill::Suggestion());
233 suggestions_[0].frontend_id = POPUP_ITEM_ID_SEPARATOR;
234 elided_values_.insert(elided_values_.begin(), base::string16());
235 elided_labels_.insert(elided_labels_.begin(), base::string16());
238 // Prepend the parameters to the suggestions we already have.
239 suggestions_.insert(suggestions_.begin(), values.size(), Suggestion());
240 elided_values_.insert(elided_values_.begin(), values.size(),
241 base::string16());
242 elided_labels_.insert(elided_labels_.begin(), values.size(),
243 base::string16());
244 for (size_t i = 0; i < values.size(); i++) {
245 suggestions_[i].value = values[i];
246 suggestions_[i].label = labels[i];
247 suggestions_[i].frontend_id = POPUP_ITEM_ID_DATALIST_ENTRY;
249 // TODO(brettw) it looks like these should be elided.
250 elided_values_[i] = values[i];
251 elided_labels_[i] = labels[i];
254 UpdateBoundsAndRedrawPopup();
255 DCHECK_EQ(suggestions_.size(), elided_values_.size());
256 DCHECK_EQ(suggestions_.size(), elided_labels_.size());
259 void AutofillPopupControllerImpl::Hide() {
260 controller_common_->RemoveKeyPressCallback();
261 if (delegate_)
262 delegate_->OnPopupHidden();
264 if (view_)
265 view_->Hide();
267 delete this;
270 void AutofillPopupControllerImpl::ViewDestroyed() {
271 // The view has already been destroyed so clear the reference to it.
272 view_ = NULL;
274 Hide();
277 bool AutofillPopupControllerImpl::HandleKeyPressEvent(
278 const content::NativeWebKeyboardEvent& event) {
279 switch (event.windowsKeyCode) {
280 case ui::VKEY_UP:
281 SelectPreviousLine();
282 return true;
283 case ui::VKEY_DOWN:
284 SelectNextLine();
285 return true;
286 case ui::VKEY_PRIOR: // Page up.
287 // Set no line and then select the next line in case the first line is not
288 // selectable.
289 SetSelectedLine(kNoSelection);
290 SelectNextLine();
291 return true;
292 case ui::VKEY_NEXT: // Page down.
293 SetSelectedLine(GetLineCount() - 1);
294 return true;
295 case ui::VKEY_ESCAPE:
296 Hide();
297 return true;
298 case ui::VKEY_DELETE:
299 return (event.modifiers & content::NativeWebKeyboardEvent::ShiftKey) &&
300 RemoveSelectedLine();
301 case ui::VKEY_TAB:
302 // A tab press should cause the selected line to be accepted, but still
303 // return false so the tab key press propagates and changes the cursor
304 // location.
305 AcceptSelectedLine();
306 return false;
307 case ui::VKEY_RETURN:
308 return AcceptSelectedLine();
309 default:
310 return false;
314 void AutofillPopupControllerImpl::UpdateBoundsAndRedrawPopup() {
315 #if !defined(OS_ANDROID)
316 // TODO(csharp): Since UpdatePopupBounds can change the position of the popup,
317 // the popup could end up jumping from above the element to below it.
318 // It is unclear if it is better to keep the popup where it was, or if it
319 // should try and move to its desired position.
320 UpdatePopupBounds();
321 #endif
323 view_->UpdateBoundsAndRedrawPopup();
326 void AutofillPopupControllerImpl::SetSelectionAtPoint(const gfx::Point& point) {
327 SetSelectedLine(LineFromY(point.y()));
330 bool AutofillPopupControllerImpl::AcceptSelectedLine() {
331 if (selected_line_ == kNoSelection)
332 return false;
334 DCHECK_GE(selected_line_, 0);
335 DCHECK_LT(selected_line_, static_cast<int>(GetLineCount()));
337 if (!CanAccept(suggestions_[selected_line_].frontend_id))
338 return false;
340 AcceptSuggestion(selected_line_);
341 return true;
344 void AutofillPopupControllerImpl::SelectionCleared() {
345 SetSelectedLine(kNoSelection);
348 void AutofillPopupControllerImpl::AcceptSuggestion(size_t index) {
349 const autofill::Suggestion& suggestion = suggestions_[index];
350 delegate_->DidAcceptSuggestion(suggestion.value, suggestion.frontend_id,
351 index);
354 int AutofillPopupControllerImpl::GetIconResourceID(
355 const base::string16& resource_name) const {
356 int result = -1;
357 for (size_t i = 0; i < arraysize(kDataResources); ++i) {
358 if (resource_name == base::ASCIIToUTF16(kDataResources[i].name)) {
359 result = kDataResources[i].id;
360 break;
364 #if defined(OS_ANDROID)
365 if (result == IDR_AUTOFILL_CC_SCAN_NEW && IsKeyboardAccessoryEnabled())
366 result = IDR_AUTOFILL_CC_SCAN_NEW_KEYBOARD_ACCESSORY;
367 #endif // OS_ANDROID
369 return result;
372 bool AutofillPopupControllerImpl::IsWarning(size_t index) const {
373 return suggestions_[index].frontend_id == POPUP_ITEM_ID_WARNING_MESSAGE;
376 gfx::Rect AutofillPopupControllerImpl::GetRowBounds(size_t index) {
377 int top = kPopupBorderThickness;
378 for (size_t i = 0; i < index; ++i) {
379 top += GetRowHeightFromId(suggestions_[i].frontend_id);
382 return gfx::Rect(
383 kPopupBorderThickness,
384 top,
385 popup_bounds_.width() - 2 * kPopupBorderThickness,
386 GetRowHeightFromId(suggestions_[index].frontend_id));
389 void AutofillPopupControllerImpl::SetPopupBounds(const gfx::Rect& bounds) {
390 popup_bounds_ = bounds;
391 UpdateBoundsAndRedrawPopup();
394 const gfx::Rect& AutofillPopupControllerImpl::popup_bounds() const {
395 return popup_bounds_;
398 content::WebContents* AutofillPopupControllerImpl::web_contents() {
399 return controller_common_->web_contents();
402 gfx::NativeView AutofillPopupControllerImpl::container_view() {
403 return controller_common_->container_view();
406 const gfx::RectF& AutofillPopupControllerImpl::element_bounds() const {
407 return controller_common_->element_bounds();
410 bool AutofillPopupControllerImpl::IsRTL() const {
411 return controller_common_->is_rtl();
414 size_t AutofillPopupControllerImpl::GetLineCount() const {
415 return suggestions_.size();
418 const autofill::Suggestion& AutofillPopupControllerImpl::GetSuggestionAt(
419 size_t row) const {
420 return suggestions_[row];
423 const base::string16& AutofillPopupControllerImpl::GetElidedValueAt(
424 size_t row) const {
425 return elided_values_[row];
428 const base::string16& AutofillPopupControllerImpl::GetElidedLabelAt(
429 size_t row) const {
430 return elided_labels_[row];
433 bool AutofillPopupControllerImpl::GetRemovalConfirmationText(
434 int list_index,
435 base::string16* title,
436 base::string16* body) {
437 return delegate_->GetDeletionConfirmationText(
438 suggestions_[list_index].value, suggestions_[list_index].frontend_id,
439 title, body);
442 bool AutofillPopupControllerImpl::RemoveSuggestion(int list_index) {
443 if (!delegate_->RemoveSuggestion(suggestions_[list_index].value,
444 suggestions_[list_index].frontend_id)) {
445 return false;
448 // Remove the deleted element.
449 suggestions_.erase(suggestions_.begin() + list_index);
450 elided_values_.erase(elided_values_.begin() + list_index);
451 elided_labels_.erase(elided_labels_.begin() + list_index);
453 SetSelectedLine(kNoSelection);
455 if (HasSuggestions()) {
456 delegate_->ClearPreviewedForm();
457 UpdateBoundsAndRedrawPopup();
458 } else {
459 Hide();
462 return true;
465 #if !defined(OS_ANDROID)
466 const gfx::FontList& AutofillPopupControllerImpl::GetValueFontListForRow(
467 size_t index) const {
468 if (suggestions_[index].frontend_id == POPUP_ITEM_ID_WARNING_MESSAGE)
469 return warning_font_list_;
471 if (suggestions_[index].frontend_id == POPUP_ITEM_ID_TITLE)
472 return title_font_list_;
474 return value_font_list_;
477 const gfx::FontList& AutofillPopupControllerImpl::GetLabelFontList() const {
478 return label_font_list_;
480 #endif
482 int AutofillPopupControllerImpl::selected_line() const {
483 return selected_line_;
486 void AutofillPopupControllerImpl::SetSelectedLine(int selected_line) {
487 if (selected_line_ == selected_line)
488 return;
490 if (selected_line_ != kNoSelection &&
491 static_cast<size_t>(selected_line_) < suggestions_.size())
492 InvalidateRow(selected_line_);
494 if (selected_line != kNoSelection) {
495 InvalidateRow(selected_line);
497 if (!CanAccept(suggestions_[selected_line].frontend_id))
498 selected_line = kNoSelection;
501 selected_line_ = selected_line;
503 if (selected_line_ != kNoSelection) {
504 delegate_->DidSelectSuggestion(suggestions_[selected_line_].value,
505 suggestions_[selected_line_].frontend_id);
506 } else {
507 delegate_->ClearPreviewedForm();
511 void AutofillPopupControllerImpl::SelectNextLine() {
512 int new_selected_line = selected_line_ + 1;
514 // Skip over any lines that can't be selected.
515 while (static_cast<size_t>(new_selected_line) < GetLineCount() &&
516 !CanAccept(suggestions_[new_selected_line].frontend_id)) {
517 ++new_selected_line;
520 if (new_selected_line >= static_cast<int>(GetLineCount()))
521 new_selected_line = 0;
523 SetSelectedLine(new_selected_line);
526 void AutofillPopupControllerImpl::SelectPreviousLine() {
527 int new_selected_line = selected_line_ - 1;
529 // Skip over any lines that can't be selected.
530 while (new_selected_line > kNoSelection &&
531 !CanAccept(GetSuggestionAt(new_selected_line).frontend_id)) {
532 --new_selected_line;
535 if (new_selected_line <= kNoSelection)
536 new_selected_line = GetLineCount() - 1;
538 SetSelectedLine(new_selected_line);
541 bool AutofillPopupControllerImpl::RemoveSelectedLine() {
542 if (selected_line_ == kNoSelection)
543 return false;
545 DCHECK_GE(selected_line_, 0);
546 DCHECK_LT(selected_line_, static_cast<int>(GetLineCount()));
547 return RemoveSuggestion(selected_line_);
550 int AutofillPopupControllerImpl::LineFromY(int y) {
551 int current_height = kPopupBorderThickness;
553 for (size_t i = 0; i < suggestions_.size(); ++i) {
554 current_height += GetRowHeightFromId(suggestions_[i].frontend_id);
556 if (y <= current_height)
557 return i;
560 // The y value goes beyond the popup so stop the selection at the last line.
561 return GetLineCount() - 1;
564 int AutofillPopupControllerImpl::GetRowHeightFromId(int identifier) const {
565 if (identifier == POPUP_ITEM_ID_SEPARATOR)
566 return kSeparatorHeight;
568 return kRowHeight;
571 bool AutofillPopupControllerImpl::CanAccept(int id) {
572 return id != POPUP_ITEM_ID_SEPARATOR && id != POPUP_ITEM_ID_WARNING_MESSAGE &&
573 id != POPUP_ITEM_ID_TITLE;
576 bool AutofillPopupControllerImpl::HasSuggestions() {
577 if (suggestions_.empty())
578 return false;
579 int id = suggestions_[0].frontend_id;
580 return id > 0 ||
581 id == POPUP_ITEM_ID_AUTOCOMPLETE_ENTRY ||
582 id == POPUP_ITEM_ID_PASSWORD_ENTRY ||
583 id == POPUP_ITEM_ID_DATALIST_ENTRY ||
584 id == POPUP_ITEM_ID_MAC_ACCESS_CONTACTS ||
585 id == POPUP_ITEM_ID_SCAN_CREDIT_CARD;
588 void AutofillPopupControllerImpl::SetValues(
589 const std::vector<autofill::Suggestion>& suggestions) {
590 suggestions_ = suggestions;
591 elided_values_.resize(suggestions.size());
592 elided_labels_.resize(suggestions.size());
593 for (size_t i = 0; i < suggestions.size(); i++) {
594 elided_values_[i] = suggestions[i].value;
595 elided_labels_[i] = suggestions[i].label;
599 void AutofillPopupControllerImpl::ShowView() {
600 view_->Show();
603 void AutofillPopupControllerImpl::InvalidateRow(size_t row) {
604 DCHECK(0 <= row);
605 DCHECK(row < suggestions_.size());
606 view_->InvalidateRow(row);
609 #if !defined(OS_ANDROID)
610 int AutofillPopupControllerImpl::GetDesiredPopupWidth() const {
611 int popup_width = controller_common_->RoundedElementBounds().width();
612 for (size_t i = 0; i < GetLineCount(); ++i) {
613 int row_size =
614 gfx::GetStringWidth(GetElidedValueAt(i), value_font_list_) +
615 gfx::GetStringWidth(GetElidedLabelAt(i), label_font_list_) +
616 RowWidthWithoutText(i);
618 popup_width = std::max(popup_width, row_size);
621 return popup_width;
624 int AutofillPopupControllerImpl::GetDesiredPopupHeight() const {
625 int popup_height = 2 * kPopupBorderThickness;
627 for (size_t i = 0; i < suggestions_.size(); ++i) {
628 popup_height += GetRowHeightFromId(suggestions_[i].frontend_id);
631 return popup_height;
634 int AutofillPopupControllerImpl::RowWidthWithoutText(int row) const {
635 int row_size = kEndPadding;
637 if (!elided_labels_[row].empty())
638 row_size += kNamePadding;
640 // Add the Autofill icon size, if required.
641 const base::string16& icon = suggestions_[row].icon;
642 if (!icon.empty()) {
643 int icon_width = ui::ResourceBundle::GetSharedInstance().GetImageNamed(
644 GetIconResourceID(icon)).Width();
645 row_size += icon_width + kIconPadding;
648 // Add the padding at the end.
649 row_size += kEndPadding;
651 // Add room for the popup border.
652 row_size += 2 * kPopupBorderThickness;
654 return row_size;
657 void AutofillPopupControllerImpl::UpdatePopupBounds() {
658 int popup_width = GetDesiredPopupWidth();
659 int popup_height = GetDesiredPopupHeight();
661 popup_bounds_ = controller_common_->GetPopupBounds(popup_width, popup_height);
663 #endif // !defined(OS_ANDROID)
665 WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetWeakPtr() {
666 return weak_ptr_factory_.GetWeakPtr();
669 void AutofillPopupControllerImpl::ClearState() {
670 // Don't clear view_, because otherwise the popup will have to get regenerated
671 // and this will cause flickering.
673 popup_bounds_ = gfx::Rect();
675 suggestions_.clear();
676 elided_values_.clear();
677 elided_labels_.clear();
679 selected_line_ = kNoSelection;
682 } // namespace autofill