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