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