autofill_popup_controller_impl.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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/utf_string_conversions.h"
12#include "chrome/browser/ui/autofill/autofill_popup_view.h"
13#include "components/autofill/browser/autofill_popup_delegate.h"
14#include "content/public/browser/native_web_keyboard_event.h"
15#include "grit/webkit_resources.h"
16#include "third_party/WebKit/Source/WebKit/chromium/public/WebAutofillClient.h"
17#include "ui/base/events/event.h"
18#include "ui/base/text/text_elider.h"
19#include "ui/gfx/display.h"
20#include "ui/gfx/rect_conversions.h"
21#include "ui/gfx/screen.h"
22#include "ui/gfx/vector2d.h"
23
24using base::WeakPtr;
25using WebKit::WebAutofillClient;
26
27namespace {
28
29// Used to indicate that no line is currently selected by the user.
30const int kNoSelection = -1;
31
32// Size difference between name and subtext in pixels.
33const int kLabelFontSizeDelta = -2;
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// The amount of minimum padding between the Autofill name and subtext in
42// pixels.
43const size_t kNamePadding = 15;
44
45// The maximum amount of characters to display from either the name or subtext.
46const size_t kMaxTextLength = 15;
47
48#if !defined(OS_ANDROID)
49const size_t kIconPadding = AutofillPopupView::kIconPadding;
50const size_t kEndPadding = AutofillPopupView::kEndPadding;
51const size_t kAutofillIconWidth = AutofillPopupView::kAutofillIconWidth;
52#endif
53
54struct DataResource {
55  const char* name;
56  int id;
57};
58
59const DataResource kDataResources[] = {
60  { "americanExpressCC", IDR_AUTOFILL_CC_AMEX },
61  { "dinersCC", IDR_AUTOFILL_CC_DINERS },
62  { "discoverCC", IDR_AUTOFILL_CC_DISCOVER },
63  { "genericCC", IDR_AUTOFILL_CC_GENERIC },
64  { "jcbCC", IDR_AUTOFILL_CC_JCB },
65  { "masterCardCC", IDR_AUTOFILL_CC_MASTERCARD },
66  { "soloCC", IDR_AUTOFILL_CC_SOLO },
67  { "visaCC", IDR_AUTOFILL_CC_VISA },
68};
69
70}  // end namespace
71
72// static
73WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetOrCreate(
74    WeakPtr<AutofillPopupControllerImpl> previous,
75    AutofillPopupDelegate* delegate,
76    gfx::NativeView container_view,
77    const gfx::RectF& element_bounds) {
78  DCHECK(!previous || previous->delegate_ == delegate);
79
80  if (previous &&
81      previous->container_view() == container_view &&
82      previous->element_bounds() == element_bounds) {
83    return previous;
84  }
85
86  if (previous)
87    previous->Hide();
88
89  AutofillPopupControllerImpl* controller =
90      new AutofillPopupControllerImpl(delegate, container_view, element_bounds);
91  return controller->GetWeakPtr();
92}
93
94AutofillPopupControllerImpl::AutofillPopupControllerImpl(
95    AutofillPopupDelegate* delegate,
96    gfx::NativeView container_view,
97    const gfx::RectF& element_bounds)
98    : view_(NULL),
99      delegate_(delegate),
100      container_view_(container_view),
101      element_bounds_(element_bounds),
102      selected_line_(kNoSelection),
103      weak_ptr_factory_(this) {
104#if !defined(OS_ANDROID)
105  subtext_font_ = name_font_.DeriveFont(kLabelFontSizeDelta);
106  warning_font_ = name_font_.DeriveFont(0, gfx::Font::ITALIC);
107#endif
108}
109
110AutofillPopupControllerImpl::~AutofillPopupControllerImpl() {}
111
112void AutofillPopupControllerImpl::Show(
113    const std::vector<string16>& names,
114    const std::vector<string16>& subtexts,
115    const std::vector<string16>& icons,
116    const std::vector<int>& identifiers) {
117  names_ = names;
118  full_names_ = names;
119  subtexts_ = subtexts;
120  icons_ = icons;
121  identifiers_ = identifiers;
122
123#if !defined(OS_ANDROID)
124  // Android displays the long text with ellipsis using the view attributes.
125
126  UpdatePopupBounds();
127  int popup_width = popup_bounds().width();
128
129  // Elide the name and subtext strings so that the popup fits in the available
130  // space.
131  for (size_t i = 0; i < names_.size(); ++i) {
132    int name_width = GetNameFontForRow(i).GetStringWidth(names_[i]);
133    int subtext_width = subtext_font().GetStringWidth(subtexts_[i]);
134    int total_text_length = name_width + subtext_width;
135
136    // The line can have no strings if it represents a UI element, such as
137    // a separator line.
138    if (total_text_length == 0)
139      continue;
140
141    int available_width = popup_width - RowWidthWithoutText(i);
142
143    // Each field recieves space in proportion to its length.
144    int name_size = available_width * name_width / total_text_length;
145    names_[i] = ui::ElideText(names_[i],
146                              GetNameFontForRow(i),
147                              name_size,
148                              ui::ELIDE_AT_END);
149
150    int subtext_size = available_width * subtext_width / total_text_length;
151    subtexts_[i] = ui::ElideText(subtexts_[i],
152                                 subtext_font(),
153                                 subtext_size,
154                                 ui::ELIDE_AT_END);
155  }
156#endif
157
158  if (!view_) {
159    view_ = AutofillPopupView::Create(this);
160    ShowView();
161  } else {
162    UpdateBoundsAndRedrawPopup();
163  }
164
165  delegate_->OnPopupShown(this);
166}
167
168void AutofillPopupControllerImpl::Hide() {
169  SetSelectedLine(kNoSelection);
170
171  delegate_->OnPopupHidden(this);
172
173  if (view_)
174    view_->Hide();
175
176  delete this;
177}
178
179void AutofillPopupControllerImpl::ViewDestroyed() {
180  // The view has already been destroyed so clear the reference to it.
181  view_ = NULL;
182
183  Hide();
184}
185
186bool AutofillPopupControllerImpl::HandleKeyPressEvent(
187    const content::NativeWebKeyboardEvent& event) {
188  switch (event.windowsKeyCode) {
189    case ui::VKEY_UP:
190      SelectPreviousLine();
191      return true;
192    case ui::VKEY_DOWN:
193      SelectNextLine();
194      return true;
195    case ui::VKEY_PRIOR:  // Page up.
196      SetSelectedLine(0);
197      return true;
198    case ui::VKEY_NEXT:  // Page down.
199      SetSelectedLine(names().size() - 1);
200      return true;
201    case ui::VKEY_ESCAPE:
202      Hide();
203      return true;
204    case ui::VKEY_DELETE:
205      return (event.modifiers & content::NativeWebKeyboardEvent::ShiftKey) &&
206             RemoveSelectedLine();
207    case ui::VKEY_RETURN:
208      return AcceptSelectedLine();
209    default:
210      return false;
211  }
212}
213
214void AutofillPopupControllerImpl::UpdateBoundsAndRedrawPopup() {
215#if !defined(OS_ANDROID)
216  // TODO(csharp): Since UpdatePopupBounds can change the position of the popup,
217  // the popup could end up jumping from above the element to below it.
218  // It is unclear if it is better to keep the popup where it was, or if it
219  // should try and move to its desired position.
220  UpdatePopupBounds();
221#endif
222
223  view_->UpdateBoundsAndRedrawPopup();
224}
225
226void AutofillPopupControllerImpl::MouseHovered(int x, int y) {
227  SetSelectedLine(LineFromY(y));
228}
229
230void AutofillPopupControllerImpl::MouseClicked(int x, int y) {
231  MouseHovered(x, y);
232  AcceptSelectedLine();
233}
234
235void AutofillPopupControllerImpl::MouseExitedPopup() {
236  SetSelectedLine(kNoSelection);
237}
238
239void AutofillPopupControllerImpl::AcceptSuggestion(size_t index) {
240  delegate_->DidAcceptSuggestion(full_names_[index], identifiers_[index]);
241}
242
243int AutofillPopupControllerImpl::GetIconResourceID(
244    const string16& resource_name) {
245  for (size_t i = 0; i < arraysize(kDataResources); ++i) {
246    if (resource_name == ASCIIToUTF16(kDataResources[i].name))
247      return kDataResources[i].id;
248  }
249
250  return -1;
251}
252
253bool AutofillPopupControllerImpl::CanDelete(size_t index) const {
254  // TODO(isherman): Native AddressBook suggestions on Mac and Android should
255  // not be considered to be deleteable.
256  int id = identifiers_[index];
257  return id > 0 ||
258      id == WebAutofillClient::MenuItemIDAutocompleteEntry ||
259      id == WebAutofillClient::MenuItemIDPasswordEntry;
260}
261
262gfx::Rect AutofillPopupControllerImpl::GetRowBounds(size_t index) {
263  int top = 0;
264  for (size_t i = 0; i < index; ++i) {
265    top += GetRowHeightFromId(identifiers()[i]);
266  }
267
268  return gfx::Rect(
269      0,
270      top,
271      popup_bounds_.width(),
272      GetRowHeightFromId(identifiers()[index]));
273}
274
275void AutofillPopupControllerImpl::SetPopupBounds(const gfx::Rect& bounds) {
276  popup_bounds_ = bounds;
277  UpdateBoundsAndRedrawPopup();
278}
279
280const gfx::Rect& AutofillPopupControllerImpl::popup_bounds() const {
281  return popup_bounds_;
282}
283
284gfx::NativeView AutofillPopupControllerImpl::container_view() const {
285  return container_view_;
286}
287
288const gfx::RectF& AutofillPopupControllerImpl::element_bounds() const {
289  return element_bounds_;
290}
291
292const std::vector<string16>& AutofillPopupControllerImpl::names() const {
293  return names_;
294}
295
296const std::vector<string16>& AutofillPopupControllerImpl::subtexts() const {
297  return subtexts_;
298}
299
300const std::vector<string16>& AutofillPopupControllerImpl::icons() const {
301  return icons_;
302}
303
304const std::vector<int>& AutofillPopupControllerImpl::identifiers() const {
305  return identifiers_;
306}
307
308#if !defined(OS_ANDROID)
309const gfx::Font& AutofillPopupControllerImpl::GetNameFontForRow(size_t index)
310    const {
311  if (identifiers_[index] == WebAutofillClient::MenuItemIDWarningMessage)
312    return warning_font_;
313
314  return name_font_;
315}
316
317const gfx::Font& AutofillPopupControllerImpl::subtext_font() const {
318  return subtext_font_;
319}
320#endif
321
322int AutofillPopupControllerImpl::selected_line() const {
323  return selected_line_;
324}
325
326void AutofillPopupControllerImpl::SetSelectedLine(int selected_line) {
327  if (selected_line_ == selected_line)
328    return;
329
330  if (selected_line_ != kNoSelection)
331    InvalidateRow(selected_line_);
332
333  if (selected_line != kNoSelection)
334    InvalidateRow(selected_line);
335
336  selected_line_ = selected_line;
337
338  if (selected_line_ != kNoSelection)
339    delegate_->DidSelectSuggestion(identifiers_[selected_line_]);
340  else
341    delegate_->ClearPreviewedForm();
342}
343
344void AutofillPopupControllerImpl::SelectNextLine() {
345  int new_selected_line = selected_line_ + 1;
346
347  // Skip over any lines that can't be selected.
348  while (static_cast<size_t>(new_selected_line) < names_.size() &&
349         !CanAccept(identifiers()[new_selected_line])) {
350    ++new_selected_line;
351  }
352
353  if (new_selected_line == static_cast<int>(names_.size()))
354    new_selected_line = 0;
355
356  SetSelectedLine(new_selected_line);
357}
358
359void AutofillPopupControllerImpl::SelectPreviousLine() {
360  int new_selected_line = selected_line_ - 1;
361
362  // Skip over any lines that can't be selected.
363  while (new_selected_line > kNoSelection &&
364         !CanAccept(identifiers()[new_selected_line])) {
365    --new_selected_line;
366  }
367
368  if (new_selected_line <= kNoSelection)
369    new_selected_line = names_.size() - 1;
370
371  SetSelectedLine(new_selected_line);
372}
373
374bool AutofillPopupControllerImpl::AcceptSelectedLine() {
375  if (selected_line_ == kNoSelection)
376    return false;
377
378  DCHECK_GE(selected_line_, 0);
379  DCHECK_LT(selected_line_, static_cast<int>(names_.size()));
380
381  if (!CanAccept(identifiers_[selected_line_]))
382    return false;
383
384  AcceptSuggestion(selected_line_);
385  return true;
386}
387
388bool AutofillPopupControllerImpl::RemoveSelectedLine() {
389  if (selected_line_ == kNoSelection)
390    return false;
391
392  DCHECK_GE(selected_line_, 0);
393  DCHECK_LT(selected_line_, static_cast<int>(names_.size()));
394
395  if (!CanDelete(selected_line_))
396    return false;
397
398  delegate_->RemoveSuggestion(full_names_[selected_line_],
399                              identifiers_[selected_line_]);
400
401  // Remove the deleted element.
402  names_.erase(names_.begin() + selected_line_);
403  full_names_.erase(full_names_.begin() + selected_line_);
404  subtexts_.erase(subtexts_.begin() + selected_line_);
405  icons_.erase(icons_.begin() + selected_line_);
406  identifiers_.erase(identifiers_.begin() + selected_line_);
407
408  SetSelectedLine(kNoSelection);
409
410  if (HasSuggestions()) {
411    delegate_->ClearPreviewedForm();
412    UpdateBoundsAndRedrawPopup();
413  } else {
414    Hide();
415  }
416
417  return true;
418}
419
420int AutofillPopupControllerImpl::LineFromY(int y) {
421  int current_height = 0;
422
423  for (size_t i = 0; i < identifiers().size(); ++i) {
424    current_height += GetRowHeightFromId(identifiers()[i]);
425
426    if (y <= current_height)
427      return i;
428  }
429
430  // The y value goes beyond the popup so stop the selection at the last line.
431  return identifiers().size() - 1;
432}
433
434int AutofillPopupControllerImpl::GetRowHeightFromId(int identifier) const {
435  if (identifier == WebAutofillClient::MenuItemIDSeparator)
436    return kSeparatorHeight;
437
438  return kRowHeight;
439}
440
441bool AutofillPopupControllerImpl::CanAccept(int id) {
442  return id != WebAutofillClient::MenuItemIDSeparator &&
443      id != WebAutofillClient::MenuItemIDWarningMessage;
444}
445
446bool AutofillPopupControllerImpl::HasSuggestions() {
447  return identifiers_.size() != 0 &&
448      (identifiers_[0] > 0 ||
449       identifiers_[0] ==
450           WebAutofillClient::MenuItemIDAutocompleteEntry ||
451       identifiers_[0] == WebAutofillClient::MenuItemIDPasswordEntry ||
452       identifiers_[0] == WebAutofillClient::MenuItemIDDataListEntry);
453}
454
455void AutofillPopupControllerImpl::ShowView() {
456  view_->Show();
457}
458
459void AutofillPopupControllerImpl::InvalidateRow(size_t row) {
460  view_->InvalidateRow(row);
461}
462
463#if !defined(OS_ANDROID)
464int AutofillPopupControllerImpl::GetDesiredPopupWidth() const {
465  if (!name_font_.platform_font() || !subtext_font_.platform_font()) {
466    // We can't calculate the size of the popup if the fonts
467    // aren't present.
468    return 0;
469  }
470
471  int popup_width = RoundedElementBounds().width();
472  DCHECK_EQ(names().size(), subtexts().size());
473  for (size_t i = 0; i < names().size(); ++i) {
474    int row_size = name_font_.GetStringWidth(names()[i]) +
475        subtext_font_.GetStringWidth(subtexts()[i]) +
476        RowWidthWithoutText(i);
477
478    popup_width = std::max(popup_width, row_size);
479  }
480
481  return popup_width;
482}
483
484int AutofillPopupControllerImpl::GetDesiredPopupHeight() const {
485  int popup_height = 0;
486
487  for (size_t i = 0; i < identifiers().size(); ++i) {
488    popup_height += GetRowHeightFromId(identifiers()[i]);
489  }
490
491  return popup_height;
492}
493
494int AutofillPopupControllerImpl::RowWidthWithoutText(int row) const {
495  int row_size = kEndPadding + kNamePadding;
496
497  // Add the Autofill icon size, if required.
498  if (!icons_[row].empty())
499    row_size += kAutofillIconWidth + kIconPadding;
500
501  // Add the padding at the end
502  row_size += kEndPadding;
503
504  return row_size;
505}
506
507void AutofillPopupControllerImpl::UpdatePopupBounds() {
508  int popup_required_width = GetDesiredPopupWidth();
509  int popup_height = GetDesiredPopupHeight();
510  // This is the top left point of the popup if the popup is above the element
511  // and grows to the left (since that is the highest and furthest left the
512  // popup go could).
513  gfx::Point top_left_corner_of_popup = RoundedElementBounds().origin() +
514      gfx::Vector2d(RoundedElementBounds().width() - popup_required_width,
515                    -popup_height);
516
517  // This is the bottom right point of the popup if the popup is below the
518  // element and grows to the right (since the is the lowest and furthest right
519  // the popup could go).
520  gfx::Point bottom_right_corner_of_popup = RoundedElementBounds().origin() +
521      gfx::Vector2d(popup_required_width,
522                    RoundedElementBounds().height() + popup_height);
523
524  gfx::Display top_left_display = GetDisplayNearestPoint(
525      top_left_corner_of_popup);
526  gfx::Display bottom_right_display = GetDisplayNearestPoint(
527      bottom_right_corner_of_popup);
528
529  std::pair<int, int> popup_x_and_width = CalculatePopupXAndWidth(
530      top_left_display, bottom_right_display, popup_required_width);
531  std::pair<int, int> popup_y_and_height = CalculatePopupYAndHeight(
532      top_left_display, bottom_right_display, popup_height);
533
534  popup_bounds_ = gfx::Rect(popup_x_and_width.first,
535                            popup_y_and_height.first,
536                            popup_x_and_width.second,
537                            popup_y_and_height.second);
538}
539#endif  // !defined(OS_ANDROID)
540
541WeakPtr<AutofillPopupControllerImpl> AutofillPopupControllerImpl::GetWeakPtr() {
542  return weak_ptr_factory_.GetWeakPtr();
543}
544
545const gfx::Rect AutofillPopupControllerImpl::RoundedElementBounds() const {
546  return gfx::ToEnclosingRect(element_bounds_);
547}
548
549gfx::Display AutofillPopupControllerImpl::GetDisplayNearestPoint(
550    const gfx::Point& point) const {
551  return gfx::Screen::GetScreenFor(container_view())->GetDisplayNearestPoint(
552      point);
553}
554
555std::pair<int, int> AutofillPopupControllerImpl::CalculatePopupXAndWidth(
556    const gfx::Display& left_display,
557    const gfx::Display& right_display,
558    int popup_required_width) const {
559  int leftmost_display_x = left_display.bounds().x() *
560      left_display.device_scale_factor();
561  int rightmost_display_x = right_display.GetSizeInPixel().width() +
562      right_display.bounds().x() * right_display.device_scale_factor();
563
564  // Calculate the start coordinates for the popup if it is growing right or
565  // the end position if it is growing to the left, capped to screen space.
566  int right_growth_start = std::max(leftmost_display_x,
567                                    std::min(rightmost_display_x,
568                                             RoundedElementBounds().x()));
569  int left_growth_end = std::max(leftmost_display_x,
570                                   std::min(rightmost_display_x,
571                                            RoundedElementBounds().right()));
572
573  int right_available = rightmost_display_x - right_growth_start;
574  int left_available = left_growth_end - leftmost_display_x;
575
576  int popup_width = std::min(popup_required_width,
577                             std::max(right_available, left_available));
578
579  // If there is enough space for the popup on the right, show it there,
580  // otherwise choose the larger size.
581  if (right_available >= popup_width || right_available >= left_available)
582    return std::make_pair(right_growth_start, popup_width);
583  else
584    return std::make_pair(left_growth_end - popup_width, popup_width);
585}
586
587std::pair<int,int> AutofillPopupControllerImpl::CalculatePopupYAndHeight(
588    const gfx::Display& top_display,
589    const gfx::Display& bottom_display,
590    int popup_required_height) const {
591  int topmost_display_y = top_display.bounds().y() *
592      top_display.device_scale_factor();
593  int bottommost_display_y = bottom_display.GetSizeInPixel().height() +
594      (bottom_display.bounds().y() *
595       bottom_display.device_scale_factor());
596
597  // Calculate the start coordinates for the popup if it is growing down or
598  // the end position if it is growing up, capped to screen space.
599  int top_growth_end = std::max(topmost_display_y,
600                                std::min(bottommost_display_y,
601                                         RoundedElementBounds().y()));
602  int bottom_growth_start = std::max(topmost_display_y,
603      std::min(bottommost_display_y, RoundedElementBounds().bottom()));
604
605  int top_available = bottom_growth_start - topmost_display_y;
606  int bottom_available = bottommost_display_y - top_growth_end;
607
608  // TODO(csharp): Restrict the popup height to what is available.
609  if (bottom_available >= popup_required_height ||
610      bottom_available >= top_available) {
611    // The popup can appear below the field.
612    return std::make_pair(bottom_growth_start, popup_required_height);
613  } else {
614    // The popup must appear above the field.
615    return std::make_pair(top_growth_end - popup_required_height,
616                          popup_required_height);
617  }
618}
619