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