content_setting_bubble_contents.cc revision 116680a4aac90f2aa7413d9095a592090648e557
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/plugin_service.h" 22#include "content/public/browser/web_contents.h" 23#include "grit/generated_resources.h" 24#include "grit/theme_resources.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 43#if defined(USE_AURA) 44#include "ui/base/cursor/cursor.h" 45#endif 46 47namespace { 48 49// If we don't clamp the maximum width, then very long URLs and titles can make 50// the bubble arbitrarily wide. 51const int kMaxContentsWidth = 500; 52 53// When we have multiline labels, we should set a minimum width lest we get very 54// narrow bubbles with lots of line-wrapping. 55const int kMinMultiLineContentsWidth = 250; 56 57// The minimum width of the media menu buttons. 58const int kMinMediaMenuButtonWidth = 100; 59 60} // namespace 61 62using content::PluginService; 63using content::WebContents; 64 65 66// ContentSettingBubbleContents::Favicon -------------------------------------- 67 68class ContentSettingBubbleContents::Favicon : public views::ImageView { 69 public: 70 Favicon(const gfx::Image& image, 71 ContentSettingBubbleContents* parent, 72 views::Link* link); 73 virtual ~Favicon(); 74 75 private: 76 // views::View overrides: 77 virtual bool OnMousePressed(const ui::MouseEvent& event) OVERRIDE; 78 virtual void OnMouseReleased(const ui::MouseEvent& event) OVERRIDE; 79 virtual gfx::NativeCursor GetCursor(const ui::MouseEvent& event) OVERRIDE; 80 81 ContentSettingBubbleContents* parent_; 82 views::Link* link_; 83}; 84 85ContentSettingBubbleContents::Favicon::Favicon( 86 const gfx::Image& image, 87 ContentSettingBubbleContents* parent, 88 views::Link* link) 89 : parent_(parent), 90 link_(link) { 91 SetImage(image.AsImageSkia()); 92} 93 94ContentSettingBubbleContents::Favicon::~Favicon() { 95} 96 97bool ContentSettingBubbleContents::Favicon::OnMousePressed( 98 const ui::MouseEvent& event) { 99 return event.IsLeftMouseButton() || event.IsMiddleMouseButton(); 100} 101 102void ContentSettingBubbleContents::Favicon::OnMouseReleased( 103 const ui::MouseEvent& event) { 104 if ((event.IsLeftMouseButton() || event.IsMiddleMouseButton()) && 105 HitTestPoint(event.location())) { 106 parent_->LinkClicked(link_, event.flags()); 107 } 108} 109 110gfx::NativeCursor ContentSettingBubbleContents::Favicon::GetCursor( 111 const ui::MouseEvent& event) { 112#if defined(USE_AURA) 113 return ui::kCursorHand; 114#elif defined(OS_WIN) 115 static HCURSOR g_hand_cursor = LoadCursor(NULL, IDC_HAND); 116 return g_hand_cursor; 117#endif 118} 119 120 121// ContentSettingBubbleContents::MediaMenuParts ------------------------------- 122 123struct ContentSettingBubbleContents::MediaMenuParts { 124 explicit MediaMenuParts(content::MediaStreamType type); 125 ~MediaMenuParts(); 126 127 content::MediaStreamType type; 128 scoped_ptr<ui::SimpleMenuModel> menu_model; 129 130 private: 131 DISALLOW_COPY_AND_ASSIGN(MediaMenuParts); 132}; 133 134ContentSettingBubbleContents::MediaMenuParts::MediaMenuParts( 135 content::MediaStreamType type) 136 : type(type) {} 137 138ContentSettingBubbleContents::MediaMenuParts::~MediaMenuParts() {} 139 140// ContentSettingBubbleContents ----------------------------------------------- 141 142ContentSettingBubbleContents::ContentSettingBubbleContents( 143 ContentSettingBubbleModel* content_setting_bubble_model, 144 content::WebContents* web_contents, 145 views::View* anchor_view, 146 views::BubbleBorder::Arrow arrow) 147 : content::WebContentsObserver(web_contents), 148 BubbleDelegateView(anchor_view, arrow), 149 content_setting_bubble_model_(content_setting_bubble_model), 150 custom_link_(NULL), 151 manage_link_(NULL), 152 learn_more_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 158ContentSettingBubbleContents::~ContentSettingBubbleContents() { 159 STLDeleteValues(&media_menus_); 160} 161 162gfx::Size ContentSettingBubbleContents::GetPreferredSize() const { 163 gfx::Size preferred_size(views::View::GetPreferredSize()); 164 int preferred_width = 165 (!content_setting_bubble_model_->bubble_content().domain_lists.empty() && 166 (kMinMultiLineContentsWidth > preferred_size.width())) ? 167 kMinMultiLineContentsWidth : preferred_size.width(); 168 preferred_size.set_width(std::min(preferred_width, kMaxContentsWidth)); 169 return preferred_size; 170} 171 172void ContentSettingBubbleContents::UpdateMenuLabel( 173 content::MediaStreamType type, 174 const std::string& label) { 175 for (MediaMenuPartsMap::const_iterator it = media_menus_.begin(); 176 it != media_menus_.end(); ++it) { 177 if (it->second->type == type) { 178 it->first->SetText(base::UTF8ToUTF16(label)); 179 return; 180 } 181 } 182 NOTREACHED(); 183} 184 185void ContentSettingBubbleContents::Init() { 186 using views::GridLayout; 187 188 GridLayout* layout = new views::GridLayout(this); 189 SetLayoutManager(layout); 190 191 const int kSingleColumnSetId = 0; 192 views::ColumnSet* column_set = layout->AddColumnSet(kSingleColumnSetId); 193 column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1, 194 GridLayout::USE_PREF, 0, 0); 195 column_set->AddPaddingColumn(0, views::kRelatedControlHorizontalSpacing); 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(base::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 if (!bubble_content.learn_more_link.empty()) { 214 learn_more_link_ = 215 new views::Link(base::UTF8ToUTF16(bubble_content.learn_more_link)); 216 learn_more_link_->set_listener(this); 217 learn_more_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 218 layout->AddView(learn_more_link_); 219 bubble_content_empty = false; 220 } 221 222 if (content_setting_bubble_model_->content_type() == 223 CONTENT_SETTINGS_TYPE_POPUPS) { 224 const int kPopupColumnSetId = 2; 225 views::ColumnSet* popup_column_set = 226 layout->AddColumnSet(kPopupColumnSetId); 227 popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0, 228 GridLayout::USE_PREF, 0, 0); 229 popup_column_set->AddPaddingColumn( 230 0, views::kRelatedControlHorizontalSpacing); 231 popup_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1, 232 GridLayout::USE_PREF, 0, 0); 233 234 for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator 235 i(bubble_content.popup_items.begin()); 236 i != bubble_content.popup_items.end(); ++i) { 237 if (!bubble_content_empty) 238 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 239 layout->StartRow(0, kPopupColumnSetId); 240 241 views::Link* link = new views::Link(base::UTF8ToUTF16(i->title)); 242 link->set_listener(this); 243 link->SetElideBehavior(gfx::ELIDE_MIDDLE); 244 popup_links_[link] = i - bubble_content.popup_items.begin(); 245 layout->AddView(new Favicon(i->image, this, link)); 246 layout->AddView(link); 247 bubble_content_empty = false; 248 } 249 } 250 251 const int indented_kSingleColumnSetId = 3; 252 // Insert a column set with greater indent. 253 views::ColumnSet* indented_single_column_set = 254 layout->AddColumnSet(indented_kSingleColumnSetId); 255 indented_single_column_set->AddPaddingColumn(0, views::kCheckboxIndent); 256 indented_single_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 257 1, GridLayout::USE_PREF, 0, 0); 258 259 const ContentSettingBubbleModel::RadioGroup& radio_group = 260 bubble_content.radio_group; 261 if (!radio_group.radio_items.empty()) { 262 if (!bubble_content_empty) 263 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 264 for (ContentSettingBubbleModel::RadioItems::const_iterator i( 265 radio_group.radio_items.begin()); 266 i != radio_group.radio_items.end(); ++i) { 267 views::RadioButton* radio = 268 new views::RadioButton(base::UTF8ToUTF16(*i), 0); 269 radio->SetEnabled(bubble_content.radio_group_enabled); 270 radio->set_listener(this); 271 radio_group_.push_back(radio); 272 layout->StartRow(0, indented_kSingleColumnSetId); 273 layout->AddView(radio); 274 bubble_content_empty = false; 275 } 276 DCHECK(!radio_group_.empty()); 277 // Now that the buttons have been added to the view hierarchy, it's safe 278 // to call SetChecked() on them. 279 radio_group_[radio_group.default_item]->SetChecked(true); 280 } 281 282 // Layout code for the media device menus. 283 if (content_setting_bubble_model_->content_type() == 284 CONTENT_SETTINGS_TYPE_MEDIASTREAM) { 285 const int kMediaMenuColumnSetId = 2; 286 views::ColumnSet* menu_column_set = 287 layout->AddColumnSet(kMediaMenuColumnSetId); 288 menu_column_set->AddPaddingColumn(0, views::kCheckboxIndent); 289 menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 0, 290 GridLayout::USE_PREF, 0, 0); 291 menu_column_set->AddPaddingColumn( 292 0, views::kRelatedControlHorizontalSpacing); 293 menu_column_set->AddColumn(GridLayout::LEADING, GridLayout::FILL, 1, 294 GridLayout::USE_PREF, 0, 0); 295 296 for (ContentSettingBubbleModel::MediaMenuMap::const_iterator i( 297 bubble_content.media_menus.begin()); 298 i != bubble_content.media_menus.end(); ++i) { 299 if (!bubble_content_empty) 300 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 301 layout->StartRow(0, kMediaMenuColumnSetId); 302 303 views::Label* label = 304 new views::Label(base::UTF8ToUTF16(i->second.label)); 305 label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 306 307 views::MenuButton* menu_button = new views::MenuButton( 308 NULL, base::UTF8ToUTF16((i->second.selected_device.name)), 309 this, true); 310 menu_button->SetStyle(views::Button::STYLE_BUTTON); 311 menu_button->SetHorizontalAlignment(gfx::ALIGN_LEFT); 312 menu_button->set_animate_on_state_change(false); 313 314 MediaMenuParts* menu_view = new MediaMenuParts(i->first); 315 menu_view->menu_model.reset(new ContentSettingMediaMenuModel( 316 i->first, 317 content_setting_bubble_model_.get(), 318 base::Bind(&ContentSettingBubbleContents::UpdateMenuLabel, 319 base::Unretained(this)))); 320 media_menus_[menu_button] = menu_view; 321 322 if (!menu_view->menu_model->GetItemCount()) { 323 // Show a "None available" title and grey out the menu when there are 324 // no available devices. 325 menu_button->SetText( 326 l10n_util::GetStringUTF16(IDS_MEDIA_MENU_NO_DEVICE_TITLE)); 327 menu_button->SetEnabled(false); 328 } 329 330 // Disable the device selection when the website is managing the devices 331 // itself. 332 if (i->second.disabled) 333 menu_button->SetEnabled(false); 334 335 layout->AddView(label); 336 layout->AddView(menu_button); 337 338 bubble_content_empty = false; 339 } 340 } 341 342 UpdateMenuButtonSizes(GetNativeTheme()); 343 344 const gfx::FontList& domain_font = 345 ui::ResourceBundle::GetSharedInstance().GetFontList( 346 ui::ResourceBundle::BoldFont); 347 for (std::vector<ContentSettingBubbleModel::DomainList>::const_iterator i( 348 bubble_content.domain_lists.begin()); 349 i != bubble_content.domain_lists.end(); ++i) { 350 layout->StartRow(0, kSingleColumnSetId); 351 views::Label* section_title = new views::Label(base::UTF8ToUTF16(i->title)); 352 section_title->SetMultiLine(true); 353 section_title->SetHorizontalAlignment(gfx::ALIGN_LEFT); 354 layout->AddView(section_title, 1, 1, GridLayout::FILL, GridLayout::LEADING); 355 for (std::set<std::string>::const_iterator j = i->hosts.begin(); 356 j != i->hosts.end(); ++j) { 357 layout->StartRow(0, indented_kSingleColumnSetId); 358 layout->AddView(new views::Label(base::UTF8ToUTF16(*j), domain_font)); 359 } 360 bubble_content_empty = false; 361 } 362 363 if (!bubble_content.custom_link.empty()) { 364 custom_link_ = 365 new views::Link(base::UTF8ToUTF16(bubble_content.custom_link)); 366 custom_link_->SetEnabled(bubble_content.custom_link_enabled); 367 custom_link_->set_listener(this); 368 if (!bubble_content_empty) 369 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 370 layout->StartRow(0, kSingleColumnSetId); 371 layout->AddView(custom_link_); 372 bubble_content_empty = false; 373 } 374 375 const int kDoubleColumnSetId = 1; 376 views::ColumnSet* double_column_set = 377 layout->AddColumnSet(kDoubleColumnSetId); 378 if (!bubble_content_empty) { 379 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 380 layout->StartRow(0, kSingleColumnSetId); 381 layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 1, 1, 382 GridLayout::FILL, GridLayout::FILL); 383 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 384 } 385 386 double_column_set->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 1, 387 GridLayout::USE_PREF, 0, 0); 388 double_column_set->AddPaddingColumn( 389 0, views::kUnrelatedControlHorizontalSpacing); 390 double_column_set->AddColumn(GridLayout::TRAILING, GridLayout::CENTER, 0, 391 GridLayout::USE_PREF, 0, 0); 392 393 layout->StartRow(0, kDoubleColumnSetId); 394 manage_link_ = 395 new views::Link(base::UTF8ToUTF16(bubble_content.manage_link)); 396 manage_link_->set_listener(this); 397 layout->AddView(manage_link_); 398 399 close_button_ = 400 new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_DONE)); 401 close_button_->SetStyle(views::Button::STYLE_BUTTON); 402 layout->AddView(close_button_); 403} 404 405void ContentSettingBubbleContents::DidNavigateMainFrame( 406 const content::LoadCommittedDetails& details, 407 const content::FrameNavigateParams& params) { 408 // Content settings are based on the main frame, so if it switches then 409 // close up shop. 410 content_setting_bubble_model_->OnDoneClicked(); 411 GetWidget()->Close(); 412} 413 414void ContentSettingBubbleContents::OnNativeThemeChanged( 415 const ui::NativeTheme* theme) { 416 views::BubbleDelegateView::OnNativeThemeChanged(theme); 417 UpdateMenuButtonSizes(theme); 418} 419 420void ContentSettingBubbleContents::ButtonPressed(views::Button* sender, 421 const ui::Event& event) { 422 RadioGroup::const_iterator i( 423 std::find(radio_group_.begin(), radio_group_.end(), sender)); 424 if (i != radio_group_.end()) { 425 content_setting_bubble_model_->OnRadioClicked(i - radio_group_.begin()); 426 return; 427 } 428 DCHECK_EQ(sender, close_button_); 429 content_setting_bubble_model_->OnDoneClicked(); 430 GetWidget()->Close(); 431} 432 433void ContentSettingBubbleContents::LinkClicked(views::Link* source, 434 int event_flags) { 435 if (source == learn_more_link_) { 436 content_setting_bubble_model_->OnLearnMoreLinkClicked(); 437 GetWidget()->Close(); 438 return; 439 } 440 if (source == custom_link_) { 441 content_setting_bubble_model_->OnCustomLinkClicked(); 442 GetWidget()->Close(); 443 return; 444 } 445 if (source == manage_link_) { 446 GetWidget()->Close(); 447 content_setting_bubble_model_->OnManageLinkClicked(); 448 // CAREFUL: Showing the settings window activates it, which deactivates the 449 // info bubble, which causes it to close, which deletes us. 450 return; 451 } 452 453 PopupLinks::const_iterator i(popup_links_.find(source)); 454 DCHECK(i != popup_links_.end()); 455 content_setting_bubble_model_->OnPopupClicked(i->second); 456} 457 458void ContentSettingBubbleContents::OnMenuButtonClicked( 459 views::View* source, 460 const gfx::Point& point) { 461 MediaMenuPartsMap::iterator j(media_menus_.find( 462 static_cast<views::MenuButton*>(source))); 463 DCHECK(j != media_menus_.end()); 464 menu_runner_.reset(new views::MenuRunner(j->second->menu_model.get(), 465 views::MenuRunner::HAS_MNEMONICS)); 466 467 gfx::Point screen_location; 468 views::View::ConvertPointToScreen(j->first, &screen_location); 469 ignore_result( 470 menu_runner_->RunMenuAt(source->GetWidget(), 471 j->first, 472 gfx::Rect(screen_location, j->first->size()), 473 views::MENU_ANCHOR_TOPLEFT, 474 ui::MENU_SOURCE_NONE)); 475} 476 477void ContentSettingBubbleContents::UpdateMenuButtonSizes( 478 const ui::NativeTheme* theme) { 479 const views::MenuConfig config = views::MenuConfig(theme); 480 const int margins = config.item_left_margin + config.check_width + 481 config.label_to_arrow_padding + config.arrow_width + 482 config.arrow_to_edge_padding; 483 484 // The preferred media menu size sort of copies the logic in 485 // MenuItemView::CalculateDimensions(). When this was using TextButton, it 486 // completely coincidentally matched the logic in MenuItemView. We now need 487 // to redo this manually. 488 int menu_width = 0; 489 for (MediaMenuPartsMap::const_iterator i = media_menus_.begin(); 490 i != media_menus_.end(); ++i) { 491 for (int j = 0; j < i->second->menu_model->GetItemCount(); ++j) { 492 int string_width = gfx::GetStringWidth( 493 i->second->menu_model->GetLabelAt(j), 494 config.font_list); 495 496 menu_width = std::max(menu_width, string_width); 497 } 498 } 499 500 // Make sure the width is at least kMinMediaMenuButtonWidth. The 501 // maximum width will be clamped by kMaxContentsWidth of the view. 502 menu_width = std::max(kMinMediaMenuButtonWidth, menu_width + margins); 503 504 for (MediaMenuPartsMap::const_iterator i = media_menus_.begin(); 505 i != media_menus_.end(); ++i) { 506 i->first->set_min_size(gfx::Size(menu_width, 0)); 507 i->first->set_max_size(gfx::Size(menu_width, 0)); 508 } 509} 510