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