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