manage_passwords_bubble_view.cc revision 0529e5d033099cbfc42635f6f6183833b09dff6e
1// Copyright 2013 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/views/passwords/manage_passwords_bubble_view.h"
6
7#include "base/metrics/histogram.h"
8#include "chrome/browser/chrome_notification_types.h"
9#include "chrome/browser/ui/browser.h"
10#include "chrome/browser/ui/browser_finder.h"
11#include "chrome/browser/ui/browser_window.h"
12#include "chrome/browser/ui/passwords/manage_passwords_bubble_model.h"
13#include "chrome/browser/ui/views/frame/browser_view.h"
14#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
15#include "chrome/browser/ui/views/passwords/manage_password_item_view.h"
16#include "chrome/browser/ui/views/passwords/manage_passwords_icon_view.h"
17#include "content/public/browser/notification_source.h"
18#include "content/public/browser/web_contents_view.h"
19#include "grit/generated_resources.h"
20#include "ui/base/l10n/l10n_util.h"
21#include "ui/base/models/combobox_model.h"
22#include "ui/base/resource/resource_bundle.h"
23#include "ui/gfx/text_utils.h"
24#include "ui/views/controls/button/blue_button.h"
25#include "ui/views/controls/button/label_button.h"
26#include "ui/views/controls/combobox/combobox.h"
27#include "ui/views/layout/grid_layout.h"
28#include "ui/views/layout/layout_constants.h"
29
30
31// Helpers --------------------------------------------------------------------
32
33namespace {
34
35enum FieldType { USERNAME_FIELD, PASSWORD_FIELD };
36
37// Upper limit on the size of the username and password fields.
38const int kUsernameFieldSize = 30;
39const int kPasswordFieldSize = 22;
40
41// Returns the width of |type| field.
42int GetFieldWidth(FieldType type) {
43  return ui::ResourceBundle::GetSharedInstance()
44      .GetFontList(ui::ResourceBundle::SmallFont)
45      .GetExpectedTextWidth(type == USERNAME_FIELD ? kUsernameFieldSize
46                                                   : kPasswordFieldSize);
47}
48
49class SavePasswordRefusalComboboxModel : public ui::ComboboxModel {
50 public:
51  enum { INDEX_NOPE = 0, INDEX_NEVER_FOR_THIS_SITE = 1, };
52
53  SavePasswordRefusalComboboxModel() {
54    items_.push_back(
55        l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_CANCEL_BUTTON));
56    items_.push_back(
57        l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_BLACKLIST_BUTTON));
58  }
59  virtual ~SavePasswordRefusalComboboxModel() {}
60
61 private:
62  // Overridden from ui::ComboboxModel:
63  virtual int GetItemCount() const OVERRIDE { return items_.size(); }
64  virtual base::string16 GetItemAt(int index) OVERRIDE { return items_[index]; }
65  virtual bool IsItemSeparatorAt(int index) OVERRIDE {
66    return items_[index].empty();
67  }
68  virtual int GetDefaultIndex() const OVERRIDE { return 0; }
69
70  std::vector<base::string16> items_;
71
72  DISALLOW_COPY_AND_ASSIGN(SavePasswordRefusalComboboxModel);
73};
74
75}  // namespace
76
77
78// ManagePasswordsBubbleView --------------------------------------------------
79
80// static
81ManagePasswordsBubbleView* ManagePasswordsBubbleView::manage_passwords_bubble_ =
82    NULL;
83
84// static
85void ManagePasswordsBubbleView::ShowBubble(content::WebContents* web_contents,
86                                           DisplayReason reason) {
87  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
88  DCHECK(browser);
89  DCHECK(browser->window());
90  DCHECK(browser->fullscreen_controller());
91  DCHECK(!IsShowing());
92
93  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
94  bool is_fullscreen = browser_view->IsFullscreen();
95  views::View* anchor_view = is_fullscreen ?
96      NULL : browser_view->GetLocationBarView()->manage_passwords_icon_view();
97  manage_passwords_bubble_ = new ManagePasswordsBubbleView(
98      web_contents, anchor_view, reason);
99
100  if (is_fullscreen) {
101    manage_passwords_bubble_->set_parent_window(
102        web_contents->GetView()->GetTopLevelNativeWindow());
103  }
104
105  views::BubbleDelegateView::CreateBubble(manage_passwords_bubble_);
106
107  // Adjust for fullscreen after creation as it relies on the content size.
108  if (is_fullscreen) {
109    manage_passwords_bubble_->AdjustForFullscreen(
110        browser_view->GetBoundsInScreen());
111  }
112
113  manage_passwords_bubble_->GetWidget()->Show();
114}
115
116// static
117void ManagePasswordsBubbleView::CloseBubble() {
118  if (manage_passwords_bubble_)
119    manage_passwords_bubble_->CloseWithoutLogging();
120}
121
122// static
123bool ManagePasswordsBubbleView::IsShowing() {
124  // The bubble may be in the process of closing.
125  return (manage_passwords_bubble_ != NULL) &&
126      manage_passwords_bubble_->GetWidget()->IsVisible();
127}
128
129ManagePasswordsBubbleView::ManagePasswordsBubbleView(
130    content::WebContents* web_contents,
131    views::View* anchor_view,
132    DisplayReason reason)
133    : ManagePasswordsBubble(web_contents, reason),
134      BubbleDelegateView(anchor_view,
135                         anchor_view ? views::BubbleBorder::TOP_RIGHT
136                                     : views::BubbleBorder::NONE) {
137  // Compensate for built-in vertical padding in the anchor view's image.
138  set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
139  set_notify_enter_exit_on_child(true);
140}
141
142ManagePasswordsBubbleView::~ManagePasswordsBubbleView() {}
143
144void ManagePasswordsBubbleView::BuildColumnSet(views::GridLayout* layout,
145                                               ColumnSetType type) {
146  views::ColumnSet* column_set = layout->AddColumnSet(type);
147  column_set->AddPaddingColumn(0, views::kPanelHorizMargin);
148  switch (type) {
149    case SINGLE_VIEW_COLUMN_SET:
150      column_set->AddColumn(views::GridLayout::FILL,
151                            views::GridLayout::FILL,
152                            0,
153                            views::GridLayout::USE_PREF,
154                            0,
155                            0);
156      break;
157
158    case DOUBLE_BUTTON_COLUMN_SET:
159      column_set->AddColumn(views::GridLayout::TRAILING,
160                            views::GridLayout::CENTER,
161                            1,
162                            views::GridLayout::USE_PREF,
163                            0,
164                            0);
165      column_set->AddPaddingColumn(0, views::kRelatedButtonHSpacing);
166      column_set->AddColumn(views::GridLayout::TRAILING,
167                            views::GridLayout::CENTER,
168                            0,
169                            views::GridLayout::USE_PREF,
170                            0,
171                            0);
172      break;
173    case LINK_BUTTON_COLUMN_SET:
174      column_set->AddColumn(views::GridLayout::LEADING,
175                            views::GridLayout::CENTER,
176                            1,
177                            views::GridLayout::USE_PREF,
178                            0,
179                            0);
180      column_set->AddPaddingColumn(0, views::kRelatedButtonHSpacing);
181      column_set->AddColumn(views::GridLayout::TRAILING,
182                            views::GridLayout::CENTER,
183                            0,
184                            views::GridLayout::USE_PREF,
185                            0,
186                            0);
187      break;
188  }
189  column_set->AddPaddingColumn(0, views::kPanelHorizMargin);
190}
191
192void ManagePasswordsBubbleView::AdjustForFullscreen(
193    const gfx::Rect& screen_bounds) {
194  if (GetAnchorView())
195    return;
196
197  // The bubble's padding from the screen edge, used in fullscreen.
198  const int kFullscreenPaddingEnd = 20;
199  const size_t bubble_half_width = width() / 2;
200  const int x_pos = base::i18n::IsRTL() ?
201      screen_bounds.x() + bubble_half_width + kFullscreenPaddingEnd :
202      screen_bounds.right() - bubble_half_width - kFullscreenPaddingEnd;
203  SetAnchorRect(gfx::Rect(x_pos, screen_bounds.y(), 0, 0));
204}
205
206void ManagePasswordsBubbleView::Close() {
207  GetWidget()->Close();
208}
209
210void ManagePasswordsBubbleView::CloseWithoutLogging() {
211  model()->OnCloseWithoutLogging();
212  GetWidget()->Close();
213}
214
215void ManagePasswordsBubbleView::Init() {
216  using views::GridLayout;
217
218  GridLayout* layout = new GridLayout(this);
219  SetFocusable(true);
220  SetLayoutManager(layout);
221  BuildColumnSet(layout, SINGLE_VIEW_COLUMN_SET);
222  BuildColumnSet(layout, DOUBLE_BUTTON_COLUMN_SET);
223  BuildColumnSet(layout, LINK_BUTTON_COLUMN_SET);
224
225  // This calculates the necessary widths for credential columns in the bubble.
226  const int first_field_width = std::max(
227      GetFieldWidth(USERNAME_FIELD),
228      views::Label(l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_DELETED))
229          .GetPreferredSize()
230          .width());
231
232  const int second_field_width = std::max(
233      GetFieldWidth(PASSWORD_FIELD),
234      views::Label(l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_UNDO))
235          .GetPreferredSize()
236          .width());
237
238  // Build and populate the header.
239  views::Label* title_label = new views::Label(model()->title());
240  title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
241  title_label->SetMultiLine(true);
242  title_label->SetFontList(ui::ResourceBundle::GetSharedInstance().GetFontList(
243      ui::ResourceBundle::MediumFont));
244
245  layout->StartRowWithPadding(
246      0, SINGLE_VIEW_COLUMN_SET, 0, views::kRelatedControlSmallVerticalSpacing);
247  layout->AddView(title_label);
248  layout->AddPaddingRow(0, views::kUnrelatedControlVerticalSpacing);
249
250  if (model()->WaitingToSavePassword()) {
251    // If we've got a password that we're deciding whether or not to save,
252    // then we need to display a single-view columnset containing the
253    // ManagePasswordItemView, followed by double-view columnset containing
254    // a "Save" and "Reject" button.
255    ManagePasswordItemView* item =
256        new ManagePasswordItemView(model(),
257                                   model()->pending_credentials(),
258                                   first_field_width,
259                                   second_field_width,
260                                   ManagePasswordItemView::FIRST_ITEM);
261    layout->StartRow(0, SINGLE_VIEW_COLUMN_SET);
262    layout->AddView(item);
263
264    refuse_combobox_ =
265        new views::Combobox(new SavePasswordRefusalComboboxModel());
266    refuse_combobox_->set_listener(this);
267    refuse_combobox_->SetStyle(views::Combobox::STYLE_ACTION);
268
269    save_button_ = new views::BlueButton(
270        this, l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_SAVE_BUTTON));
271
272    layout->StartRowWithPadding(
273        0, DOUBLE_BUTTON_COLUMN_SET, 0, views::kRelatedControlVerticalSpacing);
274    layout->AddView(save_button_);
275    layout->AddView(refuse_combobox_);
276    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
277  } else {
278    // If we have a list of passwords to store for the current site, display
279    // them to the user for management. Otherwise, render a "No passwords for
280    // this site" message.
281    //
282    // TODO(mkwst): Do we really want the "No passwords" case? It would probably
283    // be better to only clear the pending password upon navigation, rather than
284    // as soon as the bubble closes.
285    if (!model()->best_matches().empty()) {
286      for (autofill::PasswordFormMap::const_iterator i(
287               model()->best_matches().begin());
288           i != model()->best_matches().end();
289           ++i) {
290        ManagePasswordItemView* item = new ManagePasswordItemView(
291            model(),
292            *i->second,
293            first_field_width,
294            second_field_width,
295            i == model()->best_matches().begin()
296                ? ManagePasswordItemView::FIRST_ITEM
297                : ManagePasswordItemView::SUBSEQUENT_ITEM);
298
299        layout->StartRow(0, SINGLE_VIEW_COLUMN_SET);
300        layout->AddView(item);
301      }
302    } else {
303        views::Label* empty_label = new views::Label(
304            l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_NO_PASSWORDS));
305        empty_label->SetMultiLine(true);
306
307        layout->StartRow(0, SINGLE_VIEW_COLUMN_SET);
308        layout->AddView(empty_label);
309    }
310
311    // Build a "manage" link and "done" button, and throw them both into a new
312    // row
313    // containing a double-view columnset.
314    manage_link_ = new views::Link(model()->manage_link());
315    manage_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
316    manage_link_->SetUnderline(false);
317    manage_link_->set_listener(this);
318
319    done_button_ =
320        new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_DONE));
321    done_button_->SetStyle(views::Button::STYLE_BUTTON);
322
323    layout->StartRowWithPadding(
324        0, LINK_BUTTON_COLUMN_SET, 0, views::kRelatedControlVerticalSpacing);
325    layout->AddView(manage_link_);
326    layout->AddView(done_button_);
327  }
328}
329
330void ManagePasswordsBubbleView::WindowClosing() {
331  // Close() closes the window asynchronously, so by the time we reach here,
332  // |manage_passwords_bubble_| may have already been reset.
333  if (manage_passwords_bubble_ == this)
334    manage_passwords_bubble_ = NULL;
335}
336
337void ManagePasswordsBubbleView::ButtonPressed(views::Button* sender,
338                                              const ui::Event& event) {
339  DCHECK(sender == save_button_ || sender == done_button_);
340
341  if (sender == save_button_)
342    model()->OnSaveClicked();
343  else
344    model()->OnDoneClicked();
345  Close();
346}
347
348void ManagePasswordsBubbleView::LinkClicked(views::Link* source,
349                                            int event_flags) {
350  DCHECK_EQ(source, manage_link_);
351  model()->OnManageLinkClicked();
352  Close();
353}
354
355void ManagePasswordsBubbleView::OnPerformAction(views::Combobox* source) {
356  DCHECK_EQ(source, refuse_combobox_);
357  switch (refuse_combobox_->selected_index()) {
358    case SavePasswordRefusalComboboxModel::INDEX_NOPE:
359      model()->OnNopeClicked();
360      break;
361    case SavePasswordRefusalComboboxModel::INDEX_NEVER_FOR_THIS_SITE:
362      model()->OnNeverForThisSiteClicked();
363      break;
364    default:
365      NOTREACHED();
366      break;
367  }
368  Close();
369}
370