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