permissions_bubble_view.cc revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
1// Copyright 2014 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/website_settings/permissions_bubble_view.h"
6
7#include "base/strings/string16.h"
8#include "chrome/browser/ui/views/website_settings/permission_selector_view.h"
9#include "chrome/browser/ui/views/website_settings/permission_selector_view_observer.h"
10#include "chrome/browser/ui/website_settings/permission_bubble_request.h"
11#include "chrome/grit/generated_resources.h"
12#include "net/base/net_util.h"
13#include "ui/accessibility/ax_view_state.h"
14#include "ui/base/l10n/l10n_util.h"
15#include "ui/base/models/combobox_model.h"
16#include "ui/base/resource/resource_bundle.h"
17#include "ui/gfx/text_constants.h"
18#include "ui/views/bubble/bubble_delegate.h"
19#include "ui/views/controls/button/checkbox.h"
20#include "ui/views/controls/button/label_button.h"
21#include "ui/views/controls/button/label_button_border.h"
22#include "ui/views/controls/button/menu_button.h"
23#include "ui/views/controls/button/menu_button_listener.h"
24#include "ui/views/controls/combobox/combobox.h"
25#include "ui/views/controls/combobox/combobox_listener.h"
26#include "ui/views/controls/label.h"
27#include "ui/views/controls/menu/menu_runner.h"
28#include "ui/views/layout/box_layout.h"
29#include "ui/views/layout/grid_layout.h"
30
31namespace {
32
33// Spacing constant for outer margin. This is added to the
34// bubble margin itself to equalize the margins at 13px.
35const int kBubbleOuterMargin = 5;
36
37// Spacing between major items should be 9px.
38const int kItemMajorSpacing = 9;
39
40// Button border size, draws inside the spacing distance.
41const int kButtonBorderSize = 2;
42
43// (Square) pixel size of icon.
44const int kIconSize = 18;
45
46// Number of pixels to indent the permission request labels.
47const int kPermissionIndentSpacing = 12;
48
49}  // namespace
50
51// This class is a MenuButton which is given a PermissionMenuModel. It
52// shows the current checked item in the menu model, and notifies its listener
53// about any updates to the state of the selection.
54// TODO: refactor PermissionMenuButton to work like this and re-use?
55class PermissionCombobox : public views::MenuButton,
56                           public views::MenuButtonListener {
57 public:
58  // Get notifications when the selection changes.
59  class Listener {
60   public:
61    virtual void PermissionSelectionChanged(int index, bool allowed) = 0;
62  };
63
64  PermissionCombobox(Listener* listener,
65                     int index,
66                     const GURL& url,
67                     ContentSetting setting);
68  virtual ~PermissionCombobox();
69
70  int index() const { return index_; }
71
72  virtual void GetAccessibleState(ui::AXViewState* state) OVERRIDE;
73
74  // MenuButtonListener:
75  virtual void OnMenuButtonClicked(View* source,
76                                   const gfx::Point& point) OVERRIDE;
77
78  // Callback when a permission's setting is changed.
79  void PermissionChanged(const WebsiteSettingsUI::PermissionInfo& permission);
80
81 private:
82  int index_;
83  Listener* listener_;
84  scoped_ptr<PermissionMenuModel> model_;
85  scoped_ptr<views::MenuRunner> menu_runner_;
86};
87
88PermissionCombobox::PermissionCombobox(Listener* listener,
89                                       int index,
90                                       const GURL& url,
91                                       ContentSetting setting)
92    : MenuButton(NULL, base::string16(), this, true),
93      index_(index),
94      listener_(listener),
95      model_(new PermissionMenuModel(
96          url,
97          setting,
98          base::Bind(&PermissionCombobox::PermissionChanged,
99                     base::Unretained(this)))) {
100  SetText(model_->GetLabelAt(model_->GetIndexOfCommandId(setting)));
101  SizeToPreferredSize();
102}
103
104PermissionCombobox::~PermissionCombobox() {}
105
106void PermissionCombobox::GetAccessibleState(ui::AXViewState* state) {
107  MenuButton::GetAccessibleState(state);
108  state->value = GetText();
109}
110
111void PermissionCombobox::OnMenuButtonClicked(View* source,
112                                             const gfx::Point& point) {
113  menu_runner_.reset(
114      new views::MenuRunner(model_.get(), views::MenuRunner::HAS_MNEMONICS));
115
116  gfx::Point p(point);
117  p.Offset(-source->width(), 0);
118  if (menu_runner_->RunMenuAt(source->GetWidget()->GetTopLevelWidget(),
119                              this,
120                              gfx::Rect(p, gfx::Size()),
121                              views::MENU_ANCHOR_TOPLEFT,
122                              ui::MENU_SOURCE_NONE) ==
123      views::MenuRunner::MENU_DELETED) {
124    return;
125  }
126}
127
128void PermissionCombobox::PermissionChanged(
129    const WebsiteSettingsUI::PermissionInfo& permission) {
130  SetText(model_->GetLabelAt(model_->GetIndexOfCommandId(permission.setting)));
131  SizeToPreferredSize();
132
133  listener_->PermissionSelectionChanged(
134      index_, permission.setting == CONTENT_SETTING_ALLOW);
135}
136
137// A combobox originating on the Allow button allowing for customization
138// of permissions.
139class CustomizeAllowComboboxModel : public ui::ComboboxModel {
140 public:
141  enum Item {
142    INDEX_ALLOW = 0,
143    INDEX_CUSTOMIZE = 1
144  };
145
146  CustomizeAllowComboboxModel() {}
147  virtual ~CustomizeAllowComboboxModel() {}
148
149  virtual int GetItemCount() const OVERRIDE;
150  virtual base::string16 GetItemAt(int index) OVERRIDE;
151  virtual int GetDefaultIndex() const OVERRIDE;
152};
153
154int CustomizeAllowComboboxModel::GetItemCount() const {
155  return 2;
156}
157
158base::string16 CustomizeAllowComboboxModel::GetItemAt(int index) {
159  if (index == INDEX_ALLOW)
160    return l10n_util::GetStringUTF16(IDS_PERMISSION_ALLOW);
161  else
162    return l10n_util::GetStringUTF16(IDS_PERMISSION_CUSTOMIZE);
163}
164
165int CustomizeAllowComboboxModel::GetDefaultIndex() const {
166  return INDEX_ALLOW;
167}
168
169///////////////////////////////////////////////////////////////////////////////
170// View implementation for the permissions bubble.
171class PermissionsBubbleDelegateView : public views::BubbleDelegateView,
172                                      public views::ButtonListener,
173                                      public views::ComboboxListener,
174                                      public PermissionCombobox::Listener {
175 public:
176  PermissionsBubbleDelegateView(
177      views::View* anchor,
178      PermissionBubbleViewViews* owner,
179      const std::vector<PermissionBubbleRequest*>& requests,
180      const std::vector<bool>& accept_state,
181      bool customization_mode);
182  virtual ~PermissionsBubbleDelegateView();
183
184  void Close();
185  void SizeToContents();
186
187  // BubbleDelegateView:
188  virtual bool ShouldShowCloseButton() const OVERRIDE;
189  virtual bool ShouldShowWindowTitle() const OVERRIDE;
190  virtual const gfx::FontList& GetTitleFontList() const OVERRIDE;
191  virtual base::string16 GetWindowTitle() const OVERRIDE;
192  virtual void OnWidgetDestroying(views::Widget* widget) OVERRIDE;
193
194  // ButtonListener:
195  virtual void ButtonPressed(views::Button* button,
196                             const ui::Event& event) OVERRIDE;
197
198  // ComboboxListener:
199  virtual void OnPerformAction(views::Combobox* combobox) OVERRIDE;
200
201  // PermissionCombobox::Listener:
202  virtual void PermissionSelectionChanged(int index, bool allowed) OVERRIDE;
203
204 private:
205  PermissionBubbleViewViews* owner_;
206  views::Button* allow_;
207  views::Button* deny_;
208  views::Combobox* allow_combobox_;
209  base::string16 hostname_;
210  scoped_ptr<PermissionMenuModel> menu_button_model_;
211  std::vector<PermissionCombobox*> customize_comboboxes_;
212
213  DISALLOW_COPY_AND_ASSIGN(PermissionsBubbleDelegateView);
214};
215
216PermissionsBubbleDelegateView::PermissionsBubbleDelegateView(
217    views::View* anchor,
218    PermissionBubbleViewViews* owner,
219    const std::vector<PermissionBubbleRequest*>& requests,
220    const std::vector<bool>& accept_state,
221    bool customization_mode)
222    : views::BubbleDelegateView(anchor, views::BubbleBorder::TOP_LEFT),
223      owner_(owner),
224      allow_(NULL),
225      deny_(NULL),
226      allow_combobox_(NULL) {
227  DCHECK(!requests.empty());
228
229  RemoveAllChildViews(true);
230  customize_comboboxes_.clear();
231  set_close_on_esc(false);
232  set_close_on_deactivate(false);
233
234  SetLayoutManager(new views::BoxLayout(
235      views::BoxLayout::kVertical, kBubbleOuterMargin, 0, kItemMajorSpacing));
236
237  // TODO(gbillock): support other languages than English.
238  hostname_ = net::FormatUrl(requests[0]->GetRequestingHostname(),
239                             "en",
240                             net::kFormatUrlOmitUsernamePassword |
241                             net::kFormatUrlOmitTrailingSlashOnBareHostname,
242                             net::UnescapeRule::SPACES, NULL, NULL, NULL);
243
244  ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
245  for (size_t index = 0; index < requests.size(); index++) {
246    DCHECK(index < accept_state.size());
247    // The row is laid out containing a leading-aligned label area and a
248    // trailing column which will be filled during customization with a
249    // combobox.
250    views::View* row = new views::View();
251    views::GridLayout* row_layout = new views::GridLayout(row);
252    row->SetLayoutManager(row_layout);
253    views::ColumnSet* columns = row_layout->AddColumnSet(0);
254    columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::FILL,
255                       0, views::GridLayout::USE_PREF, 0, 0);
256    columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::FILL,
257                       100, views::GridLayout::USE_PREF, 0, 0);
258    row_layout->StartRow(0, 0);
259
260    views::View* label_container = new views::View();
261    label_container->SetLayoutManager(
262        new views::BoxLayout(views::BoxLayout::kHorizontal,
263                             kPermissionIndentSpacing,
264                             0, kBubbleOuterMargin));
265    views::ImageView* icon = new views::ImageView();
266    icon->SetImage(bundle.GetImageSkiaNamed(requests.at(index)->GetIconID()));
267    icon->SetImageSize(gfx::Size(kIconSize, kIconSize));
268    label_container->AddChildView(icon);
269    views::Label* label =
270        new views::Label(requests.at(index)->GetMessageTextFragment());
271    label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
272    label_container->AddChildView(label);
273    row_layout->AddView(label_container);
274
275    if (customization_mode) {
276      PermissionCombobox* combobox = new PermissionCombobox(
277          this,
278          index,
279          requests[index]->GetRequestingHostname(),
280          accept_state[index] ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK);
281      row_layout->AddView(combobox);
282      customize_comboboxes_.push_back(combobox);
283    } else {
284      row_layout->AddView(new views::View());
285    }
286
287    AddChildView(row);
288  }
289
290  views::View* button_row = new views::View();
291  views::GridLayout* button_layout = new views::GridLayout(button_row);
292  views::ColumnSet* columns = button_layout->AddColumnSet(0);
293  button_row->SetLayoutManager(button_layout);
294  AddChildView(button_row);
295
296  // Customization case: just an "OK" button
297  if (customization_mode) {
298    columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::FILL,
299                       100, views::GridLayout::USE_PREF, 0, 0);
300    button_layout->StartRow(0, 0);
301    views::LabelButton* ok_button =
302        new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_OK));
303    ok_button->SetStyle(views::Button::STYLE_BUTTON);
304    button_layout->AddView(ok_button);
305    allow_ = ok_button;
306
307    button_layout->AddPaddingRow(0, kBubbleOuterMargin);
308    return;
309  }
310
311  // No customization: lay out the Deny/Allow buttons.
312
313  columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::FILL,
314                     100, views::GridLayout::USE_PREF, 0, 0);
315  columns->AddPaddingColumn(0, kItemMajorSpacing - (2*kButtonBorderSize));
316  columns->AddColumn(views::GridLayout::TRAILING, views::GridLayout::FILL,
317                     0, views::GridLayout::USE_PREF, 0, 0);
318  button_layout->StartRow(0, 0);
319
320  // Allow button is a regular button when there's only one option, and a
321  // STYLE_ACTION Combobox when there are more than one option and
322  // customization is an option.
323
324  base::string16 allow_text = l10n_util::GetStringUTF16(IDS_PERMISSION_ALLOW);
325  if (requests.size() == 1) {
326    views::LabelButton* allow_button = new views::LabelButton(this, allow_text);
327    allow_button->SetStyle(views::Button::STYLE_BUTTON);
328    button_layout->AddView(allow_button);
329    allow_ = allow_button;
330  } else {
331    views::Combobox* allow_combobox = new views::Combobox(
332        new CustomizeAllowComboboxModel());
333    allow_combobox->set_listener(this);
334    allow_combobox->SetStyle(views::Combobox::STYLE_ACTION);
335    button_layout->AddView(allow_combobox);
336    allow_combobox_ = allow_combobox;
337  }
338
339  base::string16 deny_text = l10n_util::GetStringUTF16(IDS_PERMISSION_DENY);
340  views::LabelButton* deny_button = new views::LabelButton(this, deny_text);
341  deny_button->SetStyle(views::Button::STYLE_BUTTON);
342  button_layout->AddView(deny_button);
343  deny_ = deny_button;
344
345  button_layout->AddPaddingRow(0, kBubbleOuterMargin);
346}
347
348PermissionsBubbleDelegateView::~PermissionsBubbleDelegateView() {
349  if (owner_)
350    owner_->Closing();
351}
352
353void PermissionsBubbleDelegateView::Close() {
354  owner_ = NULL;
355  GetWidget()->Close();
356}
357
358bool PermissionsBubbleDelegateView::ShouldShowCloseButton() const {
359  return true;
360}
361
362bool PermissionsBubbleDelegateView::ShouldShowWindowTitle() const {
363  return true;
364}
365
366const gfx::FontList& PermissionsBubbleDelegateView::GetTitleFontList() const {
367  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
368  return rb.GetFontList(ui::ResourceBundle::BaseFont);
369}
370
371base::string16 PermissionsBubbleDelegateView::GetWindowTitle() const {
372  return l10n_util::GetStringFUTF16(IDS_PERMISSIONS_BUBBLE_PROMPT,
373                                    hostname_);
374}
375
376void PermissionsBubbleDelegateView::SizeToContents() {
377  BubbleDelegateView::SizeToContents();
378}
379
380void PermissionsBubbleDelegateView::OnWidgetDestroying(views::Widget* widget) {
381  views::BubbleDelegateView::OnWidgetDestroying(widget);
382  if (owner_) {
383    owner_->Closing();
384    owner_ = NULL;
385  }
386}
387
388void PermissionsBubbleDelegateView::ButtonPressed(views::Button* button,
389                                                  const ui::Event& event) {
390  if (!owner_)
391    return;
392
393  if (button == allow_)
394    owner_->Accept();
395  else if (button == deny_)
396    owner_->Deny();
397}
398
399void PermissionsBubbleDelegateView::PermissionSelectionChanged(
400    int index, bool allowed) {
401  owner_->Toggle(index, allowed);
402}
403
404void PermissionsBubbleDelegateView::OnPerformAction(
405    views::Combobox* combobox) {
406  if (combobox == allow_combobox_) {
407    if (combobox->selected_index() ==
408        CustomizeAllowComboboxModel::INDEX_CUSTOMIZE)
409      owner_->SetCustomizationMode();
410    else if (combobox->selected_index() ==
411             CustomizeAllowComboboxModel::INDEX_ALLOW)
412      owner_->Accept();
413  }
414}
415
416//////////////////////////////////////////////////////////////////////////////
417// PermissionBubbleViewViews
418
419PermissionBubbleViewViews::PermissionBubbleViewViews(views::View* anchor_view)
420    : anchor_view_(anchor_view),
421      delegate_(NULL),
422      bubble_delegate_(NULL) {}
423
424PermissionBubbleViewViews::~PermissionBubbleViewViews() {
425  if (delegate_)
426    delegate_->SetView(NULL);
427}
428
429void PermissionBubbleViewViews::SetDelegate(Delegate* delegate) {
430  delegate_ = delegate;
431}
432
433void PermissionBubbleViewViews::Show(
434    const std::vector<PermissionBubbleRequest*>& requests,
435    const std::vector<bool>& values,
436    bool customization_mode) {
437  if (bubble_delegate_ != NULL)
438    bubble_delegate_->Close();
439
440  bubble_delegate_ =
441      new PermissionsBubbleDelegateView(anchor_view_, this,
442                                        requests, values, customization_mode);
443  views::BubbleDelegateView::CreateBubble(bubble_delegate_)->Show();
444  bubble_delegate_->SizeToContents();
445}
446
447bool PermissionBubbleViewViews::CanAcceptRequestUpdate() {
448  return !(bubble_delegate_ && bubble_delegate_->IsMouseHovered());
449}
450
451void PermissionBubbleViewViews::Hide() {
452  if (bubble_delegate_) {
453    bubble_delegate_->Close();
454    bubble_delegate_ = NULL;
455  }
456}
457
458bool PermissionBubbleViewViews::IsVisible() {
459  return bubble_delegate_ != NULL;
460}
461
462void PermissionBubbleViewViews::Closing() {
463  if (bubble_delegate_)
464    bubble_delegate_ = NULL;
465  if (delegate_)
466    delegate_->Closing();
467}
468
469void PermissionBubbleViewViews::Toggle(int index, bool value) {
470  if (delegate_)
471    delegate_->ToggleAccept(index, value);
472}
473
474void PermissionBubbleViewViews::Accept() {
475  if (delegate_)
476    delegate_->Accept();
477}
478
479void PermissionBubbleViewViews::Deny() {
480  if (delegate_)
481    delegate_->Deny();
482}
483
484void PermissionBubbleViewViews::SetCustomizationMode() {
485  if (delegate_)
486    delegate_->SetCustomizationMode();
487}
488