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