blob: ab1db38c7ff0b9180816ddd3a81fdf08502be51e [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/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/content_settings/host_content_settings_map.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/content_settings/content_setting_media_menu_model.h"
#include "chrome/browser/ui/views/browser_dialogs.h"
#include "content/public/browser/plugin_service.h"
#include "content/public/browser/web_contents.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.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.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/button/radio_button.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.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"
#if defined(USE_AURA)
#include "ui/base/cursor/cursor.h"
#endif
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;
// 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;
// The minimum width of the media menu buttons.
const int kMinMediaMenuButtonWidth = 150;
} // namespace
using content::PluginService;
using content::WebContents;
// ContentSettingBubbleContents::Favicon --------------------------------------
class ContentSettingBubbleContents::Favicon : public views::ImageView {
public:
Favicon(const gfx::Image& image,
ContentSettingBubbleContents* parent,
views::Link* link);
virtual ~Favicon();
private:
// views::View overrides:
virtual bool OnMousePressed(const ui::MouseEvent& event) OVERRIDE;
virtual void OnMouseReleased(const ui::MouseEvent& event) OVERRIDE;
virtual 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) {
#if defined(USE_AURA)
return ui::kCursorHand;
#elif defined(OS_WIN)
static HCURSOR g_hand_cursor = LoadCursor(NULL, IDC_HAND);
return g_hand_cursor;
#endif
}
// ContentSettingBubbleContents::MediaMenuParts -------------------------------
struct ContentSettingBubbleContents::MediaMenuParts {
explicit MediaMenuParts(content::MediaStreamType type);
~MediaMenuParts();
content::MediaStreamType type;
scoped_ptr<ui::SimpleMenuModel> menu_model;
private:
DISALLOW_COPY_AND_ASSIGN(MediaMenuParts);
};
ContentSettingBubbleContents::MediaMenuParts::MediaMenuParts(
content::MediaStreamType type)
: type(type) {}
ContentSettingBubbleContents::MediaMenuParts::~MediaMenuParts() {}
// ContentSettingBubbleContents -----------------------------------------------
ContentSettingBubbleContents::ContentSettingBubbleContents(
ContentSettingBubbleModel* content_setting_bubble_model,
content::WebContents* web_contents,
views::View* anchor_view,
views::BubbleBorder::Arrow arrow)
: content::WebContentsObserver(web_contents),
BubbleDelegateView(anchor_view, arrow),
content_setting_bubble_model_(content_setting_bubble_model),
custom_link_(NULL),
manage_link_(NULL),
learn_more_link_(NULL),
close_button_(NULL) {
// Compensate for built-in vertical padding in the anchor view's image.
set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
}
ContentSettingBubbleContents::~ContentSettingBubbleContents() {
STLDeleteValues(&media_menus_);
}
gfx::Size ContentSettingBubbleContents::GetPreferredSize() const {
gfx::Size preferred_size(views::View::GetPreferredSize());
int preferred_width =
(!content_setting_bubble_model_->bubble_content().domain_lists.empty() &&
(kMinMultiLineContentsWidth > preferred_size.width())) ?
kMinMultiLineContentsWidth : preferred_size.width();
preferred_size.set_width(std::min(preferred_width, kMaxContentsWidth));
return preferred_size;
}
void ContentSettingBubbleContents::UpdateMenuLabel(
content::MediaStreamType type,
const std::string& label) {
for (MediaMenuPartsMap::const_iterator it = media_menus_.begin();
it != media_menus_.end(); ++it) {
if (it->second->type == type) {
it->first->SetText(base::UTF8ToUTF16(label));
return;
}
}
NOTREACHED();
}
void ContentSettingBubbleContents::Init() {
using views::GridLayout;
GridLayout* layout = new views::GridLayout(this);
SetLayoutManager(layout);
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, views::kRelatedControlHorizontalSpacing);
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()) {
views::Label* title_label = new views::Label(base::UTF8ToUTF16(
bubble_content.title));
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.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;
}
if (content_setting_bubble_model_->content_type() ==
CONTENT_SETTINGS_TYPE_POPUPS) {
const int kPopupColumnSetId = 2;
views::ColumnSet* popup_column_set =
layout->AddColumnSet(kPopupColumnSetId);
popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
GridLayout::USE_PREF, 0, 0);
popup_column_set->AddPaddingColumn(
0, views::kRelatedControlHorizontalSpacing);
popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1,
GridLayout::USE_PREF, 0, 0);
for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator
i(bubble_content.popup_items.begin());
i != bubble_content.popup_items.end(); ++i) {
if (!bubble_content_empty)
layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
layout->StartRow(0, kPopupColumnSetId);
views::Link* link = new views::Link(base::UTF8ToUTF16(i->title));
link->set_listener(this);
link->SetElideBehavior(gfx::ELIDE_MIDDLE);
popup_links_[link] = i - bubble_content.popup_items.begin();
layout->AddView(new Favicon(i->image, this, link));
layout->AddView(link);
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, views::kCheckboxIndent);
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, views::kRelatedControlVerticalSpacing);
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);
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_->content_type() ==
CONTENT_SETTINGS_TYPE_MEDIASTREAM) {
const int kMediaMenuColumnSetId = 2;
views::ColumnSet* menu_column_set =
layout->AddColumnSet(kMediaMenuColumnSetId);
menu_column_set->AddPaddingColumn(0, views::kCheckboxIndent);
menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0,
GridLayout::USE_PREF, 0, 0);
menu_column_set->AddPaddingColumn(
0, views::kRelatedControlHorizontalSpacing);
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, views::kRelatedControlVerticalSpacing);
layout->StartRow(0, kMediaMenuColumnSetId);
views::Label* label =
new views::Label(base::UTF8ToUTF16(i->second.label));
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
views::MenuButton* menu_button = new views::MenuButton(
NULL, base::UTF8ToUTF16((i->second.selected_device.name)),
this, true);
menu_button->SetStyle(views::Button::STYLE_BUTTON);
menu_button->SetHorizontalAlignment(gfx::ALIGN_LEFT);
menu_button->set_animate_on_state_change(false);
MediaMenuParts* menu_view = new MediaMenuParts(i->first);
menu_view->menu_model.reset(new ContentSettingMediaMenuModel(
i->first,
content_setting_bubble_model_.get(),
base::Bind(&ContentSettingBubbleContents::UpdateMenuLabel,
base::Unretained(this))));
media_menus_[menu_button] = menu_view;
if (!menu_view->menu_model->GetItemCount()) {
// Show a "None available" title and grey out the menu when there are
// no available devices.
menu_button->SetText(
l10n_util::GetStringUTF16(IDS_MEDIA_MENU_NO_DEVICE_TITLE));
menu_button->SetEnabled(false);
}
// Disable the device selection when the website is managing the devices
// itself.
if (i->second.disabled)
menu_button->SetEnabled(false);
layout->AddView(label);
layout->AddView(menu_button);
bubble_content_empty = false;
}
}
UpdateMenuButtonSizes(GetNativeTheme());
const gfx::FontList& domain_font =
ui::ResourceBundle::GetSharedInstance().GetFontList(
ui::ResourceBundle::BoldFont);
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);
layout->AddView(new views::Label(base::UTF8ToUTF16(*j), domain_font));
}
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, views::kRelatedControlVerticalSpacing);
layout->StartRow(0, kSingleColumnSetId);
layout->AddView(custom_link_);
bubble_content_empty = false;
}
const int kDoubleColumnSetId = 1;
views::ColumnSet* double_column_set =
layout->AddColumnSet(kDoubleColumnSetId);
if (!bubble_content_empty) {
layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
layout->StartRow(0, kSingleColumnSetId);
layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 1, 1,
GridLayout::FILL, GridLayout::FILL);
layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
}
double_column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 1,
GridLayout::USE_PREF, 0, 0);
double_column_set->AddPaddingColumn(
0, views::kUnrelatedControlHorizontalSpacing);
double_column_set->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 0,
GridLayout::USE_PREF, 0, 0);
layout->StartRow(0, kDoubleColumnSetId);
manage_link_ =
new views::Link(base::UTF8ToUTF16(bubble_content.manage_link));
manage_link_->set_listener(this);
layout->AddView(manage_link_);
close_button_ =
new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_DONE));
close_button_->SetStyle(views::Button::STYLE_BUTTON);
layout->AddView(close_button_);
}
void ContentSettingBubbleContents::DidNavigateMainFrame(
const content::LoadCommittedDetails& details,
const content::FrameNavigateParams& params) {
// Content settings are based on the main frame, so if it switches then
// close up shop.
content_setting_bubble_model_->OnDoneClicked();
GetWidget()->Close();
}
void ContentSettingBubbleContents::OnNativeThemeChanged(
const ui::NativeTheme* theme) {
views::BubbleDelegateView::OnNativeThemeChanged(theme);
UpdateMenuButtonSizes(theme);
}
void ContentSettingBubbleContents::ButtonPressed(views::Button* sender,
const ui::Event& event) {
RadioGroup::const_iterator i(
std::find(radio_group_.begin(), radio_group_.end(), sender));
if (i != radio_group_.end()) {
content_setting_bubble_model_->OnRadioClicked(i - radio_group_.begin());
return;
}
DCHECK_EQ(sender, close_button_);
content_setting_bubble_model_->OnDoneClicked();
GetWidget()->Close();
}
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;
}
PopupLinks::const_iterator i(popup_links_.find(source));
DCHECK(i != popup_links_.end());
content_setting_bubble_model_->OnPopupClicked(i->second);
}
void ContentSettingBubbleContents::OnMenuButtonClicked(
views::View* source,
const gfx::Point& point) {
MediaMenuPartsMap::iterator j(media_menus_.find(
static_cast<views::MenuButton*>(source)));
DCHECK(j != media_menus_.end());
menu_runner_.reset(new views::MenuRunner(j->second->menu_model.get(),
views::MenuRunner::HAS_MNEMONICS));
gfx::Point screen_location;
views::View::ConvertPointToScreen(j->first, &screen_location);
ignore_result(
menu_runner_->RunMenuAt(source->GetWidget(),
j->first,
gfx::Rect(screen_location, j->first->size()),
views::MENU_ANCHOR_TOPLEFT,
ui::MENU_SOURCE_NONE));
}
void ContentSettingBubbleContents::UpdateMenuButtonSizes(
const ui::NativeTheme* theme) {
const views::MenuConfig config = views::MenuConfig(theme);
const int margins = config.item_left_margin + config.check_width +
config.label_to_arrow_padding + config.arrow_width +
config.arrow_to_edge_padding;
// The preferred media menu size sort of copies the logic in
// MenuItemView::CalculateDimensions(). When this was using TextButton, it
// completely coincidentally matched the logic in MenuItemView. We now need
// to redo this manually.
int menu_width = 0;
for (MediaMenuPartsMap::const_iterator i = media_menus_.begin();
i != media_menus_.end(); ++i) {
for (int j = 0; j < i->second->menu_model->GetItemCount(); ++j) {
int string_width = gfx::GetStringWidth(
i->second->menu_model->GetLabelAt(j),
config.font_list);
menu_width = std::max(menu_width, string_width);
}
}
// Make sure the width is at least kMinMediaMenuButtonWidth. The
// maximum width will be clamped by kMaxContentsWidth of the view.
menu_width = std::max(kMinMediaMenuButtonWidth, menu_width + margins);
for (MediaMenuPartsMap::const_iterator i = media_menus_.begin();
i != media_menus_.end(); ++i) {
i->first->SetMinSize(gfx::Size(menu_width, 0));
i->first->SetMaxSize(gfx::Size(menu_width, 0));
}
}