blob: 6d0ade5ffe0f0cbbf4fa4092130fd9b44a1151a0 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/content_setting_bubble_contents.h"
#include <algorithm>
#include <set>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/plugins/plugin_finder.h"
#include "chrome/browser/plugins/plugin_metadata.h"
#include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/harmony/chrome_typography.h"
#include "chrome/browser/ui/views/harmony/layout_delegate.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/plugin_service.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/default_style.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_utils.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/button/radio_button.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/layout/layout_constants.h"
#include "ui/views/native_cursor.h"
namespace {
// If we don't clamp the maximum width, then very long URLs and titles can make
// the bubble arbitrarily wide.
const int kMaxContentsWidth = 500;
// The new default width for the content settings bubble. The review process to
// the width on per-bubble basis is tracked with https://crbug.com/649650.
const int kMaxDefaultContentsWidth = 320;
// When we have multiline labels, we should set a minimum width lest we get very
// narrow bubbles with lots of line-wrapping.
const int kMinMultiLineContentsWidth = 250;
} // namespace
using content::PluginService;
using content::WebContents;
// ContentSettingBubbleContents::MediaComboboxModel ----------------------------
ContentSettingBubbleContents::MediaComboboxModel::MediaComboboxModel(
content::MediaStreamType type)
: type_(type) {
DCHECK(type_ == content::MEDIA_DEVICE_AUDIO_CAPTURE ||
type_ == content::MEDIA_DEVICE_VIDEO_CAPTURE);
}
ContentSettingBubbleContents::MediaComboboxModel::~MediaComboboxModel() {}
const content::MediaStreamDevices&
ContentSettingBubbleContents::MediaComboboxModel::GetDevices() const {
MediaCaptureDevicesDispatcher* dispatcher =
MediaCaptureDevicesDispatcher::GetInstance();
return type_ == content::MEDIA_DEVICE_AUDIO_CAPTURE
? dispatcher->GetAudioCaptureDevices()
: dispatcher->GetVideoCaptureDevices();
}
int ContentSettingBubbleContents::MediaComboboxModel::GetDeviceIndex(
const content::MediaStreamDevice& device) const {
const auto& devices = GetDevices();
for (size_t i = 0; i < devices.size(); ++i) {
if (device.id == devices[i].id)
return i;
}
NOTREACHED();
return 0;
}
int ContentSettingBubbleContents::MediaComboboxModel::GetItemCount() const {
return std::max(1, static_cast<int>(GetDevices().size()));
}
base::string16 ContentSettingBubbleContents::MediaComboboxModel::GetItemAt(
int index) {
return GetDevices().empty()
? l10n_util::GetStringUTF16(IDS_MEDIA_MENU_NO_DEVICE_TITLE)
: base::UTF8ToUTF16(GetDevices()[index].name);
}
// ContentSettingBubbleContents::Favicon --------------------------------------
class ContentSettingBubbleContents::Favicon : public views::ImageView {
public:
Favicon(const gfx::Image& image,
ContentSettingBubbleContents* parent,
views::Link* link);
~Favicon() override;
private:
// views::View overrides:
bool OnMousePressed(const ui::MouseEvent& event) override;
void OnMouseReleased(const ui::MouseEvent& event) override;
gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override;
ContentSettingBubbleContents* parent_;
views::Link* link_;
};
ContentSettingBubbleContents::Favicon::Favicon(
const gfx::Image& image,
ContentSettingBubbleContents* parent,
views::Link* link)
: parent_(parent),
link_(link) {
SetImage(image.AsImageSkia());
}
ContentSettingBubbleContents::Favicon::~Favicon() {
}
bool ContentSettingBubbleContents::Favicon::OnMousePressed(
const ui::MouseEvent& event) {
return event.IsLeftMouseButton() || event.IsMiddleMouseButton();
}
void ContentSettingBubbleContents::Favicon::OnMouseReleased(
const ui::MouseEvent& event) {
if ((event.IsLeftMouseButton() || event.IsMiddleMouseButton()) &&
HitTestPoint(event.location())) {
parent_->LinkClicked(link_, event.flags());
}
}
gfx::NativeCursor ContentSettingBubbleContents::Favicon::GetCursor(
const ui::MouseEvent& event) {
return views::GetNativeHandCursor();
}
// ContentSettingBubbleContents -----------------------------------------------
ContentSettingBubbleContents::ContentSettingBubbleContents(
ContentSettingBubbleModel* content_setting_bubble_model,
content::WebContents* web_contents,
views::View* anchor_view,
views::BubbleBorder::Arrow arrow)
: content::WebContentsObserver(web_contents),
BubbleDialogDelegateView(anchor_view, arrow),
content_setting_bubble_model_(content_setting_bubble_model),
custom_link_(nullptr),
manage_link_(nullptr),
manage_button_(nullptr),
learn_more_link_(nullptr) {
// Compensate for built-in vertical padding in the anchor view's image.
set_anchor_view_insets(gfx::Insets(
GetLayoutConstant(LOCATION_BAR_BUBBLE_ANCHOR_VERTICAL_INSET), 0));
}
ContentSettingBubbleContents::~ContentSettingBubbleContents() {
// Must remove the children here so the comboboxes get destroyed before
// their associated models.
RemoveAllChildViews(true);
}
gfx::Size ContentSettingBubbleContents::GetPreferredSize() const {
gfx::Size preferred_size(views::View::GetPreferredSize());
int preferred_width = LayoutDelegate::Get()->GetDialogPreferredWidth(
LayoutDelegate::DialogWidth::SMALL);
if (!preferred_width)
preferred_width = (!content_setting_bubble_model_->bubble_content()
.domain_lists.empty() &&
(kMinMultiLineContentsWidth > preferred_size.width()))
? kMinMultiLineContentsWidth
: preferred_size.width();
else
preferred_width -= margins().width();
if (content_setting_bubble_model_->AsSubresourceFilterBubbleModel()) {
preferred_size.set_width(std::min(preferred_width,
kMaxDefaultContentsWidth));
} else {
preferred_size.set_width(std::min(preferred_width, kMaxContentsWidth));
}
return preferred_size;
}
void ContentSettingBubbleContents::Init() {
using views::GridLayout;
GridLayout* layout = new views::GridLayout(this);
SetLayoutManager(layout);
const LayoutDelegate* layout_delegate = LayoutDelegate::Get();
const int related_control_horizontal_spacing = layout_delegate->GetMetric(
LayoutDelegate::Metric::RELATED_CONTROL_HORIZONTAL_SPACING);
const int related_control_vertical_spacing = layout_delegate->GetMetric(
LayoutDelegate::Metric::RELATED_CONTROL_VERTICAL_SPACING);
const int unrelated_control_vertical_spacing = layout_delegate->GetMetric(
LayoutDelegate::Metric::UNRELATED_CONTROL_VERTICAL_SPACING);
const int kSingleColumnSetId = 0;
views::ColumnSet* column_set = layout->AddColumnSet(kSingleColumnSetId);
column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
GridLayout::USE_PREF, 0, 0);
column_set->AddPaddingColumn(0, related_control_horizontal_spacing);
column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
GridLayout::USE_PREF, 0, 0);
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model_->bubble_content();
bool bubble_content_empty = true;
if (!bubble_content.title.empty()) {
const int title_context =
layout_delegate->IsHarmonyMode()
? static_cast<int>(views::style::CONTEXT_DIALOG_TITLE)
: CONTEXT_BODY_TEXT_SMALL;
views::Label* title_label =
new views::Label(bubble_content.title, title_context);
title_label->SetMultiLine(true);
title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->StartRow(0, kSingleColumnSetId);
layout->AddView(title_label);
bubble_content_empty = false;
}
if (!bubble_content.message.empty()) {
views::Label* message_label = new views::Label(bubble_content.message);
layout->AddPaddingRow(0, unrelated_control_vertical_spacing);
message_label->SetMultiLine(true);
message_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->StartRow(0, kSingleColumnSetId);
layout->AddView(message_label);
bubble_content_empty = false;
}
if (!bubble_content.learn_more_link.empty()) {
learn_more_link_ =
new views::Link(base::UTF8ToUTF16(bubble_content.learn_more_link));
learn_more_link_->set_listener(this);
learn_more_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->AddView(learn_more_link_);
bubble_content_empty = false;
}
// Layout for the item list (blocked plugins and popups).
if (!bubble_content.list_items.empty()) {
const int kItemListColumnSetId = 2;
views::ColumnSet* item_list_column_set =
layout->AddColumnSet(kItemListColumnSetId);
item_list_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
GridLayout::USE_PREF, 0, 0);
item_list_column_set->AddPaddingColumn(0,
related_control_horizontal_spacing);
item_list_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
GridLayout::USE_PREF, 0, 0);
int row = 0;
for (const ContentSettingBubbleModel::ListItem& list_item :
bubble_content.list_items) {
if (!bubble_content_empty)
layout->AddPaddingRow(0, related_control_vertical_spacing);
layout->StartRow(0, kItemListColumnSetId);
if (list_item.has_link) {
views::Link* link = new views::Link(base::UTF8ToUTF16(list_item.title));
link->set_listener(this);
link->SetElideBehavior(gfx::ELIDE_MIDDLE);
list_item_links_[link] = row;
layout->AddView(new Favicon(list_item.image, this, link));
layout->AddView(link);
} else {
views::ImageView* icon = new views::ImageView();
icon->SetImage(list_item.image.AsImageSkia());
layout->AddView(icon);
layout->AddView(new views::Label(base::UTF8ToUTF16(list_item.title)));
}
row++;
bubble_content_empty = false;
}
}
const int indented_kSingleColumnSetId = 3;
// Insert a column set with greater indent.
views::ColumnSet* indented_single_column_set =
layout->AddColumnSet(indented_kSingleColumnSetId);
indented_single_column_set->AddPaddingColumn(
0,
layout_delegate->GetMetric(
LayoutDelegate::Metric::SUBSECTION_HORIZONTAL_INDENT));
indented_single_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL,
1, GridLayout::USE_PREF, 0, 0);
const ContentSettingBubbleModel::RadioGroup& radio_group =
bubble_content.radio_group;
if (!radio_group.radio_items.empty()) {
if (!bubble_content_empty)
layout->AddPaddingRow(0, related_control_vertical_spacing);
for (ContentSettingBubbleModel::RadioItems::const_iterator i(
radio_group.radio_items.begin());
i != radio_group.radio_items.end(); ++i) {
views::RadioButton* radio =
new views::RadioButton(base::UTF8ToUTF16(*i), 0);
radio->SetEnabled(bubble_content.radio_group_enabled);
radio->set_listener(this);
if (layout_delegate->IsHarmonyMode()) {
std::unique_ptr<views::LabelButtonBorder> border =
radio->CreateDefaultBorder();
gfx::Insets insets = border->GetInsets();
border->set_insets(
gfx::Insets(insets.top(), 0, insets.bottom(), insets.right()));
radio->SetBorder(std::move(border));
}
radio_group_.push_back(radio);
layout->StartRow(0, indented_kSingleColumnSetId);
layout->AddView(radio);
bubble_content_empty = false;
}
DCHECK(!radio_group_.empty());
// Now that the buttons have been added to the view hierarchy, it's safe
// to call SetChecked() on them.
radio_group_[radio_group.default_item]->SetChecked(true);
}
// Layout code for the media device menus.
if (content_setting_bubble_model_->AsMediaStreamBubbleModel()) {
const int kMediaMenuColumnSetId = 4;
views::ColumnSet* menu_column_set =
layout->AddColumnSet(kMediaMenuColumnSetId);
menu_column_set->AddPaddingColumn(
0,
layout_delegate->GetMetric(
LayoutDelegate::Metric::SUBSECTION_HORIZONTAL_INDENT));
menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
GridLayout::USE_PREF, 0, 0);
menu_column_set->AddPaddingColumn(0, related_control_horizontal_spacing);
menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
GridLayout::USE_PREF, 0, 0);
for (ContentSettingBubbleModel::MediaMenuMap::const_iterator i(
bubble_content.media_menus.begin());
i != bubble_content.media_menus.end(); ++i) {
if (!bubble_content_empty)
layout->AddPaddingRow(0, related_control_vertical_spacing);
layout->StartRow(0, kMediaMenuColumnSetId);
views::Label* label =
new views::Label(base::UTF8ToUTF16(i->second.label));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->AddView(label);
combobox_models_.emplace_back(i->first);
MediaComboboxModel* model = &combobox_models_.back();
views::Combobox* combobox = new views::Combobox(model);
// Disable the device selection when the website is managing the devices
// itself or if there are no devices present.
combobox->SetEnabled(
!(i->second.disabled || model->GetDevices().empty()));
combobox->set_listener(this);
combobox->SetSelectedIndex(
model->GetDevices().empty()
? 0
: model->GetDeviceIndex(i->second.selected_device));
layout->AddView(combobox);
bubble_content_empty = false;
}
}
for (std::vector<ContentSettingBubbleModel::DomainList>::const_iterator i(
bubble_content.domain_lists.begin());
i != bubble_content.domain_lists.end(); ++i) {
layout->StartRow(0, kSingleColumnSetId);
views::Label* section_title = new views::Label(base::UTF8ToUTF16(i->title));
section_title->SetMultiLine(true);
section_title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->AddView(section_title, 1, 1, GridLayout::FILL, GridLayout::LEADING);
for (std::set<std::string>::const_iterator j = i->hosts.begin();
j != i->hosts.end(); ++j) {
layout->StartRow(0, indented_kSingleColumnSetId);
// TODO(tapted): Verify this when we have a mock. http://crbug.com/700196.
layout->AddView(new views::Label(
base::UTF8ToUTF16(*j), CONTEXT_BODY_TEXT_LARGE, STYLE_EMPHASIZED));
}
bubble_content_empty = false;
}
if (!bubble_content.custom_link.empty()) {
custom_link_ =
new views::Link(base::UTF8ToUTF16(bubble_content.custom_link));
custom_link_->SetEnabled(bubble_content.custom_link_enabled);
custom_link_->set_listener(this);
if (!bubble_content_empty)
layout->AddPaddingRow(0, related_control_vertical_spacing);
layout->StartRow(0, kSingleColumnSetId);
layout->AddView(custom_link_);
bubble_content_empty = false;
}
if (!bubble_content_empty) {
if (!layout_delegate->IsHarmonyMode()) {
layout->AddPaddingRow(0, related_control_vertical_spacing);
layout->StartRow(0, kSingleColumnSetId);
layout->AddView(new views::Separator(), 1, 1, GridLayout::FILL,
GridLayout::FILL);
}
layout->AddPaddingRow(0, related_control_vertical_spacing);
}
}
views::View* ContentSettingBubbleContents::CreateExtraView() {
if (content_setting_bubble_model_->bubble_content()
.show_manage_text_as_button) {
manage_button_ = views::MdTextButton::CreateSecondaryUiButton(
this, base::UTF8ToUTF16(
content_setting_bubble_model_->bubble_content().manage_text));
return manage_button_;
} else {
manage_link_ = new views::Link(base::UTF8ToUTF16(
content_setting_bubble_model_->bubble_content().manage_text));
manage_link_->set_listener(this);
return manage_link_;
}
}
bool ContentSettingBubbleContents::Accept() {
content_setting_bubble_model_->OnDoneClicked();
return true;
}
bool ContentSettingBubbleContents::Close() {
return true;
}
int ContentSettingBubbleContents::GetDialogButtons() const {
return ui::DIALOG_BUTTON_OK;
}
base::string16 ContentSettingBubbleContents::GetDialogButtonLabel(
ui::DialogButton button) const {
return l10n_util::GetStringUTF16(IDS_DONE);
}
void ContentSettingBubbleContents::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInMainFrame() || !navigation_handle->HasCommitted())
return;
// Content settings are based on the main frame, so if it switches then
// close up shop.
GetWidget()->Close();
}
void ContentSettingBubbleContents::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (manage_button_ == sender) {
GetWidget()->Close();
content_setting_bubble_model_->OnManageLinkClicked();
} else {
RadioGroup::const_iterator i(
std::find(radio_group_.begin(), radio_group_.end(), sender));
DCHECK(i != radio_group_.end());
content_setting_bubble_model_->OnRadioClicked(i - radio_group_.begin());
}
}
void ContentSettingBubbleContents::LinkClicked(views::Link* source,
int event_flags) {
if (source == learn_more_link_) {
content_setting_bubble_model_->OnLearnMoreLinkClicked();
GetWidget()->Close();
return;
}
if (source == custom_link_) {
content_setting_bubble_model_->OnCustomLinkClicked();
GetWidget()->Close();
return;
}
if (source == manage_link_) {
GetWidget()->Close();
content_setting_bubble_model_->OnManageLinkClicked();
// CAREFUL: Showing the settings window activates it, which deactivates the
// info bubble, which causes it to close, which deletes us.
return;
}
ListItemLinks::const_iterator i(list_item_links_.find(source));
DCHECK(i != list_item_links_.end());
content_setting_bubble_model_->OnListItemClicked(i->second);
}
void ContentSettingBubbleContents::OnPerformAction(views::Combobox* combobox) {
MediaComboboxModel* model =
static_cast<MediaComboboxModel*>(combobox->model());
content_setting_bubble_model_->OnMediaMenuClicked(
model->type(), model->GetDevices()[combobox->selected_index()].id);
}