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