Home | History | Annotate | Download | only in views
      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