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.
5 #include "chrome/browser/ui/views/content_setting_bubble_contents.h"
12 #include "base/bind.h"
13 #include "base/stl_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/browser/plugins/plugin_finder.h"
16 #include "chrome/browser/plugins/plugin_metadata.h"
17 #include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
18 #include "chrome/browser/ui/content_settings/content_setting_media_menu_model.h"
19 #include "chrome/browser/ui/views/browser_dialogs.h"
20 #include "chrome/grit/generated_resources.h"
21 #include "components/content_settings/core/browser/host_content_settings_map.h"
22 #include "content/public/browser/plugin_service.h"
23 #include "content/public/browser/web_contents.h"
24 #include "ui/base/cursor/cursor.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 #include "ui/views/native_cursor.h"
46 // If we don't clamp the maximum width, then very long URLs and titles can make
47 // the bubble arbitrarily wide.
48 const int kMaxContentsWidth
= 500;
50 // When we have multiline labels, we should set a minimum width lest we get very
51 // narrow bubbles with lots of line-wrapping.
52 const int kMinMultiLineContentsWidth
= 250;
54 // The minimum width of the media menu buttons.
55 const int kMinMediaMenuButtonWidth
= 150;
59 using content::PluginService
;
60 using content::WebContents
;
63 // ContentSettingBubbleContents::Favicon --------------------------------------
65 class ContentSettingBubbleContents::Favicon
: public views::ImageView
{
67 Favicon(const gfx::Image
& image
,
68 ContentSettingBubbleContents
* parent
,
73 // views::View overrides:
74 bool OnMousePressed(const ui::MouseEvent
& event
) override
;
75 void OnMouseReleased(const ui::MouseEvent
& event
) override
;
76 gfx::NativeCursor
GetCursor(const ui::MouseEvent
& event
) override
;
78 ContentSettingBubbleContents
* parent_
;
82 ContentSettingBubbleContents::Favicon::Favicon(
83 const gfx::Image
& image
,
84 ContentSettingBubbleContents
* parent
,
88 SetImage(image
.AsImageSkia());
91 ContentSettingBubbleContents::Favicon::~Favicon() {
94 bool ContentSettingBubbleContents::Favicon::OnMousePressed(
95 const ui::MouseEvent
& event
) {
96 return event
.IsLeftMouseButton() || event
.IsMiddleMouseButton();
99 void ContentSettingBubbleContents::Favicon::OnMouseReleased(
100 const ui::MouseEvent
& event
) {
101 if ((event
.IsLeftMouseButton() || event
.IsMiddleMouseButton()) &&
102 HitTestPoint(event
.location())) {
103 parent_
->LinkClicked(link_
, event
.flags());
107 gfx::NativeCursor
ContentSettingBubbleContents::Favicon::GetCursor(
108 const ui::MouseEvent
& event
) {
109 return views::GetNativeHandCursor();
113 // ContentSettingBubbleContents::MediaMenuParts -------------------------------
115 struct ContentSettingBubbleContents::MediaMenuParts
{
116 explicit MediaMenuParts(content::MediaStreamType type
);
119 content::MediaStreamType type
;
120 scoped_ptr
<ui::SimpleMenuModel
> menu_model
;
123 DISALLOW_COPY_AND_ASSIGN(MediaMenuParts
);
126 ContentSettingBubbleContents::MediaMenuParts::MediaMenuParts(
127 content::MediaStreamType type
)
130 ContentSettingBubbleContents::MediaMenuParts::~MediaMenuParts() {}
132 // ContentSettingBubbleContents -----------------------------------------------
134 ContentSettingBubbleContents::ContentSettingBubbleContents(
135 ContentSettingBubbleModel
* content_setting_bubble_model
,
136 content::WebContents
* web_contents
,
137 views::View
* anchor_view
,
138 views::BubbleBorder::Arrow arrow
)
139 : content::WebContentsObserver(web_contents
),
140 BubbleDelegateView(anchor_view
, arrow
),
141 content_setting_bubble_model_(content_setting_bubble_model
),
144 learn_more_link_(NULL
),
145 close_button_(NULL
) {
146 // Compensate for built-in vertical padding in the anchor view's image.
147 set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
150 ContentSettingBubbleContents::~ContentSettingBubbleContents() {
151 STLDeleteValues(&media_menus_
);
154 gfx::Size
ContentSettingBubbleContents::GetPreferredSize() const {
155 gfx::Size
preferred_size(views::View::GetPreferredSize());
156 int preferred_width
=
157 (!content_setting_bubble_model_
->bubble_content().domain_lists
.empty() &&
158 (kMinMultiLineContentsWidth
> preferred_size
.width())) ?
159 kMinMultiLineContentsWidth
: preferred_size
.width();
160 preferred_size
.set_width(std::min(preferred_width
, kMaxContentsWidth
));
161 return preferred_size
;
164 void ContentSettingBubbleContents::UpdateMenuLabel(
165 content::MediaStreamType type
,
166 const std::string
& label
) {
167 for (MediaMenuPartsMap::const_iterator it
= media_menus_
.begin();
168 it
!= media_menus_
.end(); ++it
) {
169 if (it
->second
->type
== type
) {
170 it
->first
->SetText(base::UTF8ToUTF16(label
));
177 void ContentSettingBubbleContents::Init() {
178 using views::GridLayout
;
180 GridLayout
* layout
= new views::GridLayout(this);
181 SetLayoutManager(layout
);
183 const int kSingleColumnSetId
= 0;
184 views::ColumnSet
* column_set
= layout
->AddColumnSet(kSingleColumnSetId
);
185 column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 1,
186 GridLayout::USE_PREF
, 0, 0);
187 column_set
->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing
);
188 column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 1,
189 GridLayout::USE_PREF
, 0, 0);
191 const ContentSettingBubbleModel::BubbleContent
& bubble_content
=
192 content_setting_bubble_model_
->bubble_content();
193 bool bubble_content_empty
= true;
195 if (!bubble_content
.title
.empty()) {
196 views::Label
* title_label
= new views::Label(base::UTF8ToUTF16(
197 bubble_content
.title
));
198 title_label
->SetMultiLine(true);
199 title_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
200 layout
->StartRow(0, kSingleColumnSetId
);
201 layout
->AddView(title_label
);
202 bubble_content_empty
= false;
205 if (!bubble_content
.learn_more_link
.empty()) {
207 new views::Link(base::UTF8ToUTF16(bubble_content
.learn_more_link
));
208 learn_more_link_
->set_listener(this);
209 learn_more_link_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
210 layout
->AddView(learn_more_link_
);
211 bubble_content_empty
= false;
214 if (!bubble_content
.plugin_names
.empty()) {
215 const int kPluginsColumnSetId
= 4;
216 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
217 views::ColumnSet
* plugins_column_set
=
218 layout
->AddColumnSet(kPluginsColumnSetId
);
219 plugins_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 1,
220 GridLayout::USE_PREF
, 0, 0);
221 plugins_column_set
->AddPaddingColumn(
222 0, views::kRelatedControlHorizontalSpacing
);
223 plugins_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 1,
224 GridLayout::USE_PREF
, 0, 0);
226 views::Label
* plugin_names_label
=
227 new views::Label(bubble_content
.plugin_names
);
228 plugin_names_label
->SetMultiLine(true);
229 plugin_names_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
230 layout
->StartRow(0, kPluginsColumnSetId
);
231 layout
->AddView(plugin_names_label
);
232 bubble_content_empty
= false;
235 if (content_setting_bubble_model_
->content_type() ==
236 CONTENT_SETTINGS_TYPE_POPUPS
) {
237 const int kPopupColumnSetId
= 2;
238 views::ColumnSet
* popup_column_set
=
239 layout
->AddColumnSet(kPopupColumnSetId
);
240 popup_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 0,
241 GridLayout::USE_PREF
, 0, 0);
242 popup_column_set
->AddPaddingColumn(
243 0, views::kRelatedControlHorizontalSpacing
);
244 popup_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 1,
245 GridLayout::USE_PREF
, 0, 0);
247 for (std::vector
<ContentSettingBubbleModel::PopupItem
>::const_iterator
248 i(bubble_content
.popup_items
.begin());
249 i
!= bubble_content
.popup_items
.end(); ++i
) {
250 if (!bubble_content_empty
)
251 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
252 layout
->StartRow(0, kPopupColumnSetId
);
254 views::Link
* link
= new views::Link(base::UTF8ToUTF16(i
->title
));
255 link
->set_listener(this);
256 link
->SetElideBehavior(gfx::ELIDE_MIDDLE
);
257 popup_links_
[link
] = i
- bubble_content
.popup_items
.begin();
258 layout
->AddView(new Favicon(i
->image
, this, link
));
259 layout
->AddView(link
);
260 bubble_content_empty
= false;
264 const int indented_kSingleColumnSetId
= 3;
265 // Insert a column set with greater indent.
266 views::ColumnSet
* indented_single_column_set
=
267 layout
->AddColumnSet(indented_kSingleColumnSetId
);
268 indented_single_column_set
->AddPaddingColumn(0, views::kCheckboxIndent
);
269 indented_single_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
,
270 1, GridLayout::USE_PREF
, 0, 0);
272 const ContentSettingBubbleModel::RadioGroup
& radio_group
=
273 bubble_content
.radio_group
;
274 if (!radio_group
.radio_items
.empty()) {
275 if (!bubble_content_empty
)
276 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
277 for (ContentSettingBubbleModel::RadioItems::const_iterator
i(
278 radio_group
.radio_items
.begin());
279 i
!= radio_group
.radio_items
.end(); ++i
) {
280 views::RadioButton
* radio
=
281 new views::RadioButton(base::UTF8ToUTF16(*i
), 0);
282 radio
->SetEnabled(bubble_content
.radio_group_enabled
);
283 radio
->set_listener(this);
284 radio_group_
.push_back(radio
);
285 layout
->StartRow(0, indented_kSingleColumnSetId
);
286 layout
->AddView(radio
);
287 bubble_content_empty
= false;
289 DCHECK(!radio_group_
.empty());
290 // Now that the buttons have been added to the view hierarchy, it's safe
291 // to call SetChecked() on them.
292 radio_group_
[radio_group
.default_item
]->SetChecked(true);
295 // Layout code for the media device menus.
296 if (content_setting_bubble_model_
->content_type() ==
297 CONTENT_SETTINGS_TYPE_MEDIASTREAM
) {
298 const int kMediaMenuColumnSetId
= 2;
299 views::ColumnSet
* menu_column_set
=
300 layout
->AddColumnSet(kMediaMenuColumnSetId
);
301 menu_column_set
->AddPaddingColumn(0, views::kCheckboxIndent
);
302 menu_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 0,
303 GridLayout::USE_PREF
, 0, 0);
304 menu_column_set
->AddPaddingColumn(
305 0, views::kRelatedControlHorizontalSpacing
);
306 menu_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::FILL
, 1,
307 GridLayout::USE_PREF
, 0, 0);
309 for (ContentSettingBubbleModel::MediaMenuMap::const_iterator
i(
310 bubble_content
.media_menus
.begin());
311 i
!= bubble_content
.media_menus
.end(); ++i
) {
312 if (!bubble_content_empty
)
313 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
314 layout
->StartRow(0, kMediaMenuColumnSetId
);
316 views::Label
* label
=
317 new views::Label(base::UTF8ToUTF16(i
->second
.label
));
318 label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
320 views::MenuButton
* menu_button
= new views::MenuButton(
321 NULL
, base::UTF8ToUTF16((i
->second
.selected_device
.name
)),
323 menu_button
->SetStyle(views::Button::STYLE_BUTTON
);
324 menu_button
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
325 menu_button
->set_animate_on_state_change(false);
327 MediaMenuParts
* menu_view
= new MediaMenuParts(i
->first
);
328 menu_view
->menu_model
.reset(new ContentSettingMediaMenuModel(
330 content_setting_bubble_model_
.get(),
331 base::Bind(&ContentSettingBubbleContents::UpdateMenuLabel
,
332 base::Unretained(this))));
333 media_menus_
[menu_button
] = menu_view
;
335 if (!menu_view
->menu_model
->GetItemCount()) {
336 // Show a "None available" title and grey out the menu when there are
337 // no available devices.
338 menu_button
->SetText(
339 l10n_util::GetStringUTF16(IDS_MEDIA_MENU_NO_DEVICE_TITLE
));
340 menu_button
->SetEnabled(false);
343 // Disable the device selection when the website is managing the devices
345 if (i
->second
.disabled
)
346 menu_button
->SetEnabled(false);
348 layout
->AddView(label
);
349 layout
->AddView(menu_button
);
351 bubble_content_empty
= false;
355 UpdateMenuButtonSizes(GetNativeTheme());
357 const gfx::FontList
& domain_font
=
358 ui::ResourceBundle::GetSharedInstance().GetFontList(
359 ui::ResourceBundle::BoldFont
);
360 for (std::vector
<ContentSettingBubbleModel::DomainList
>::const_iterator
i(
361 bubble_content
.domain_lists
.begin());
362 i
!= bubble_content
.domain_lists
.end(); ++i
) {
363 layout
->StartRow(0, kSingleColumnSetId
);
364 views::Label
* section_title
= new views::Label(base::UTF8ToUTF16(i
->title
));
365 section_title
->SetMultiLine(true);
366 section_title
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
367 layout
->AddView(section_title
, 1, 1, GridLayout::FILL
, GridLayout::LEADING
);
368 for (std::set
<std::string
>::const_iterator j
= i
->hosts
.begin();
369 j
!= i
->hosts
.end(); ++j
) {
370 layout
->StartRow(0, indented_kSingleColumnSetId
);
371 layout
->AddView(new views::Label(base::UTF8ToUTF16(*j
), domain_font
));
373 bubble_content_empty
= false;
376 if (!bubble_content
.custom_link
.empty()) {
378 new views::Link(base::UTF8ToUTF16(bubble_content
.custom_link
));
379 custom_link_
->SetEnabled(bubble_content
.custom_link_enabled
);
380 custom_link_
->set_listener(this);
381 if (!bubble_content_empty
)
382 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
383 layout
->StartRow(0, kSingleColumnSetId
);
384 layout
->AddView(custom_link_
);
385 bubble_content_empty
= false;
388 const int kDoubleColumnSetId
= 1;
389 views::ColumnSet
* double_column_set
=
390 layout
->AddColumnSet(kDoubleColumnSetId
);
391 if (!bubble_content_empty
) {
392 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
393 layout
->StartRow(0, kSingleColumnSetId
);
394 layout
->AddView(new views::Separator(views::Separator::HORIZONTAL
), 1, 1,
395 GridLayout::FILL
, GridLayout::FILL
);
396 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
399 double_column_set
->AddColumn(GridLayout::LEADING
, GridLayout::CENTER
, 1,
400 GridLayout::USE_PREF
, 0, 0);
401 double_column_set
->AddPaddingColumn(
402 0, views::kUnrelatedControlHorizontalSpacing
);
403 double_column_set
->AddColumn(GridLayout::TRAILING
, GridLayout::CENTER
, 0,
404 GridLayout::USE_PREF
, 0, 0);
406 layout
->StartRow(0, kDoubleColumnSetId
);
408 new views::Link(base::UTF8ToUTF16(bubble_content
.manage_link
));
409 manage_link_
->set_listener(this);
410 layout
->AddView(manage_link_
);
413 new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_DONE
));
414 close_button_
->SetStyle(views::Button::STYLE_BUTTON
);
415 layout
->AddView(close_button_
);
418 void ContentSettingBubbleContents::DidNavigateMainFrame(
419 const content::LoadCommittedDetails
& details
,
420 const content::FrameNavigateParams
& params
) {
421 // Content settings are based on the main frame, so if it switches then
423 content_setting_bubble_model_
->OnDoneClicked();
424 GetWidget()->Close();
427 void ContentSettingBubbleContents::OnNativeThemeChanged(
428 const ui::NativeTheme
* theme
) {
429 views::BubbleDelegateView::OnNativeThemeChanged(theme
);
430 UpdateMenuButtonSizes(theme
);
433 void ContentSettingBubbleContents::ButtonPressed(views::Button
* sender
,
434 const ui::Event
& event
) {
435 RadioGroup::const_iterator
i(
436 std::find(radio_group_
.begin(), radio_group_
.end(), sender
));
437 if (i
!= radio_group_
.end()) {
438 content_setting_bubble_model_
->OnRadioClicked(i
- radio_group_
.begin());
441 DCHECK_EQ(sender
, close_button_
);
442 content_setting_bubble_model_
->OnDoneClicked();
443 GetWidget()->Close();
446 void ContentSettingBubbleContents::LinkClicked(views::Link
* source
,
448 if (source
== learn_more_link_
) {
449 content_setting_bubble_model_
->OnLearnMoreLinkClicked();
450 GetWidget()->Close();
453 if (source
== custom_link_
) {
454 content_setting_bubble_model_
->OnCustomLinkClicked();
455 GetWidget()->Close();
458 if (source
== manage_link_
) {
459 GetWidget()->Close();
460 content_setting_bubble_model_
->OnManageLinkClicked();
461 // CAREFUL: Showing the settings window activates it, which deactivates the
462 // info bubble, which causes it to close, which deletes us.
466 PopupLinks::const_iterator
i(popup_links_
.find(source
));
467 DCHECK(i
!= popup_links_
.end());
468 content_setting_bubble_model_
->OnPopupClicked(i
->second
);
471 void ContentSettingBubbleContents::OnMenuButtonClicked(
473 const gfx::Point
& point
) {
474 MediaMenuPartsMap::iterator
j(media_menus_
.find(
475 static_cast<views::MenuButton
*>(source
)));
476 DCHECK(j
!= media_menus_
.end());
477 menu_runner_
.reset(new views::MenuRunner(j
->second
->menu_model
.get(),
478 views::MenuRunner::HAS_MNEMONICS
));
480 gfx::Point screen_location
;
481 views::View::ConvertPointToScreen(j
->first
, &screen_location
);
483 menu_runner_
->RunMenuAt(source
->GetWidget(),
485 gfx::Rect(screen_location
, j
->first
->size()),
486 views::MENU_ANCHOR_TOPLEFT
,
487 ui::MENU_SOURCE_NONE
));
490 void ContentSettingBubbleContents::UpdateMenuButtonSizes(
491 const ui::NativeTheme
* theme
) {
492 const views::MenuConfig config
= views::MenuConfig(theme
);
493 const int margins
= config
.item_left_margin
+ config
.check_width
+
494 config
.label_to_arrow_padding
+ config
.arrow_width
+
495 config
.arrow_to_edge_padding
;
497 // The preferred media menu size sort of copies the logic in
498 // MenuItemView::CalculateDimensions(). When this was using TextButton, it
499 // completely coincidentally matched the logic in MenuItemView. We now need
500 // to redo this manually.
502 for (MediaMenuPartsMap::const_iterator i
= media_menus_
.begin();
503 i
!= media_menus_
.end(); ++i
) {
504 for (int j
= 0; j
< i
->second
->menu_model
->GetItemCount(); ++j
) {
505 int string_width
= gfx::GetStringWidth(
506 i
->second
->menu_model
->GetLabelAt(j
),
509 menu_width
= std::max(menu_width
, string_width
);
513 // Make sure the width is at least kMinMediaMenuButtonWidth. The
514 // maximum width will be clamped by kMaxContentsWidth of the view.
515 menu_width
= std::max(kMinMediaMenuButtonWidth
, menu_width
+ margins
);
517 for (MediaMenuPartsMap::const_iterator i
= media_menus_
.begin();
518 i
!= media_menus_
.end(); ++i
) {
519 i
->first
->SetMinSize(gfx::Size(menu_width
, 0));
520 i
->first
->SetMaxSize(gfx::Size(menu_width
, 0));