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"
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"
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
;
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
},
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();
94 AutofillPopupControllerImpl
* controller
=
95 new AutofillPopupControllerImpl(
96 delegate
, web_contents
, container_view
, element_bounds
,
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
,
113 weak_ptr_factory_(this) {
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_
;
125 warning_font_list_
= value_font_list_
.DeriveWithStyle(gfx::Font::ITALIC
);
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.
142 int popup_width
= popup_bounds().width();
144 // Elide the name and label strings so that the popup fits in the available
146 for (size_t i
= 0; i
< suggestions_
.size(); ++i
) {
148 gfx::GetStringWidth(suggestions_
[i
].value
, GetValueFontListForRow(i
));
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
155 if (total_text_length
== 0)
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
,
169 label_size
, gfx::ELIDE_TAIL
);
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.
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
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();
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(),
242 elided_labels_
.insert(elided_labels_
.begin(), values
.size(),
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();
262 delegate_
->OnPopupHidden();
270 void AutofillPopupControllerImpl::ViewDestroyed() {
271 // The view has already been destroyed so clear the reference to it.
277 bool AutofillPopupControllerImpl::HandleKeyPressEvent(
278 const content::NativeWebKeyboardEvent
& event
) {
279 switch (event
.windowsKeyCode
) {
281 SelectPreviousLine();
286 case ui::VKEY_PRIOR
: // Page up.
287 // Set no line and then select the next line in case the first line is not
289 SetSelectedLine(kNoSelection
);
292 case ui::VKEY_NEXT
: // Page down.
293 SetSelectedLine(GetLineCount() - 1);
295 case ui::VKEY_ESCAPE
:
298 case ui::VKEY_DELETE
:
299 return (event
.modifiers
& content::NativeWebKeyboardEvent::ShiftKey
) &&
300 RemoveSelectedLine();
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
305 AcceptSelectedLine();
307 case ui::VKEY_RETURN
:
308 return AcceptSelectedLine();
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.
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
)
334 DCHECK_GE(selected_line_
, 0);
335 DCHECK_LT(selected_line_
, static_cast<int>(GetLineCount()));
337 if (!CanAccept(suggestions_
[selected_line_
].frontend_id
))
340 AcceptSuggestion(selected_line_
);
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
,
354 int AutofillPopupControllerImpl::GetIconResourceID(
355 const base::string16
& resource_name
) const {
357 for (size_t i
= 0; i
< arraysize(kDataResources
); ++i
) {
358 if (resource_name
== base::ASCIIToUTF16(kDataResources
[i
].name
)) {
359 result
= kDataResources
[i
].id
;
364 #if defined(OS_ANDROID)
365 if (result
== IDR_AUTOFILL_CC_SCAN_NEW
&& IsKeyboardAccessoryEnabled())
366 result
= IDR_AUTOFILL_CC_SCAN_NEW_KEYBOARD_ACCESSORY
;
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
);
383 kPopupBorderThickness
,
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(
420 return suggestions_
[row
];
423 const base::string16
& AutofillPopupControllerImpl::GetElidedValueAt(
425 return elided_values_
[row
];
428 const base::string16
& AutofillPopupControllerImpl::GetElidedLabelAt(
430 return elided_labels_
[row
];
433 bool AutofillPopupControllerImpl::GetRemovalConfirmationText(
435 base::string16
* title
,
436 base::string16
* body
) {
437 return delegate_
->GetDeletionConfirmationText(
438 suggestions_
[list_index
].value
, suggestions_
[list_index
].frontend_id
,
442 bool AutofillPopupControllerImpl::RemoveSuggestion(int list_index
) {
443 if (!delegate_
->RemoveSuggestion(suggestions_
[list_index
].value
,
444 suggestions_
[list_index
].frontend_id
)) {
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();
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_
;
482 int AutofillPopupControllerImpl::selected_line() const {
483 return selected_line_
;
486 void AutofillPopupControllerImpl::SetSelectedLine(int selected_line
) {
487 if (selected_line_
== selected_line
)
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
);
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
)) {
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
)) {
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
)
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
)
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
;
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())
579 int id
= suggestions_
[0].frontend_id
;
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() {
603 void AutofillPopupControllerImpl::InvalidateRow(size_t 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
) {
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
);
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
);
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
;
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
;
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