content_setting_bubble_contents.cc revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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/views/content_setting_bubble_contents.h"
6
7#include <algorithm>
8#include <set>
9#include <string>
10#include <vector>
11
12#include "base/bind.h"
13#include "base/stl_util.h"
14#include "base/strings/utf_string_conversions.h"
15#include "chrome/browser/content_settings/host_content_settings_map.h"
16#include "chrome/browser/plugins/plugin_finder.h"
17#include "chrome/browser/plugins/plugin_metadata.h"
18#include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
19#include "chrome/browser/ui/content_settings/content_setting_media_menu_model.h"
20#include "chrome/browser/ui/views/browser_dialogs.h"
21#include "content/public/browser/plugin_service.h"
22#include "content/public/browser/web_contents.h"
23#include "grit/generated_resources.h"
24#include "grit/theme_resources.h"
25#include "ui/base/l10n/l10n_util.h"
26#include "ui/base/models/simple_menu_model.h"
27#include "ui/base/resource/resource_bundle.h"
28#include "ui/gfx/font_list.h"
29#include "ui/views/controls/button/label_button.h"
30#include "ui/views/controls/button/menu_button.h"
31#include "ui/views/controls/button/radio_button.h"
32#include "ui/views/controls/image_view.h"
33#include "ui/views/controls/label.h"
34#include "ui/views/controls/link.h"
35#include "ui/views/controls/menu/menu.h"
36#include "ui/views/controls/menu/menu_runner.h"
37#include "ui/views/controls/separator.h"
38#include "ui/views/layout/grid_layout.h"
39#include "ui/views/layout/layout_constants.h"
40
41#if defined(USE_AURA)
42#include "ui/base/cursor/cursor.h"
43#endif
44
45namespace {
46
47// If we don't clamp the maximum width, then very long URLs and titles can make
48// the bubble arbitrarily wide.
49const int kMaxContentsWidth = 500;
50
51// When we have multiline labels, we should set a minimum width lest we get very
52// narrow bubbles with lots of line-wrapping.
53const int kMinMultiLineContentsWidth = 250;
54
55// The minimum width of the media menu buttons.
56const int kMinMediaMenuButtonWidth = 100;
57
58}  // namespace
59
60using content::PluginService;
61using content::WebContents;
62
63
64// ContentSettingBubbleContents::Favicon --------------------------------------
65
66class ContentSettingBubbleContents::Favicon : public views::ImageView {
67 public:
68  Favicon(const gfx::Image& image,
69          ContentSettingBubbleContents* parent,
70          views::Link* link);
71  virtual ~Favicon();
72
73 private:
74  // views::View overrides:
75  virtual bool OnMousePressed(const ui::MouseEvent& event) OVERRIDE;
76  virtual void OnMouseReleased(const ui::MouseEvent& event) OVERRIDE;
77  virtual gfx::NativeCursor GetCursor(const ui::MouseEvent& event) OVERRIDE;
78
79  ContentSettingBubbleContents* parent_;
80  views::Link* link_;
81};
82
83ContentSettingBubbleContents::Favicon::Favicon(
84    const gfx::Image& image,
85    ContentSettingBubbleContents* parent,
86    views::Link* link)
87    : parent_(parent),
88      link_(link) {
89  SetImage(image.AsImageSkia());
90}
91
92ContentSettingBubbleContents::Favicon::~Favicon() {
93}
94
95bool ContentSettingBubbleContents::Favicon::OnMousePressed(
96    const ui::MouseEvent& event) {
97  return event.IsLeftMouseButton() || event.IsMiddleMouseButton();
98}
99
100void ContentSettingBubbleContents::Favicon::OnMouseReleased(
101    const ui::MouseEvent& event) {
102  if ((event.IsLeftMouseButton() || event.IsMiddleMouseButton()) &&
103     HitTestPoint(event.location())) {
104    parent_->LinkClicked(link_, event.flags());
105  }
106}
107
108gfx::NativeCursor ContentSettingBubbleContents::Favicon::GetCursor(
109    const ui::MouseEvent& event) {
110#if defined(USE_AURA)
111  return ui::kCursorHand;
112#elif defined(OS_WIN)
113  static HCURSOR g_hand_cursor = LoadCursor(NULL, IDC_HAND);
114  return g_hand_cursor;
115#endif
116}
117
118
119// ContentSettingBubbleContents::MediaMenuParts -------------------------------
120
121struct ContentSettingBubbleContents::MediaMenuParts {
122  explicit MediaMenuParts(content::MediaStreamType type);
123  ~MediaMenuParts();
124
125  content::MediaStreamType type;
126  scoped_ptr<ui::SimpleMenuModel> menu_model;
127
128 private:
129  DISALLOW_COPY_AND_ASSIGN(MediaMenuParts);
130};
131
132ContentSettingBubbleContents::MediaMenuParts::MediaMenuParts(
133    content::MediaStreamType type)
134    : type(type) {}
135
136ContentSettingBubbleContents::MediaMenuParts::~MediaMenuParts() {}
137
138// ContentSettingBubbleContents -----------------------------------------------
139
140ContentSettingBubbleContents::ContentSettingBubbleContents(
141    ContentSettingBubbleModel* content_setting_bubble_model,
142    content::WebContents* web_contents,
143    views::View* anchor_view,
144    views::BubbleBorder::Arrow arrow)
145    : content::WebContentsObserver(web_contents),
146      BubbleDelegateView(anchor_view, arrow),
147      content_setting_bubble_model_(content_setting_bubble_model),
148      custom_link_(NULL),
149      manage_link_(NULL),
150      close_button_(NULL) {
151  // Compensate for built-in vertical padding in the anchor view's image.
152  set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
153}
154
155ContentSettingBubbleContents::~ContentSettingBubbleContents() {
156  STLDeleteValues(&media_menus_);
157}
158
159gfx::Size ContentSettingBubbleContents::GetPreferredSize() const {
160  gfx::Size preferred_size(views::View::GetPreferredSize());
161  int preferred_width =
162      (!content_setting_bubble_model_->bubble_content().domain_lists.empty() &&
163       (kMinMultiLineContentsWidth > preferred_size.width())) ?
164      kMinMultiLineContentsWidth : preferred_size.width();
165  preferred_size.set_width(std::min(preferred_width, kMaxContentsWidth));
166  return preferred_size;
167}
168
169void ContentSettingBubbleContents::UpdateMenuLabel(
170    content::MediaStreamType type,
171    const std::string& label) {
172  for (MediaMenuPartsMap::const_iterator it = media_menus_.begin();
173       it != media_menus_.end(); ++it) {
174    if (it->second->type == type) {
175      it->first->SetText(base::UTF8ToUTF16(label));
176      return;
177    }
178  }
179  NOTREACHED();
180}
181
182void ContentSettingBubbleContents::Init() {
183  using views::GridLayout;
184
185  GridLayout* layout = new views::GridLayout(this);
186  SetLayoutManager(layout);
187
188  const int kSingleColumnSetId = 0;
189  views::ColumnSet* column_set = layout->AddColumnSet(kSingleColumnSetId);
190  column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
191                        GridLayout::USE_PREF, 0, 0);
192
193  const ContentSettingBubbleModel::BubbleContent& bubble_content =
194      content_setting_bubble_model_->bubble_content();
195  bool bubble_content_empty = true;
196
197  if (!bubble_content.title.empty()) {
198    views::Label* title_label = new views::Label(base::UTF8ToUTF16(
199        bubble_content.title));
200    title_label->SetMultiLine(true);
201    title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
202    layout->StartRow(0, kSingleColumnSetId);
203    layout->AddView(title_label);
204    bubble_content_empty = false;
205  }
206
207  if (content_setting_bubble_model_->content_type() ==
208      CONTENT_SETTINGS_TYPE_POPUPS) {
209    const int kPopupColumnSetId = 2;
210    views::ColumnSet* popup_column_set =
211        layout->AddColumnSet(kPopupColumnSetId);
212    popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
213                                GridLayout::USE_PREF, 0, 0);
214    popup_column_set->AddPaddingColumn(
215        0, views::kRelatedControlHorizontalSpacing);
216    popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
217                                GridLayout::USE_PREF, 0, 0);
218
219    for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator
220         i(bubble_content.popup_items.begin());
221         i != bubble_content.popup_items.end(); ++i) {
222      if (!bubble_content_empty)
223        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
224      layout->StartRow(0, kPopupColumnSetId);
225
226      views::Link* link = new views::Link(base::UTF8ToUTF16(i->title));
227      link->set_listener(this);
228      link->SetElideBehavior(views::Label::ELIDE_IN_MIDDLE);
229      popup_links_[link] = i - bubble_content.popup_items.begin();
230      layout->AddView(new Favicon(i->image, this, link));
231      layout->AddView(link);
232      bubble_content_empty = false;
233    }
234  }
235
236  const int indented_kSingleColumnSetId = 3;
237  // Insert a column set with greater indent.
238  views::ColumnSet* indented_single_column_set =
239      layout->AddColumnSet(indented_kSingleColumnSetId);
240  indented_single_column_set->AddPaddingColumn(0, views::kCheckboxIndent);
241  indented_single_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL,
242                                        1, GridLayout::USE_PREF, 0, 0);
243
244  const ContentSettingBubbleModel::RadioGroup& radio_group =
245      bubble_content.radio_group;
246  if (!radio_group.radio_items.empty()) {
247    if (!bubble_content_empty)
248      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
249    for (ContentSettingBubbleModel::RadioItems::const_iterator i(
250         radio_group.radio_items.begin());
251         i != radio_group.radio_items.end(); ++i) {
252      views::RadioButton* radio =
253          new views::RadioButton(base::UTF8ToUTF16(*i), 0);
254      radio->SetEnabled(bubble_content.radio_group_enabled);
255      radio->set_listener(this);
256      radio_group_.push_back(radio);
257      layout->StartRow(0, indented_kSingleColumnSetId);
258      layout->AddView(radio);
259      bubble_content_empty = false;
260    }
261    DCHECK(!radio_group_.empty());
262    // Now that the buttons have been added to the view hierarchy, it's safe
263    // to call SetChecked() on them.
264    radio_group_[radio_group.default_item]->SetChecked(true);
265  }
266
267  // Layout code for the media device menus.
268  if (content_setting_bubble_model_->content_type() ==
269      CONTENT_SETTINGS_TYPE_MEDIASTREAM) {
270    const int kMediaMenuColumnSetId = 2;
271    views::ColumnSet* menu_column_set =
272        layout->AddColumnSet(kMediaMenuColumnSetId);
273    menu_column_set->AddPaddingColumn(0, views::kCheckboxIndent);
274    menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
275                               GridLayout::USE_PREF, 0, 0);
276    menu_column_set->AddPaddingColumn(
277        0, views::kRelatedControlHorizontalSpacing);
278    menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
279                               GridLayout::USE_PREF, 0, 0);
280
281    int menu_width = 0;
282    for (ContentSettingBubbleModel::MediaMenuMap::const_iterator i(
283         bubble_content.media_menus.begin());
284         i != bubble_content.media_menus.end(); ++i) {
285      if (!bubble_content_empty)
286        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
287      layout->StartRow(0, kMediaMenuColumnSetId);
288
289      views::Label* label =
290          new views::Label(base::UTF8ToUTF16(i->second.label));
291      label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
292
293      views::MenuButton* menu_button = new views::MenuButton(
294          NULL, base::UTF8ToUTF16((i->second.selected_device.name)),
295          this, true);
296      menu_button->set_alignment(views::TextButton::ALIGN_LEFT);
297      menu_button->SetBorder(scoped_ptr<views::Border>(
298          new views::TextButtonNativeThemeBorder(menu_button)));
299      menu_button->set_animate_on_state_change(false);
300
301      MediaMenuParts* menu_view = new MediaMenuParts(i->first);
302      menu_view->menu_model.reset(new ContentSettingMediaMenuModel(
303          i->first,
304          content_setting_bubble_model_.get(),
305          base::Bind(&ContentSettingBubbleContents::UpdateMenuLabel,
306                     base::Unretained(this))));
307      media_menus_[menu_button] = menu_view;
308
309      if (!menu_view->menu_model->GetItemCount()) {
310        // Show a "None available" title and grey out the menu when there are
311        // no available devices.
312        menu_button->SetText(
313            l10n_util::GetStringUTF16(IDS_MEDIA_MENU_NO_DEVICE_TITLE));
314        menu_button->SetEnabled(false);
315      }
316
317      // Disable the device selection when the website is managing the devices
318      // itself.
319      if (i->second.disabled)
320        menu_button->SetEnabled(false);
321
322      // Use the longest width of the menus as the width of the menu buttons.
323      menu_width = std::max(menu_width,
324                            GetPreferredMediaMenuWidth(
325                                menu_button, menu_view->menu_model.get()));
326
327      layout->AddView(label);
328      layout->AddView(menu_button);
329
330      bubble_content_empty = false;
331    }
332
333    // Make sure the width is at least kMinMediaMenuButtonWidth. The
334    // maximum width will be clamped by kMaxContentsWidth of the view.
335    menu_width = std::max(kMinMediaMenuButtonWidth, menu_width);
336
337    // Set all the menu buttons to the width we calculated above.
338    for (MediaMenuPartsMap::const_iterator i = media_menus_.begin();
339         i != media_menus_.end(); ++i) {
340      i->first->set_min_width(menu_width);
341      i->first->set_max_width(menu_width);
342    }
343  }
344
345  const gfx::FontList& domain_font =
346      ui::ResourceBundle::GetSharedInstance().GetFontList(
347          ui::ResourceBundle::BoldFont);
348  for (std::vector<ContentSettingBubbleModel::DomainList>::const_iterator i(
349           bubble_content.domain_lists.begin());
350       i != bubble_content.domain_lists.end(); ++i) {
351    layout->StartRow(0, kSingleColumnSetId);
352    views::Label* section_title = new views::Label(base::UTF8ToUTF16(i->title));
353    section_title->SetMultiLine(true);
354    section_title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
355    layout->AddView(section_title, 1, 1, GridLayout::FILL, GridLayout::LEADING);
356    for (std::set<std::string>::const_iterator j = i->hosts.begin();
357         j != i->hosts.end(); ++j) {
358      layout->StartRow(0, indented_kSingleColumnSetId);
359      layout->AddView(new views::Label(base::UTF8ToUTF16(*j), domain_font));
360    }
361    bubble_content_empty = false;
362  }
363
364  if (!bubble_content.custom_link.empty()) {
365    custom_link_ =
366        new views::Link(base::UTF8ToUTF16(bubble_content.custom_link));
367    custom_link_->SetEnabled(bubble_content.custom_link_enabled);
368    custom_link_->set_listener(this);
369    if (!bubble_content_empty)
370      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
371    layout->StartRow(0, kSingleColumnSetId);
372    layout->AddView(custom_link_);
373    bubble_content_empty = false;
374  }
375
376  const int kDoubleColumnSetId = 1;
377  views::ColumnSet* double_column_set =
378      layout->AddColumnSet(kDoubleColumnSetId);
379  if (!bubble_content_empty) {
380      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
381      layout->StartRow(0, kSingleColumnSetId);
382      layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 1, 1,
383                      GridLayout::FILL, GridLayout::FILL);
384      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
385    }
386
387    double_column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 1,
388                                 GridLayout::USE_PREF, 0, 0);
389    double_column_set->AddPaddingColumn(
390        0, views::kUnrelatedControlHorizontalSpacing);
391    double_column_set->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 0,
392                                 GridLayout::USE_PREF, 0, 0);
393
394    layout->StartRow(0, kDoubleColumnSetId);
395    manage_link_ =
396        new views::Link(base::UTF8ToUTF16(bubble_content.manage_link));
397    manage_link_->set_listener(this);
398    layout->AddView(manage_link_);
399
400    close_button_ =
401        new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_DONE));
402    close_button_->SetStyle(views::Button::STYLE_BUTTON);
403    layout->AddView(close_button_);
404}
405
406void ContentSettingBubbleContents::DidNavigateMainFrame(
407    const content::LoadCommittedDetails& details,
408    const content::FrameNavigateParams& params) {
409  // Content settings are based on the main frame, so if it switches then
410  // close up shop.
411  content_setting_bubble_model_->OnDoneClicked();
412  GetWidget()->Close();
413}
414
415void ContentSettingBubbleContents::ButtonPressed(views::Button* sender,
416                                                 const ui::Event& event) {
417  RadioGroup::const_iterator i(
418      std::find(radio_group_.begin(), radio_group_.end(), sender));
419  if (i != radio_group_.end()) {
420    content_setting_bubble_model_->OnRadioClicked(i - radio_group_.begin());
421    return;
422  }
423  DCHECK_EQ(sender, close_button_);
424  content_setting_bubble_model_->OnDoneClicked();
425  GetWidget()->Close();
426}
427
428void ContentSettingBubbleContents::LinkClicked(views::Link* source,
429                                               int event_flags) {
430  if (source == custom_link_) {
431    content_setting_bubble_model_->OnCustomLinkClicked();
432    GetWidget()->Close();
433    return;
434  }
435  if (source == manage_link_) {
436    GetWidget()->Close();
437    content_setting_bubble_model_->OnManageLinkClicked();
438    // CAREFUL: Showing the settings window activates it, which deactivates the
439    // info bubble, which causes it to close, which deletes us.
440    return;
441  }
442
443  PopupLinks::const_iterator i(popup_links_.find(source));
444  DCHECK(i != popup_links_.end());
445  content_setting_bubble_model_->OnPopupClicked(i->second);
446}
447
448void ContentSettingBubbleContents::OnMenuButtonClicked(
449    views::View* source,
450    const gfx::Point& point) {
451    MediaMenuPartsMap::iterator j(media_menus_.find(
452        static_cast<views::MenuButton*>(source)));
453    DCHECK(j != media_menus_.end());
454    menu_runner_.reset(new views::MenuRunner(j->second->menu_model.get()));
455
456    gfx::Point screen_location;
457    views::View::ConvertPointToScreen(j->first, &screen_location);
458    ignore_result(
459        menu_runner_->RunMenuAt(source->GetWidget(),
460                                j->first,
461                                gfx::Rect(screen_location, j->first->size()),
462                                views::MENU_ANCHOR_TOPLEFT,
463                                ui::MENU_SOURCE_NONE,
464                                views::MenuRunner::HAS_MNEMONICS));
465}
466
467int ContentSettingBubbleContents::GetPreferredMediaMenuWidth(
468    views::MenuButton* button,
469    ui::SimpleMenuModel* menu_model) {
470  base::string16 title = button->text();
471
472  int width = button->GetPreferredSize().width();
473  for (int i = 0; i < menu_model->GetItemCount(); ++i) {
474    button->SetText(menu_model->GetLabelAt(i));
475    width = std::max(width, button->GetPreferredSize().width());
476  }
477
478  // Recover the title for the menu button.
479  button->SetText(title);
480  return width;
481}
482