blob: 064bb434d357c0addfc4ace565fb95cc5e9331c4 [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 <vector>
#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/browser_dialogs.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/content_setting_domain_list_view.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 "components/vector_icons/vector_icons.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/l10n/l10n_util.h"
#include "ui/base/models/combobox_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/button/md_text_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/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/grid_layout.h"
#include "ui/views/native_cursor.h"
#include "ui/views/window/dialog_client_view.h"
namespace {
// Display a maximum of 4 visible items in a list before scrolling.
const int kMaxVisibleListItems = 4;
enum class LayoutRowType {
DEFAULT,
INDENTED,
FULL_WIDTH,
};
// Represents a row containing a single view in the layout. The type determines
// the view's horizontal margins.
struct LayoutRow {
std::unique_ptr<views::View> view;
LayoutRowType type;
};
// A combobox model that builds the contents of the media capture devices menu
// in the content setting bubble.
class MediaComboboxModel : public ui::ComboboxModel {
public:
explicit MediaComboboxModel(content::MediaStreamType type);
~MediaComboboxModel() override;
content::MediaStreamType type() const { return type_; }
const content::MediaStreamDevices& GetDevices() const;
int GetDeviceIndex(const content::MediaStreamDevice& device) const;
// ui::ComboboxModel:
int GetItemCount() const override;
base::string16 GetItemAt(int index) override;
private:
content::MediaStreamType type_;
DISALLOW_COPY_AND_ASSIGN(MediaComboboxModel);
};
// A view representing one or more rows, each containing a label and combobox
// pair, that allow the user to select a device for each media type (microphone
// and/or camera).
class MediaMenuBlock : public views::View {
public:
MediaMenuBlock(views::ComboboxListener* listener,
ContentSettingBubbleModel::MediaMenuMap media) {
const ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
views::GridLayout* layout =
SetLayoutManager(std::make_unique<views::GridLayout>(this));
constexpr int kColumnSetId = 0;
views::ColumnSet* column_set = layout->AddColumnSet(kColumnSetId);
column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER,
views::GridLayout::kFixedSize,
views::GridLayout::USE_PREF, 0, 0);
column_set->AddPaddingColumn(
views::GridLayout::kFixedSize,
provider->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL));
column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL, 1.0,
views::GridLayout::FIXED, 0, 0);
bool first_row = true;
for (auto i = media.cbegin(); i != media.cend(); ++i) {
if (!first_row) {
layout->AddPaddingRow(views::GridLayout::kFixedSize,
provider->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_VERTICAL));
}
first_row = false;
layout->StartRow(views::GridLayout::kFixedSize, kColumnSetId);
content::MediaStreamType stream_type = i->first;
const ContentSettingBubbleModel::MediaMenu& menu = i->second;
views::Label* label = new views::Label(menu.label);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
layout->AddView(label);
auto combobox_model = std::make_unique<MediaComboboxModel>(stream_type);
// Disable the device selection when the website is managing the devices
// itself or if there are no devices present.
const bool combobox_enabled =
!menu.disabled && !combobox_model->GetDevices().empty();
const int combobox_selected_index =
combobox_model->GetDevices().empty()
? 0
: combobox_model->GetDeviceIndex(menu.selected_device);
// The combobox takes ownership of the model.
views::Combobox* combobox =
new views::Combobox(std::move(combobox_model));
combobox->SetEnabled(combobox_enabled);
combobox->set_listener(listener);
combobox->SetSelectedIndex(combobox_selected_index);
layout->AddView(combobox);
}
}
private:
DISALLOW_COPY_AND_ASSIGN(MediaMenuBlock);
};
} // namespace
// MediaComboboxModel ----------------------------------------------------------
MediaComboboxModel::MediaComboboxModel(content::MediaStreamType type)
: type_(type) {
DCHECK(type_ == content::MEDIA_DEVICE_AUDIO_CAPTURE ||
type_ == content::MEDIA_DEVICE_VIDEO_CAPTURE);
}
MediaComboboxModel::~MediaComboboxModel() {}
const content::MediaStreamDevices& MediaComboboxModel::GetDevices() const {
MediaCaptureDevicesDispatcher* dispatcher =
MediaCaptureDevicesDispatcher::GetInstance();
return type_ == content::MEDIA_DEVICE_AUDIO_CAPTURE
? dispatcher->GetAudioCaptureDevices()
: dispatcher->GetVideoCaptureDevices();
}
int 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 MediaComboboxModel::GetItemCount() const {
return std::max(1, static_cast<int>(GetDevices().size()));
}
base::string16 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::ListItemContainer -----------------------------
class ContentSettingBubbleContents::ListItemContainer : public views::View {
public:
explicit ListItemContainer(ContentSettingBubbleContents* parent);
// Creates and adds child views representing |item|.
void AddItem(const ContentSettingBubbleModel::ListItem& item);
// Calling this will delete related children.
void RemoveRowAtIndex(int index);
// Returns row index of |link| among list items.
int GetRowIndexOf(const views::Link* link) const;
private:
using Row = std::pair<views::ImageView*, views::Label*>;
void ResetLayout();
void AddRowToLayout(const Row& row);
ContentSettingBubbleContents* parent_;
// Our controls representing list items, so we can add or remove
// these dynamically. Each pair represetns one list item.
std::vector<Row> list_item_views_;
DISALLOW_COPY_AND_ASSIGN(ListItemContainer);
};
ContentSettingBubbleContents::ListItemContainer::ListItemContainer(
ContentSettingBubbleContents* parent)
: parent_(parent) {
ResetLayout();
}
void ContentSettingBubbleContents::ListItemContainer::AddItem(
const ContentSettingBubbleModel::ListItem& item) {
views::ImageView* icon = nullptr;
views::Label* label = nullptr;
if (item.has_link) {
views::Link* link = new views::Link(item.title);
link->set_listener(parent_);
link->SetElideBehavior(gfx::ELIDE_MIDDLE);
icon = new Favicon(item.image, parent_, link);
label = link;
} else {
icon = new views::ImageView();
icon->SetImage(item.image.AsImageSkia());
label = new views::Label(item.title);
}
label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
list_item_views_.push_back(Row(icon, label));
AddRowToLayout(list_item_views_.back());
}
void ContentSettingBubbleContents::ListItemContainer::RemoveRowAtIndex(
int index) {
auto& children = list_item_views_[index];
delete children.first;
delete children.second;
list_item_views_.erase(list_item_views_.begin() + index);
// As GridLayout can't remove rows, we have to rebuild it entirely.
ResetLayout();
for (size_t i = 0; i < list_item_views_.size(); i++)
AddRowToLayout(list_item_views_[i]);
}
int ContentSettingBubbleContents::ListItemContainer::GetRowIndexOf(
const views::Link* link) const {
auto has_link = [link](const Row& row) { return row.second == link; };
auto iter = std::find_if(list_item_views_.begin(), list_item_views_.end(),
has_link);
return (iter == list_item_views_.end())
? -1
: std::distance(list_item_views_.begin(), iter);
}
void ContentSettingBubbleContents::ListItemContainer::ResetLayout() {
views::GridLayout* layout =
SetLayoutManager(std::make_unique<views::GridLayout>(this));
views::ColumnSet* item_list_column_set = layout->AddColumnSet(0);
item_list_column_set->AddColumn(
views::GridLayout::LEADING, views::GridLayout::FILL,
views::GridLayout::kFixedSize, views::GridLayout::USE_PREF, 0, 0);
const int related_control_horizontal_spacing =
ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
item_list_column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
related_control_horizontal_spacing);
item_list_column_set->AddColumn(views::GridLayout::LEADING,
views::GridLayout::FILL, 1.0,
views::GridLayout::USE_PREF, 0, 0);
auto* scroll_view = views::ScrollView::GetScrollViewForContents(this);
// When this function is called from the constructor, the view has not yet
// been placed into a ScrollView.
if (scroll_view)
scroll_view->ClipHeightTo(-1, -1);
}
void ContentSettingBubbleContents::ListItemContainer::AddRowToLayout(
const Row& row) {
views::GridLayout* layout =
static_cast<views::GridLayout*>(GetLayoutManager());
DCHECK(layout);
layout->StartRow(views::GridLayout::kFixedSize, 0);
layout->AddView(row.first);
layout->AddView(row.second);
auto* scroll_view = views::ScrollView::GetScrollViewForContents(this);
DCHECK(scroll_view);
if (!scroll_view->is_bounded()) {
scroll_view->ClipHeightTo(
0, std::max(row.first->GetPreferredSize().height(),
row.second->GetPreferredSize().height()) *
kMaxVisibleListItems);
}
}
// 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),
list_item_container_(nullptr),
custom_link_(nullptr),
manage_button_(nullptr),
manage_checkbox_(nullptr),
learn_more_button_(nullptr) {
chrome::RecordDialogCreation(
chrome::DialogIdentifier::CONTENT_SETTING_CONTENTS);
}
ContentSettingBubbleContents::~ContentSettingBubbleContents() {
// Must remove the children here so the comboboxes get destroyed before
// their associated models.
RemoveAllChildViews(true);
}
void ContentSettingBubbleContents::WindowClosing() {
content_setting_bubble_model_->CommitChanges();
}
gfx::Size ContentSettingBubbleContents::CalculatePreferredSize() const {
const int width = ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_BUBBLE_PREFERRED_WIDTH) -
margins().width();
return gfx::Size(width, GetHeightForWidth(width));
}
void ContentSettingBubbleContents::OnListItemAdded(
const ContentSettingBubbleModel::ListItem& item) {
DCHECK(list_item_container_);
list_item_container_->AddItem(item);
SizeToContents();
}
void ContentSettingBubbleContents::OnListItemRemovedAt(int index) {
DCHECK(list_item_container_);
list_item_container_->RemoveRowAtIndex(index);
SizeToContents();
}
int ContentSettingBubbleContents::GetSelectedRadioOption() {
for (RadioGroup::const_iterator i(radio_group_.begin());
i != radio_group_.end(); ++i) {
if ((*i)->checked())
return i - radio_group_.begin();
}
NOTREACHED();
return 0;
}
void ContentSettingBubbleContents::OnNativeThemeChanged(
const ui::NativeTheme* theme) {
views::BubbleDialogDelegateView::OnNativeThemeChanged(theme);
if (learn_more_button_)
StyleLearnMoreButton(theme);
}
base::string16 ContentSettingBubbleContents::GetWindowTitle() const {
return content_setting_bubble_model_->bubble_content().title;
}
bool ContentSettingBubbleContents::ShouldShowCloseButton() const {
return true;
}
void ContentSettingBubbleContents::Init() {
const ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));
std::vector<LayoutRow> rows;
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model_->bubble_content();
if (!bubble_content.message.empty()) {
auto message_label = std::make_unique<views::Label>(
bubble_content.message, views::style::CONTEXT_LABEL, STYLE_SECONDARY);
message_label->SetMultiLine(true);
message_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
rows.push_back({std::move(message_label), LayoutRowType::DEFAULT});
}
// Layout for the item list (blocked plugins and popups).
if (!bubble_content.list_items.empty()) {
list_item_container_ = new ListItemContainer(this);
list_item_container_->SetBorder(
views::CreateEmptyBorder(0, margins().left(), 0, margins().right()));
auto scroll_view = std::make_unique<views::ScrollView>();
scroll_view->SetContents(list_item_container_);
rows.push_back({std::move(scroll_view), LayoutRowType::FULL_WIDTH});
for (const ContentSettingBubbleModel::ListItem& list_item :
bubble_content.list_items) {
list_item_container_->AddItem(list_item);
}
}
const ContentSettingBubbleModel::RadioGroup& radio_group =
bubble_content.radio_group;
if (!radio_group.radio_items.empty()) {
for (auto i(radio_group.radio_items.begin());
i != radio_group.radio_items.end(); ++i) {
auto radio = std::make_unique<views::RadioButton>(*i, 0);
radio->SetEnabled(bubble_content.radio_group_enabled);
radio->SetMultiLine(true);
radio_group_.push_back(radio.get());
rows.push_back({std::move(radio), LayoutRowType::INDENTED});
}
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()) {
rows.push_back(
{std::make_unique<MediaMenuBlock>(this, bubble_content.media_menus),
LayoutRowType::INDENTED});
}
for (auto i(bubble_content.domain_lists.begin());
i != bubble_content.domain_lists.end(); ++i) {
auto list_view =
std::make_unique<ContentSettingDomainListView>(i->title, i->hosts);
rows.push_back({std::move(list_view), LayoutRowType::DEFAULT});
}
if (!bubble_content.custom_link.empty()) {
auto custom_link =
std::make_unique<views::Link>(bubble_content.custom_link);
custom_link->SetEnabled(bubble_content.custom_link_enabled);
custom_link->SetMultiLine(true);
custom_link->set_listener(this);
custom_link->SetHorizontalAlignment(gfx::ALIGN_LEFT);
custom_link_ = custom_link.get();
rows.push_back({std::move(custom_link), LayoutRowType::DEFAULT});
}
if (bubble_content.manage_text_style ==
ContentSettingBubbleModel::ManageTextStyle::kCheckbox) {
auto manage_checkbox =
std::make_unique<views::Checkbox>(bubble_content.manage_text, this);
manage_checkbox_ = manage_checkbox.get();
rows.push_back({std::move(manage_checkbox), LayoutRowType::DEFAULT});
}
// We have to apply the left and right margins manually, because rows using
// LayoutRowType::FULL_WIDTH need to not have them applied to look correct.
const int left_margin = margins().left();
const int right_margin = margins().right();
set_margins(gfx::Insets(margins().top(), 0, margins().bottom(), 0));
for (LayoutRow& row : rows) {
if (row.type != LayoutRowType::FULL_WIDTH) {
const int row_left_margin =
left_margin + (row.type == LayoutRowType::INDENTED
? provider->GetDistanceMetric(
DISTANCE_SUBSECTION_HORIZONTAL_INDENT)
: 0);
row.view->SetBorder(
views::CreateEmptyBorder(0, row_left_margin, 0, right_margin));
}
AddChildView(row.view.release());
}
content_setting_bubble_model_->set_owner(this);
}
views::View* ContentSettingBubbleContents::CreateExtraView() {
const auto& bubble_content = content_setting_bubble_model_->bubble_content();
const auto* layout = ChromeLayoutProvider::Get();
std::vector<View*> extra_views;
// Optionally add a help icon if the view wants to link to a help page.
if (bubble_content.show_learn_more) {
learn_more_button_ = views::CreateVectorImageButton(this);
learn_more_button_->SetFocusForPlatform();
learn_more_button_->SetTooltipText(
l10n_util::GetStringUTF16(IDS_LEARN_MORE));
StyleLearnMoreButton(GetNativeTheme());
extra_views.push_back(learn_more_button_);
}
// Optionally add a "Manage" button if the view wants to use a button to
// invoke a separate management UI related to the dialog content.
if (bubble_content.manage_text_style ==
ContentSettingBubbleModel::ManageTextStyle::kButton) {
base::string16 title = bubble_content.manage_text;
if (title.empty())
title = l10n_util::GetStringUTF16(IDS_MANAGE);
manage_button_ = views::MdTextButton::CreateSecondaryUiButton(this, title);
manage_button_->SetMinSize(gfx::Size(
layout->GetDistanceMetric(views::DISTANCE_DIALOG_BUTTON_MINIMUM_WIDTH),
0));
extra_views.push_back(manage_button_);
}
if (extra_views.empty())
return nullptr;
if (extra_views.size() == 1)
return extra_views.front();
views::View* container = new views::View();
container->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal, gfx::Insets(),
layout->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_HORIZONTAL)));
for (auto* extra_view : extra_views)
container->AddChildView(extra_view);
return container;
}
bool ContentSettingBubbleContents::Accept() {
return true;
}
bool ContentSettingBubbleContents::Close() {
return true;
}
int ContentSettingBubbleContents::GetDialogButtons() const {
return ui::DIALOG_BUTTON_OK;
}
base::string16 ContentSettingBubbleContents::GetDialogButtonLabel(
ui::DialogButton button) const {
const base::string16& done_text =
content_setting_bubble_model_->bubble_content().done_button_text;
return done_text.empty() ? l10n_util::GetStringUTF16(IDS_DONE) : done_text;
}
void ContentSettingBubbleContents::StyleLearnMoreButton(
const ui::NativeTheme* theme) {
DCHECK(learn_more_button_);
SkColor text_color =
theme->GetSystemColor(ui::NativeTheme::kColorId_LabelEnabledColor);
views::SetImageFromVectorIcon(learn_more_button_,
vector_icons::kHelpOutlineIcon, text_color);
}
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::OnVisibilityChanged(
content::Visibility visibility) {
if (visibility == content::Visibility::HIDDEN)
GetWidget()->Close();
}
void ContentSettingBubbleContents::WebContentsDestroyed() {
GetWidget()->Close();
}
void ContentSettingBubbleContents::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (sender == manage_checkbox_) {
content_setting_bubble_model_->OnManageCheckboxChecked(
manage_checkbox_->checked());
// Toggling the check state may change the dialog button text.
DialogModelChanged();
GetDialogClientView()->Layout();
} else if (sender == learn_more_button_) {
GetWidget()->Close();
content_setting_bubble_model_->OnLearnMoreClicked();
} else if (sender == manage_button_) {
GetWidget()->Close();
content_setting_bubble_model_->OnManageButtonClicked();
} else {
NOTREACHED();
}
}
void ContentSettingBubbleContents::LinkClicked(views::Link* source,
int event_flags) {
if (source == custom_link_) {
content_setting_bubble_model_->OnCustomLinkClicked();
GetWidget()->Close();
return;
}
int row = list_item_container_->GetRowIndexOf(source);
DCHECK_NE(row, -1);
content_setting_bubble_model_->OnListItemClicked(row, event_flags);
}
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);
}