blob: 3902ab93b843bc17d68e875687c71530009cf515 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
#include <algorithm>
#include <utility>
#include "base/command_line.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/ui/autofill/autofill_popup_view.h"
#include "chrome/browser/ui/autofill/popup_constants.h"
#include "components/autofill/core/browser/autofill_popup_delegate.h"
#include "components/autofill/core/browser/popup_item_ids.h"
#include "components/autofill/core/browser/suggestion.h"
#include "components/autofill/core/common/autofill_util.h"
#include "content/public/browser/native_web_keyboard_event.h"
#include "grit/components_scaled_resources.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/screen.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
using base::WeakPtr;
namespace autofill {
namespace {
// Used to indicate that no line is currently selected by the user.
const int kNoSelection = -1;
// The vertical height of each row in pixels.
const size_t kRowHeight = 24;
// The vertical height of a separator in pixels.
const size_t kSeparatorHeight = 1;
#if !defined(OS_ANDROID)
// Size difference between name and label in pixels.
const int kLabelFontSizeDelta = -2;
const size_t kNamePadding = AutofillPopupView::kNamePadding;
const size_t kIconPadding = AutofillPopupView::kIconPadding;
const size_t kEndPadding = AutofillPopupView::kEndPadding;
#endif
struct DataResource {
const char* name;
int id;
};
const DataResource kDataResources[] = {
{ "americanExpressCC", IDR_AUTOFILL_CC_AMEX },
{ "dinersCC", IDR_AUTOFILL_CC_GENERIC },
{ "discoverCC", IDR_AUTOFILL_CC_DISCOVER },
{ "genericCC", IDR_AUTOFILL_CC_GENERIC },
{ "jcbCC", IDR_AUTOFILL_CC_GENERIC },
{ "masterCardCC", IDR_AUTOFILL_CC_MASTERCARD },
{ "visaCC", IDR_AUTOFILL_CC_VISA },
#if defined(OS_ANDROID)
{ "scanCreditCardIcon", IDR_AUTOFILL_CC_SCAN_NEW },
{ "settings", IDR_AUTOFILL_SETTINGS },
#endif
};
} // namespace
// static
WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetOrCreate(
WeakPtr<AutofillPopupControllerImpl> previous,
WeakPtr<AutofillPopupDelegate> delegate,
content::WebContents* web_contents,
gfx::NativeView container_view,
const gfx::RectF& element_bounds,
base::i18n::TextDirection text_direction) {
if (previous.get() && previous->web_contents() == web_contents &&
previous->delegate_.get() == delegate.get() &&
previous->container_view() == container_view &&
previous->element_bounds() == element_bounds) {
previous->ClearState();
return previous;
}
if (previous.get())
previous->Hide();
AutofillPopupControllerImpl* controller =
new AutofillPopupControllerImpl(
delegate, web_contents, container_view, element_bounds,
text_direction);
return controller->GetWeakPtr();
}
AutofillPopupControllerImpl::AutofillPopupControllerImpl(
base::WeakPtr<AutofillPopupDelegate> delegate,
content::WebContents* web_contents,
gfx::NativeView container_view,
const gfx::RectF& element_bounds,
base::i18n::TextDirection text_direction)
: controller_common_(new PopupControllerCommon(element_bounds,
text_direction,
container_view,
web_contents)),
view_(NULL),
delegate_(delegate),
weak_ptr_factory_(this) {
ClearState();
controller_common_->SetKeyPressCallback(
base::Bind(&AutofillPopupControllerImpl::HandleKeyPressEvent,
base::Unretained(this)));
#if !defined(OS_ANDROID)
label_font_list_ = value_font_list_.DeriveWithSizeDelta(kLabelFontSizeDelta);
title_font_list_ = value_font_list_.DeriveWithStyle(gfx::Font::BOLD);
#if defined(OS_MACOSX)
// There is no italic version of the system font.
warning_font_list_ = value_font_list_;
#else
warning_font_list_ = value_font_list_.DeriveWithStyle(gfx::Font::ITALIC);
#endif
#endif
}
AutofillPopupControllerImpl::~AutofillPopupControllerImpl() {}
void AutofillPopupControllerImpl::Show(
const std::vector<autofill::Suggestion>& suggestions) {
SetValues(suggestions);
DCHECK_EQ(suggestions_.size(), elided_values_.size());
DCHECK_EQ(suggestions_.size(), elided_labels_.size());
#if !defined(OS_ANDROID)
// Android displays the long text with ellipsis using the view attributes.
UpdatePopupBounds();
int popup_width = popup_bounds().width();
// Elide the name and label strings so that the popup fits in the available
// space.
for (size_t i = 0; i < suggestions_.size(); ++i) {
int value_width =
gfx::GetStringWidth(suggestions_[i].value, GetValueFontListForRow(i));
int label_width =
gfx::GetStringWidth(suggestions_[i].label, GetLabelFontList());
int total_text_length = value_width + label_width;
// The line can have no strings if it represents a UI element, such as
// a separator line.
if (total_text_length == 0)
continue;
int available_width = popup_width - RowWidthWithoutText(i);
// Each field receives space in proportion to its length.
int value_size = available_width * value_width / total_text_length;
elided_values_[i] = gfx::ElideText(suggestions_[i].value,
GetValueFontListForRow(i),
value_size, gfx::ELIDE_TAIL);
int label_size = available_width * label_width / total_text_length;
elided_labels_[i] = gfx::ElideText(suggestions_[i].label,
GetLabelFontList(),
label_size, gfx::ELIDE_TAIL);
}
#endif
if (!view_) {
view_ = AutofillPopupView::Create(this);
// It is possible to fail to create the popup, in this case
// treat the popup as hiding right away.
if (!view_) {
Hide();
return;
}
ShowView();
} else {
UpdateBoundsAndRedrawPopup();
}
controller_common_->RegisterKeyPressCallback();
delegate_->OnPopupShown();
DCHECK_EQ(suggestions_.size(), elided_values_.size());
DCHECK_EQ(suggestions_.size(), elided_labels_.size());
}
void AutofillPopupControllerImpl::UpdateDataListValues(
const std::vector<base::string16>& values,
const std::vector<base::string16>& labels) {
DCHECK_EQ(suggestions_.size(), elided_values_.size());
DCHECK_EQ(suggestions_.size(), elided_labels_.size());
// Remove all the old data list values, which should always be at the top of
// the list if they are present.
while (!suggestions_.empty() &&
suggestions_[0].frontend_id == POPUP_ITEM_ID_DATALIST_ENTRY) {
suggestions_.erase(suggestions_.begin());
elided_values_.erase(elided_values_.begin());
elided_labels_.erase(elided_labels_.begin());
}
// If there are no new data list values, exit (clearing the separator if there
// is one).
if (values.empty()) {
if (!suggestions_.empty() &&
suggestions_[0].frontend_id == POPUP_ITEM_ID_SEPARATOR) {
suggestions_.erase(suggestions_.begin());
elided_values_.erase(elided_values_.begin());
elided_labels_.erase(elided_labels_.begin());
}
// The popup contents have changed, so either update the bounds or hide it.
if (HasSuggestions())
UpdateBoundsAndRedrawPopup();
else
Hide();
return;
}
// Add a separator if there are any other values.
if (!suggestions_.empty() &&
suggestions_[0].frontend_id != POPUP_ITEM_ID_SEPARATOR) {
suggestions_.insert(suggestions_.begin(), autofill::Suggestion());
suggestions_[0].frontend_id = POPUP_ITEM_ID_SEPARATOR;
elided_values_.insert(elided_values_.begin(), base::string16());
elided_labels_.insert(elided_labels_.begin(), base::string16());
}
// Prepend the parameters to the suggestions we already have.
suggestions_.insert(suggestions_.begin(), values.size(), Suggestion());
elided_values_.insert(elided_values_.begin(), values.size(),
base::string16());
elided_labels_.insert(elided_labels_.begin(), values.size(),
base::string16());
for (size_t i = 0; i < values.size(); i++) {
suggestions_[i].value = values[i];
suggestions_[i].label = labels[i];
suggestions_[i].frontend_id = POPUP_ITEM_ID_DATALIST_ENTRY;
// TODO(brettw) it looks like these should be elided.
elided_values_[i] = values[i];
elided_labels_[i] = labels[i];
}
UpdateBoundsAndRedrawPopup();
DCHECK_EQ(suggestions_.size(), elided_values_.size());
DCHECK_EQ(suggestions_.size(), elided_labels_.size());
}
void AutofillPopupControllerImpl::Hide() {
controller_common_->RemoveKeyPressCallback();
if (delegate_)
delegate_->OnPopupHidden();
if (view_)
view_->Hide();
delete this;
}
void AutofillPopupControllerImpl::ViewDestroyed() {
// The view has already been destroyed so clear the reference to it.
view_ = NULL;
Hide();
}
bool AutofillPopupControllerImpl::HandleKeyPressEvent(
const content::NativeWebKeyboardEvent& event) {
switch (event.windowsKeyCode) {
case ui::VKEY_UP:
SelectPreviousLine();
return true;
case ui::VKEY_DOWN:
SelectNextLine();
return true;
case ui::VKEY_PRIOR: // Page up.
// Set no line and then select the next line in case the first line is not
// selectable.
SetSelectedLine(kNoSelection);
SelectNextLine();
return true;
case ui::VKEY_NEXT: // Page down.
SetSelectedLine(GetLineCount() - 1);
return true;
case ui::VKEY_ESCAPE:
Hide();
return true;
case ui::VKEY_DELETE:
return (event.modifiers & content::NativeWebKeyboardEvent::ShiftKey) &&
RemoveSelectedLine();
case ui::VKEY_TAB:
// A tab press should cause the selected line to be accepted, but still
// return false so the tab key press propagates and changes the cursor
// location.
AcceptSelectedLine();
return false;
case ui::VKEY_RETURN:
return AcceptSelectedLine();
default:
return false;
}
}
void AutofillPopupControllerImpl::UpdateBoundsAndRedrawPopup() {
#if !defined(OS_ANDROID)
// TODO(csharp): Since UpdatePopupBounds can change the position of the popup,
// the popup could end up jumping from above the element to below it.
// It is unclear if it is better to keep the popup where it was, or if it
// should try and move to its desired position.
UpdatePopupBounds();
#endif
view_->UpdateBoundsAndRedrawPopup();
}
void AutofillPopupControllerImpl::SetSelectionAtPoint(const gfx::Point& point) {
SetSelectedLine(LineFromY(point.y()));
}
bool AutofillPopupControllerImpl::AcceptSelectedLine() {
if (selected_line_ == kNoSelection)
return false;
DCHECK_GE(selected_line_, 0);
DCHECK_LT(selected_line_, static_cast<int>(GetLineCount()));
if (!CanAccept(suggestions_[selected_line_].frontend_id))
return false;
AcceptSuggestion(selected_line_);
return true;
}
void AutofillPopupControllerImpl::SelectionCleared() {
SetSelectedLine(kNoSelection);
}
void AutofillPopupControllerImpl::AcceptSuggestion(size_t index) {
const autofill::Suggestion& suggestion = suggestions_[index];
delegate_->DidAcceptSuggestion(suggestion.value, suggestion.frontend_id,
index);
}
int AutofillPopupControllerImpl::GetIconResourceID(
const base::string16& resource_name) const {
int result = -1;
for (size_t i = 0; i < arraysize(kDataResources); ++i) {
if (resource_name == base::ASCIIToUTF16(kDataResources[i].name)) {
result = kDataResources[i].id;
break;
}
}
#if defined(OS_ANDROID)
if (result == IDR_AUTOFILL_CC_SCAN_NEW && IsKeyboardAccessoryEnabled())
result = IDR_AUTOFILL_CC_SCAN_NEW_KEYBOARD_ACCESSORY;
#endif // OS_ANDROID
return result;
}
bool AutofillPopupControllerImpl::IsWarning(size_t index) const {
return suggestions_[index].frontend_id == POPUP_ITEM_ID_WARNING_MESSAGE;
}
gfx::Rect AutofillPopupControllerImpl::GetRowBounds(size_t index) {
int top = kPopupBorderThickness;
for (size_t i = 0; i < index; ++i) {
top += GetRowHeightFromId(suggestions_[i].frontend_id);
}
return gfx::Rect(
kPopupBorderThickness,
top,
popup_bounds_.width() - 2 * kPopupBorderThickness,
GetRowHeightFromId(suggestions_[index].frontend_id));
}
const gfx::Rect& AutofillPopupControllerImpl::popup_bounds() const {
return popup_bounds_;
}
content::WebContents* AutofillPopupControllerImpl::web_contents() {
return controller_common_->web_contents();
}
gfx::NativeView AutofillPopupControllerImpl::container_view() {
return controller_common_->container_view();
}
const gfx::RectF& AutofillPopupControllerImpl::element_bounds() const {
return controller_common_->element_bounds();
}
bool AutofillPopupControllerImpl::IsRTL() const {
return controller_common_->is_rtl();
}
size_t AutofillPopupControllerImpl::GetLineCount() const {
return suggestions_.size();
}
const autofill::Suggestion& AutofillPopupControllerImpl::GetSuggestionAt(
size_t row) const {
return suggestions_[row];
}
const base::string16& AutofillPopupControllerImpl::GetElidedValueAt(
size_t row) const {
return elided_values_[row];
}
const base::string16& AutofillPopupControllerImpl::GetElidedLabelAt(
size_t row) const {
return elided_labels_[row];
}
bool AutofillPopupControllerImpl::GetRemovalConfirmationText(
int list_index,
base::string16* title,
base::string16* body) {
return delegate_->GetDeletionConfirmationText(
suggestions_[list_index].value, suggestions_[list_index].frontend_id,
title, body);
}
bool AutofillPopupControllerImpl::RemoveSuggestion(int list_index) {
if (!delegate_->RemoveSuggestion(suggestions_[list_index].value,
suggestions_[list_index].frontend_id)) {
return false;
}
// Remove the deleted element.
suggestions_.erase(suggestions_.begin() + list_index);
elided_values_.erase(elided_values_.begin() + list_index);
elided_labels_.erase(elided_labels_.begin() + list_index);
SetSelectedLine(kNoSelection);
if (HasSuggestions()) {
delegate_->ClearPreviewedForm();
UpdateBoundsAndRedrawPopup();
} else {
Hide();
}
return true;
}
#if !defined(OS_ANDROID)
const gfx::FontList& AutofillPopupControllerImpl::GetValueFontListForRow(
size_t index) const {
if (suggestions_[index].frontend_id == POPUP_ITEM_ID_WARNING_MESSAGE)
return warning_font_list_;
if (suggestions_[index].frontend_id == POPUP_ITEM_ID_TITLE)
return title_font_list_;
return value_font_list_;
}
const gfx::FontList& AutofillPopupControllerImpl::GetLabelFontList() const {
return label_font_list_;
}
#endif
int AutofillPopupControllerImpl::selected_line() const {
return selected_line_;
}
void AutofillPopupControllerImpl::SetSelectedLine(int selected_line) {
if (selected_line_ == selected_line)
return;
if (selected_line_ != kNoSelection &&
static_cast<size_t>(selected_line_) < suggestions_.size())
InvalidateRow(selected_line_);
if (selected_line != kNoSelection) {
InvalidateRow(selected_line);
if (!CanAccept(suggestions_[selected_line].frontend_id))
selected_line = kNoSelection;
}
selected_line_ = selected_line;
if (selected_line_ != kNoSelection) {
delegate_->DidSelectSuggestion(suggestions_[selected_line_].value,
suggestions_[selected_line_].frontend_id);
} else {
delegate_->ClearPreviewedForm();
}
}
void AutofillPopupControllerImpl::SelectNextLine() {
int new_selected_line = selected_line_ + 1;
// Skip over any lines that can't be selected.
while (static_cast<size_t>(new_selected_line) < GetLineCount() &&
!CanAccept(suggestions_[new_selected_line].frontend_id)) {
++new_selected_line;
}
if (new_selected_line >= static_cast<int>(GetLineCount()))
new_selected_line = 0;
SetSelectedLine(new_selected_line);
}
void AutofillPopupControllerImpl::SelectPreviousLine() {
int new_selected_line = selected_line_ - 1;
// Skip over any lines that can't be selected.
while (new_selected_line > kNoSelection &&
!CanAccept(GetSuggestionAt(new_selected_line).frontend_id)) {
--new_selected_line;
}
if (new_selected_line <= kNoSelection)
new_selected_line = GetLineCount() - 1;
SetSelectedLine(new_selected_line);
}
bool AutofillPopupControllerImpl::RemoveSelectedLine() {
if (selected_line_ == kNoSelection)
return false;
DCHECK_GE(selected_line_, 0);
DCHECK_LT(selected_line_, static_cast<int>(GetLineCount()));
return RemoveSuggestion(selected_line_);
}
int AutofillPopupControllerImpl::LineFromY(int y) {
int current_height = kPopupBorderThickness;
for (size_t i = 0; i < suggestions_.size(); ++i) {
current_height += GetRowHeightFromId(suggestions_[i].frontend_id);
if (y <= current_height)
return i;
}
// The y value goes beyond the popup so stop the selection at the last line.
return GetLineCount() - 1;
}
int AutofillPopupControllerImpl::GetRowHeightFromId(int identifier) const {
if (identifier == POPUP_ITEM_ID_SEPARATOR)
return kSeparatorHeight;
return kRowHeight;
}
bool AutofillPopupControllerImpl::CanAccept(int id) {
return id != POPUP_ITEM_ID_SEPARATOR && id != POPUP_ITEM_ID_WARNING_MESSAGE &&
id != POPUP_ITEM_ID_TITLE;
}
bool AutofillPopupControllerImpl::HasSuggestions() {
if (suggestions_.empty())
return false;
int id = suggestions_[0].frontend_id;
return id > 0 ||
id == POPUP_ITEM_ID_AUTOCOMPLETE_ENTRY ||
id == POPUP_ITEM_ID_PASSWORD_ENTRY ||
id == POPUP_ITEM_ID_DATALIST_ENTRY ||
id == POPUP_ITEM_ID_SCAN_CREDIT_CARD;
}
void AutofillPopupControllerImpl::SetValues(
const std::vector<autofill::Suggestion>& suggestions) {
suggestions_ = suggestions;
elided_values_.resize(suggestions.size());
elided_labels_.resize(suggestions.size());
for (size_t i = 0; i < suggestions.size(); i++) {
elided_values_[i] = suggestions[i].value;
elided_labels_[i] = suggestions[i].label;
}
}
void AutofillPopupControllerImpl::ShowView() {
view_->Show();
}
void AutofillPopupControllerImpl::InvalidateRow(size_t row) {
DCHECK(0 <= row);
DCHECK(row < suggestions_.size());
view_->InvalidateRow(row);
}
#if !defined(OS_ANDROID)
int AutofillPopupControllerImpl::GetDesiredPopupWidth() const {
int popup_width = controller_common_->RoundedElementBounds().width();
for (size_t i = 0; i < GetLineCount(); ++i) {
int row_size =
gfx::GetStringWidth(GetElidedValueAt(i), value_font_list_) +
gfx::GetStringWidth(GetElidedLabelAt(i), label_font_list_) +
RowWidthWithoutText(i);
popup_width = std::max(popup_width, row_size);
}
return popup_width;
}
int AutofillPopupControllerImpl::GetDesiredPopupHeight() const {
int popup_height = 2 * kPopupBorderThickness;
for (size_t i = 0; i < suggestions_.size(); ++i) {
popup_height += GetRowHeightFromId(suggestions_[i].frontend_id);
}
return popup_height;
}
int AutofillPopupControllerImpl::RowWidthWithoutText(int row) const {
int row_size = kEndPadding;
if (!elided_labels_[row].empty())
row_size += kNamePadding;
// Add the Autofill icon size, if required.
const base::string16& icon = suggestions_[row].icon;
if (!icon.empty()) {
int icon_width = ui::ResourceBundle::GetSharedInstance().GetImageNamed(
GetIconResourceID(icon)).Width();
row_size += icon_width + kIconPadding;
}
// Add the padding at the end.
row_size += kEndPadding;
// Add room for the popup border.
row_size += 2 * kPopupBorderThickness;
return row_size;
}
void AutofillPopupControllerImpl::UpdatePopupBounds() {
int popup_width = GetDesiredPopupWidth();
int popup_height = GetDesiredPopupHeight();
popup_bounds_ = controller_common_->GetPopupBounds(popup_width, popup_height);
}
#endif // !defined(OS_ANDROID)
WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void AutofillPopupControllerImpl::ClearState() {
// Don't clear view_, because otherwise the popup will have to get regenerated
// and this will cause flickering.
popup_bounds_ = gfx::Rect();
suggestions_.clear();
elided_values_.clear();
elided_labels_.clear();
selected_line_ = kNoSelection;
}
} // namespace autofill