content_setting_bubble_contents.cc revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
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/notification_source.h"
22#include "content/public/browser/notification_types.h"
23#include "content/public/browser/plugin_service.h"
24#include "content/public/browser/web_contents.h"
25#include "grit/generated_resources.h"
26#include "grit/theme_resources.h"
27#include "ui/base/l10n/l10n_util.h"
28#include "ui/base/models/simple_menu_model.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
139// ContentSettingBubbleContents -----------------------------------------------
140
141ContentSettingBubbleContents::ContentSettingBubbleContents(
142    ContentSettingBubbleModel* content_setting_bubble_model,
143    WebContents* web_contents,
144    views::View* anchor_view,
145    views::BubbleBorder::Arrow arrow)
146    : BubbleDelegateView(anchor_view, arrow),
147      content_setting_bubble_model_(content_setting_bubble_model),
148      web_contents_(web_contents),
149      cancel_button_(NULL),
150      save_button_(NULL),
151      custom_link_(NULL),
152      manage_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  registrar_.Add(this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
158                 content::Source<WebContents>(web_contents));
159}
160
161ContentSettingBubbleContents::~ContentSettingBubbleContents() {
162  STLDeleteValues(&media_menus_);
163}
164
165gfx::Size ContentSettingBubbleContents::GetPreferredSize() {
166  gfx::Size preferred_size(views::View::GetPreferredSize());
167  int preferred_width =
168      (!content_setting_bubble_model_->bubble_content().domain_lists.empty() &&
169       (kMinMultiLineContentsWidth > preferred_size.width())) ?
170      kMinMultiLineContentsWidth : preferred_size.width();
171  preferred_size.set_width(std::min(preferred_width, kMaxContentsWidth));
172  return preferred_size;
173}
174
175void ContentSettingBubbleContents::UpdateMenuLabel(
176    content::MediaStreamType type,
177    const std::string& label) {
178  for (MediaMenuPartsMap::const_iterator it = media_menus_.begin();
179       it != media_menus_.end(); ++it) {
180    if (it->second->type == type) {
181      it->first->SetText(UTF8ToUTF16(label));
182      return;
183    }
184  }
185  NOTREACHED();
186}
187
188void ContentSettingBubbleContents::Init() {
189  using views::GridLayout;
190
191  GridLayout* layout = new views::GridLayout(this);
192  SetLayoutManager(layout);
193
194  const int kSingleColumnSetId = 0;
195  views::ColumnSet* column_set = layout->AddColumnSet(kSingleColumnSetId);
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(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  const std::set<std::string>& plugins = bubble_content.resource_identifiers;
214  if (!plugins.empty()) {
215    if (!bubble_content_empty)
216      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
217    PluginFinder* finder = PluginFinder::GetInstance();
218    for (std::set<std::string>::const_iterator i(plugins.begin());
219         i != plugins.end(); ++i) {
220      string16 name = finder->FindPluginNameWithIdentifier(*i);
221      layout->StartRow(0, kSingleColumnSetId);
222      layout->AddView(new views::Label(name));
223      bubble_content_empty = false;
224    }
225  }
226
227  if (content_setting_bubble_model_->content_type() ==
228      CONTENT_SETTINGS_TYPE_POPUPS) {
229    const int kPopupColumnSetId = 2;
230    views::ColumnSet* popup_column_set =
231        layout->AddColumnSet(kPopupColumnSetId);
232    popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
233                                GridLayout::USE_PREF, 0, 0);
234    popup_column_set->AddPaddingColumn(
235        0, views::kRelatedControlHorizontalSpacing);
236    popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
237                                GridLayout::USE_PREF, 0, 0);
238
239    for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator
240         i(bubble_content.popup_items.begin());
241         i != bubble_content.popup_items.end(); ++i) {
242      if (!bubble_content_empty)
243        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
244      layout->StartRow(0, kPopupColumnSetId);
245
246      views::Link* link = new views::Link(UTF8ToUTF16(i->title));
247      link->set_listener(this);
248      link->SetElideBehavior(views::Label::ELIDE_IN_MIDDLE);
249      popup_links_[link] = i - bubble_content.popup_items.begin();
250      layout->AddView(new Favicon(i->image, this, link));
251      layout->AddView(link);
252      bubble_content_empty = false;
253    }
254  }
255
256  const int indented_kSingleColumnSetId = 3;
257  // Insert a column set with greater indent.
258  views::ColumnSet* indented_single_column_set =
259      layout->AddColumnSet(indented_kSingleColumnSetId);
260  indented_single_column_set->AddPaddingColumn(0, views::kCheckboxIndent);
261  indented_single_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL,
262                                        1, GridLayout::USE_PREF, 0, 0);
263
264  const ContentSettingBubbleModel::RadioGroup& radio_group =
265      bubble_content.radio_group;
266  if (!radio_group.radio_items.empty()) {
267    if (!bubble_content_empty)
268      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
269    for (ContentSettingBubbleModel::RadioItems::const_iterator i(
270         radio_group.radio_items.begin());
271         i != radio_group.radio_items.end(); ++i) {
272      views::RadioButton* radio = new views::RadioButton(UTF8ToUTF16(*i), 0);
273      radio->SetEnabled(bubble_content.radio_group_enabled);
274      radio->set_listener(this);
275      radio_group_.push_back(radio);
276      layout->StartRow(0, indented_kSingleColumnSetId);
277      layout->AddView(radio);
278      bubble_content_empty = false;
279    }
280    DCHECK(!radio_group_.empty());
281    // Now that the buttons have been added to the view hierarchy, it's safe
282    // to call SetChecked() on them.
283    radio_group_[radio_group.default_item]->SetChecked(true);
284  }
285
286  // Layout code for the media device menus.
287  if (content_setting_bubble_model_->content_type() ==
288      CONTENT_SETTINGS_TYPE_MEDIASTREAM) {
289    const int kMediaMenuColumnSetId = 2;
290    views::ColumnSet* menu_column_set =
291        layout->AddColumnSet(kMediaMenuColumnSetId);
292    menu_column_set->AddPaddingColumn(0, views::kCheckboxIndent);
293    menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
294                               GridLayout::USE_PREF, 0, 0);
295    menu_column_set->AddPaddingColumn(
296        0, views::kRelatedControlHorizontalSpacing);
297    menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
298                               GridLayout::USE_PREF, 0, 0);
299
300    int menu_width = 0;
301    for (ContentSettingBubbleModel::MediaMenuMap::const_iterator i(
302         bubble_content.media_menus.begin());
303         i != bubble_content.media_menus.end(); ++i) {
304      if (!bubble_content_empty)
305        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
306      layout->StartRow(0, kMediaMenuColumnSetId);
307
308      views::Label* label = new views::Label(UTF8ToUTF16(i->second.label));
309      label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
310
311      views::MenuButton* menu_button = new views::MenuButton(
312          NULL, UTF8ToUTF16((i->second.selected_device.name)), this, true);
313      menu_button->set_alignment(views::TextButton::ALIGN_LEFT);
314      menu_button->set_border(
315          new views::TextButtonNativeThemeBorder(menu_button));
316      menu_button->set_animate_on_state_change(false);
317
318      MediaMenuParts* menu_view = new MediaMenuParts(i->first);
319      menu_view->menu_model.reset(new ContentSettingMediaMenuModel(
320          i->first,
321          content_setting_bubble_model_.get(),
322          base::Bind(&ContentSettingBubbleContents::UpdateMenuLabel,
323                     base::Unretained(this))));
324      media_menus_[menu_button] = menu_view;
325
326      if (!menu_view->menu_model->GetItemCount()) {
327        // Show a "None available" title and grey out the menu when there is no
328        // available device.
329        menu_button->SetText(
330            l10n_util::GetStringUTF16(IDS_MEDIA_MENU_NO_DEVICE_TITLE));
331        menu_button->SetEnabled(false);
332      }
333
334      // Use the longest width of the menus as the width of the menu buttons.
335      menu_width = std::max(menu_width,
336                            GetPreferredMediaMenuWidth(
337                                menu_button, menu_view->menu_model.get()));
338
339      layout->AddView(label);
340      layout->AddView(menu_button);
341
342      bubble_content_empty = false;
343    }
344
345    // Make sure the width is at least kMinMediaMenuButtonWidth. The
346    // maximum width will be clamped by kMaxContentsWidth of the view.
347    menu_width = std::max(kMinMediaMenuButtonWidth, menu_width);
348
349    // Set all the menu buttons to the width we calculated above.
350    for (MediaMenuPartsMap::const_iterator i = media_menus_.begin();
351         i != media_menus_.end(); ++i) {
352      i->first->set_min_width(menu_width);
353      i->first->set_max_width(menu_width);
354    }
355  }
356
357  gfx::Font domain_font =
358      views::Label().font().DeriveFont(0, gfx::Font::BOLD);
359  for (std::vector<ContentSettingBubbleModel::DomainList>::const_iterator i(
360       bubble_content.domain_lists.begin());
361       i != bubble_content.domain_lists.end(); ++i) {
362    layout->StartRow(0, kSingleColumnSetId);
363    views::Label* section_title = new views::Label(UTF8ToUTF16(i->title));
364    section_title->SetMultiLine(true);
365    section_title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
366    layout->AddView(section_title, 1, 1, GridLayout::FILL, GridLayout::LEADING);
367    for (std::set<std::string>::const_iterator j = i->hosts.begin();
368         j != i->hosts.end(); ++j) {
369      layout->StartRow(0, indented_kSingleColumnSetId);
370      layout->AddView(new views::Label(UTF8ToUTF16(*j), domain_font));
371    }
372    bubble_content_empty = false;
373  }
374
375  if (!bubble_content.custom_link.empty()) {
376    custom_link_ = new views::Link(UTF8ToUTF16(bubble_content.custom_link));
377    custom_link_->SetEnabled(bubble_content.custom_link_enabled);
378    custom_link_->set_listener(this);
379    if (!bubble_content_empty)
380      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
381    layout->StartRow(0, kSingleColumnSetId);
382    layout->AddView(custom_link_);
383    bubble_content_empty = false;
384  }
385
386  const int kDoubleColumnSetId = 1;
387  views::ColumnSet* double_column_set =
388      layout->AddColumnSet(kDoubleColumnSetId);
389  if (content_setting_bubble_model_->content_type() ==
390      CONTENT_SETTINGS_TYPE_SAVE_PASSWORD) {
391    double_column_set->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 1,
392                                 GridLayout::USE_PREF, 0, 0);
393    double_column_set->AddPaddingColumn(
394        0, views::kRelatedControlSmallVerticalSpacing);
395    double_column_set->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 0,
396                                 GridLayout::USE_PREF, 0, 0);
397
398    const int kSingleColumnRightSetId = 2;
399    views::ColumnSet* right_column_set =
400        layout->AddColumnSet(kSingleColumnRightSetId);
401    right_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
402                                GridLayout::USE_PREF, 0, 0);
403
404    cancel_button_ = new views::LabelButton(
405        this, l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_BLACKLIST_BUTTON));
406    cancel_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
407    save_button_ = new views::LabelButton(
408        this, l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_SAVE_BUTTON));
409    save_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
410    manage_link_ = new views::Link(UTF8ToUTF16(bubble_content.manage_link));
411    manage_link_->set_listener(this);
412
413    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
414
415    layout->StartRow(0, kDoubleColumnSetId);
416    layout->AddView(cancel_button_);
417    layout->AddView(save_button_);
418
419    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
420    layout->StartRow(0, kSingleColumnSetId);
421    layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 1, 1,
422                    GridLayout::FILL, GridLayout::FILL);
423    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
424
425    layout->StartRow(0, kSingleColumnRightSetId);
426    layout->AddView(manage_link_);
427  } else {
428    if (!bubble_content_empty) {
429      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
430      layout->StartRow(0, kSingleColumnSetId);
431      layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 1, 1,
432                      GridLayout::FILL, GridLayout::FILL);
433      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
434    }
435
436    double_column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 1,
437                                 GridLayout::USE_PREF, 0, 0);
438    double_column_set->AddPaddingColumn(
439        0, views::kUnrelatedControlHorizontalSpacing);
440    double_column_set->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 0,
441                                 GridLayout::USE_PREF, 0, 0);
442
443    layout->StartRow(0, kDoubleColumnSetId);
444    manage_link_ = new views::Link(UTF8ToUTF16(bubble_content.manage_link));
445    manage_link_->set_listener(this);
446    layout->AddView(manage_link_);
447
448    close_button_ =
449        new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_DONE));
450    close_button_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON);
451    layout->AddView(close_button_);
452  }
453}
454
455void ContentSettingBubbleContents::ButtonPressed(views::Button* sender,
456                                                 const ui::Event& event) {
457  RadioGroup::const_iterator i(
458      std::find(radio_group_.begin(), radio_group_.end(), sender));
459  if (i != radio_group_.end()) {
460    content_setting_bubble_model_->OnRadioClicked(i - radio_group_.begin());
461    return;
462  }
463
464  if (sender == save_button_)
465    content_setting_bubble_model_->OnSaveClicked();
466  else if (sender == cancel_button_)
467    content_setting_bubble_model_->OnCancelClicked();
468  else if (sender == close_button_)
469    content_setting_bubble_model_->OnDoneClicked();
470  else
471    NOTREACHED();
472  StartFade(false);
473}
474
475void ContentSettingBubbleContents::LinkClicked(views::Link* source,
476                                               int event_flags) {
477  if (source == custom_link_) {
478    content_setting_bubble_model_->OnCustomLinkClicked();
479    StartFade(false);
480    return;
481  }
482  if (source == manage_link_) {
483    StartFade(false);
484    content_setting_bubble_model_->OnManageLinkClicked();
485    // CAREFUL: Showing the settings window activates it, which deactivates the
486    // info bubble, which causes it to close, which deletes us.
487    return;
488  }
489
490  PopupLinks::const_iterator i(popup_links_.find(source));
491  DCHECK(i != popup_links_.end());
492  content_setting_bubble_model_->OnPopupClicked(i->second);
493}
494
495void ContentSettingBubbleContents::OnMenuButtonClicked(
496    views::View* source,
497    const gfx::Point& point) {
498  MediaMenuPartsMap::iterator i(media_menus_.find(
499      static_cast<views::MenuButton*>(source)));
500  DCHECK(i != media_menus_.end());
501
502  menu_runner_.reset(new views::MenuRunner(i->second->menu_model.get()));
503
504  gfx::Point screen_location;
505  views::View::ConvertPointToScreen(i->first, &screen_location);
506  ignore_result(menu_runner_->RunMenuAt(
507      source->GetWidget(),
508      i->first,
509      gfx::Rect(screen_location, i->first->size()),
510      views::MenuItemView::TOPLEFT,
511      ui::MENU_SOURCE_NONE,
512      views::MenuRunner::HAS_MNEMONICS));
513}
514
515void ContentSettingBubbleContents::Observe(
516    int type,
517    const content::NotificationSource& source,
518    const content::NotificationDetails& details) {
519  DCHECK_EQ(content::NOTIFICATION_WEB_CONTENTS_DESTROYED, type);
520  DCHECK(source == content::Source<WebContents>(web_contents_));
521  web_contents_ = NULL;
522}
523
524int ContentSettingBubbleContents::GetPreferredMediaMenuWidth(
525    views::MenuButton* button,
526    ui::SimpleMenuModel* menu_model) {
527  string16 title = button->text();
528
529  int width = button->GetPreferredSize().width();
530  for (int i = 0; i < menu_model->GetItemCount(); ++i) {
531    button->SetText(menu_model->GetLabelAt(i));
532    width = std::max(width, button->GetPreferredSize().width());
533  }
534
535  // Recover the title for the menu button.
536  button->SetText(title);
537  return width;
538}
539