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