Home | History | Annotate | Download | only in extensions
      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 <vector>
      6 
      7 #include "base/basictypes.h"
      8 #include "base/command_line.h"
      9 #include "base/compiler_specific.h"
     10 #include "base/i18n/rtl.h"
     11 #include "base/metrics/histogram.h"
     12 #include "base/strings/string_util.h"
     13 #include "base/strings/utf_string_conversions.h"
     14 #include "chrome/browser/extensions/bundle_installer.h"
     15 #include "chrome/browser/extensions/extension_install_prompt.h"
     16 #include "chrome/browser/extensions/extension_install_prompt_experiment.h"
     17 #include "chrome/browser/profiles/profile.h"
     18 #include "chrome/browser/ui/views/constrained_window_views.h"
     19 #include "chrome/common/chrome_switches.h"
     20 #include "chrome/common/extensions/extension_constants.h"
     21 #include "chrome/installer/util/browser_distribution.h"
     22 #include "content/public/browser/page_navigator.h"
     23 #include "content/public/browser/web_contents.h"
     24 #include "extensions/common/extension.h"
     25 #include "grit/chromium_strings.h"
     26 #include "grit/generated_resources.h"
     27 #include "grit/google_chrome_strings.h"
     28 #include "grit/theme_resources.h"
     29 #include "ui/base/l10n/l10n_util.h"
     30 #include "ui/base/resource/resource_bundle.h"
     31 #include "ui/gfx/animation/animation_delegate.h"
     32 #include "ui/gfx/animation/slide_animation.h"
     33 #include "ui/gfx/text_utils.h"
     34 #include "ui/gfx/transform.h"
     35 #include "ui/views/background.h"
     36 #include "ui/views/border.h"
     37 #include "ui/views/controls/button/checkbox.h"
     38 #include "ui/views/controls/button/image_button.h"
     39 #include "ui/views/controls/button/label_button.h"
     40 #include "ui/views/controls/image_view.h"
     41 #include "ui/views/controls/label.h"
     42 #include "ui/views/controls/link.h"
     43 #include "ui/views/controls/link_listener.h"
     44 #include "ui/views/controls/scroll_view.h"
     45 #include "ui/views/controls/separator.h"
     46 #include "ui/views/layout/box_layout.h"
     47 #include "ui/views/layout/grid_layout.h"
     48 #include "ui/views/layout/layout_constants.h"
     49 #include "ui/views/view.h"
     50 #include "ui/views/widget/widget.h"
     51 #include "ui/views/window/dialog_client_view.h"
     52 #include "ui/views/window/dialog_delegate.h"
     53 
     54 using content::OpenURLParams;
     55 using content::Referrer;
     56 using extensions::BundleInstaller;
     57 
     58 namespace {
     59 
     60 // Size of extension icon in top left of dialog.
     61 const int kIconSize = 64;
     62 
     63 // We offset the icon a little bit from the right edge of the dialog, to make it
     64 // align with the button below it.
     65 const int kIconOffset = 16;
     66 
     67 // The dialog will resize based on its content, but this sets a maximum height
     68 // before overflowing a scrollbar.
     69 const int kDialogMaxHeight = 300;
     70 
     71 // Width of the left column of the dialog when the extension requests
     72 // permissions.
     73 const int kPermissionsLeftColumnWidth = 250;
     74 
     75 // Width of the left column of the dialog when the extension requests no
     76 // permissions.
     77 const int kNoPermissionsLeftColumnWidth = 200;
     78 
     79 // Width of the left column for bundle install prompts. There's only one column
     80 // in this case, so make it wider than normal.
     81 const int kBundleLeftColumnWidth = 300;
     82 
     83 // Width of the left column for external install prompts. The text is long in
     84 // this case, so make it wider than normal.
     85 const int kExternalInstallLeftColumnWidth = 350;
     86 
     87 // Lighter color for labels.
     88 const SkColor kLighterLabelColor = SkColorSetRGB(0x99, 0x99, 0x99);
     89 
     90 // Represents an action on a clickable link created by the install prompt
     91 // experiment. This is used to group the actions in UMA histograms named
     92 // Extensions.InstallPromptExperiment.ShowDetails and
     93 // Extensions.InstallPromptExperiment.ShowPermissions.
     94 enum ExperimentLinkAction {
     95   LINK_SHOWN = 0,
     96   LINK_NOT_SHOWN,
     97   LINK_CLICKED,
     98   NUM_LINK_ACTIONS
     99 };
    100 
    101 typedef std::vector<base::string16> PermissionDetails;
    102 class ExpandableContainerView;
    103 
    104 void AddResourceIcon(const gfx::ImageSkia* skia_image, void* data) {
    105   views::View* parent = static_cast<views::View*>(data);
    106   views::ImageView* image_view = new views::ImageView();
    107   image_view->SetImage(*skia_image);
    108   parent->AddChildView(image_view);
    109 }
    110 
    111 // Creates a string for displaying |message| to the user. If it has to look
    112 // like a entry in a bullet point list, one is added.
    113 base::string16 PrepareForDisplay(const base::string16& message,
    114                                  bool bullet_point) {
    115   return bullet_point ? l10n_util::GetStringFUTF16(
    116       IDS_EXTENSION_PERMISSION_LINE,
    117       message) : message;
    118 }
    119 
    120 // A custom scrollable view implementation for the dialog.
    121 class CustomScrollableView : public views::View {
    122  public:
    123   CustomScrollableView();
    124   virtual ~CustomScrollableView();
    125 
    126  private:
    127   virtual void Layout() OVERRIDE;
    128 
    129   DISALLOW_COPY_AND_ASSIGN(CustomScrollableView);
    130 };
    131 
    132 // Implements the extension installation dialog for TOOLKIT_VIEWS.
    133 class ExtensionInstallDialogView : public views::DialogDelegateView,
    134                                    public views::LinkListener,
    135                                    public views::ButtonListener {
    136  public:
    137   ExtensionInstallDialogView(
    138       content::PageNavigator* navigator,
    139       ExtensionInstallPrompt::Delegate* delegate,
    140       scoped_refptr<ExtensionInstallPrompt::Prompt> prompt);
    141   virtual ~ExtensionInstallDialogView();
    142 
    143   // Called when one of the child elements has expanded/collapsed.
    144   void ContentsChanged();
    145 
    146  private:
    147   // views::DialogDelegateView:
    148   virtual int GetDialogButtons() const OVERRIDE;
    149   virtual base::string16 GetDialogButtonLabel(
    150       ui::DialogButton button) const OVERRIDE;
    151   virtual int GetDefaultDialogButton() const OVERRIDE;
    152   virtual bool Cancel() OVERRIDE;
    153   virtual bool Accept() OVERRIDE;
    154   virtual ui::ModalType GetModalType() const OVERRIDE;
    155   virtual base::string16 GetWindowTitle() const OVERRIDE;
    156   virtual void Layout() OVERRIDE;
    157   virtual gfx::Size GetPreferredSize() const OVERRIDE;
    158   virtual void ViewHierarchyChanged(
    159       const ViewHierarchyChangedDetails& details) OVERRIDE;
    160 
    161   // views::LinkListener:
    162   virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE;
    163 
    164   // views::ButtonListener:
    165   virtual void ButtonPressed(views::Button* sender,
    166                              const ui::Event& event) OVERRIDE;
    167 
    168   // Experimental: Toggles inline permission explanations with an animation.
    169   void ToggleInlineExplanations();
    170 
    171   // Creates a layout consisting of dialog header, extension name and icon.
    172   views::GridLayout* CreateLayout(
    173       views::View* parent,
    174       int left_column_width,
    175       int column_set_id,
    176       bool single_detail_row) const;
    177 
    178   bool is_inline_install() const {
    179     return prompt_->type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT;
    180   }
    181 
    182   bool is_bundle_install() const {
    183     return prompt_->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT;
    184   }
    185 
    186   bool is_external_install() const {
    187     return prompt_->type() == ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT;
    188   }
    189 
    190   // Updates the histogram that holds installation accepted/aborted data.
    191   void UpdateInstallResultHistogram(bool accepted) const;
    192 
    193   // Updates the histogram that holds data about whether "Show details" or
    194   // "Show permissions" links were shown and/or clicked.
    195   void UpdateLinkActionHistogram(int action_type) const;
    196 
    197   content::PageNavigator* navigator_;
    198   ExtensionInstallPrompt::Delegate* delegate_;
    199   scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_;
    200 
    201   // The scroll view containing all the details for the dialog (including all
    202   // collapsible/expandable sections).
    203   views::ScrollView* scroll_view_;
    204 
    205   // The container view for the scroll view.
    206   CustomScrollableView* scrollable_;
    207 
    208   // The container for the simpler view with only the dialog header and the
    209   // extension icon. Used for the experiment where the permissions are
    210   // initially hidden when the dialog shows.
    211   CustomScrollableView* scrollable_header_only_;
    212 
    213   // The preferred size of the dialog.
    214   gfx::Size dialog_size_;
    215 
    216   // Experimental: "Show details" link to expand inline explanations and reveal
    217   // permision dialog.
    218   views::Link* show_details_link_;
    219 
    220   // Experimental: Label for showing information about the checkboxes.
    221   views::Label* checkbox_info_label_;
    222 
    223   // Experimental: Contains pointers to inline explanation views.
    224   typedef std::vector<ExpandableContainerView*> InlineExplanations;
    225   InlineExplanations inline_explanations_;
    226 
    227   // Experimental: Number of unchecked checkboxes in the permission list.
    228   // If this becomes zero, the accept button is enabled, otherwise disabled.
    229   int unchecked_boxes_;
    230 
    231   DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogView);
    232 };
    233 
    234 // A simple view that prepends a view with a bullet with the help of a grid
    235 // layout.
    236 class BulletedView : public views::View {
    237  public:
    238   explicit BulletedView(views::View* view);
    239  private:
    240   DISALLOW_COPY_AND_ASSIGN(BulletedView);
    241 };
    242 
    243 BulletedView::BulletedView(views::View* view) {
    244   views::GridLayout* layout = new views::GridLayout(this);
    245   SetLayoutManager(layout);
    246   views::ColumnSet* column_set = layout->AddColumnSet(0);
    247   column_set->AddColumn(views::GridLayout::LEADING,
    248                         views::GridLayout::LEADING,
    249                         0,
    250                         views::GridLayout::USE_PREF,
    251                         0, // no fixed width
    252                         0);
    253    column_set->AddColumn(views::GridLayout::LEADING,
    254                          views::GridLayout::LEADING,
    255                          0,
    256                          views::GridLayout::USE_PREF,
    257                          0,  // no fixed width
    258                          0);
    259   layout->StartRow(0, 0);
    260   layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true)));
    261   layout->AddView(view);
    262 }
    263 
    264 // A simple view that prepends a view with a checkbox with the help of a grid
    265 // layout. Used for the permission experiment.
    266 // TODO(meacer): Remove once the experiment is completed.
    267 class CheckboxedView : public views::View {
    268  public:
    269   CheckboxedView(views::View* view, views::ButtonListener* listener);
    270  private:
    271   DISALLOW_COPY_AND_ASSIGN(CheckboxedView);
    272 };
    273 
    274 CheckboxedView::CheckboxedView(views::View* view,
    275                                views::ButtonListener* listener) {
    276   views::GridLayout* layout = new views::GridLayout(this);
    277   SetLayoutManager(layout);
    278   views::ColumnSet* column_set = layout->AddColumnSet(0);
    279   column_set->AddColumn(views::GridLayout::LEADING,
    280                         views::GridLayout::LEADING,
    281                         0,
    282                         views::GridLayout::USE_PREF,
    283                         0, // no fixed width
    284                         0);
    285    column_set->AddColumn(views::GridLayout::LEADING,
    286                          views::GridLayout::LEADING,
    287                          0,
    288                          views::GridLayout::USE_PREF,
    289                          0,  // no fixed width
    290                          0);
    291   layout->StartRow(0, 0);
    292   views::Checkbox* checkbox = new views::Checkbox(base::string16());
    293   checkbox->set_listener(listener);
    294   // Alignment needs to be explicitly set again here, otherwise the views are
    295   // not vertically centered.
    296   layout->AddView(checkbox, 1, 1,
    297                   views::GridLayout::LEADING, views::GridLayout::CENTER);
    298   layout->AddView(view, 1, 1,
    299                   views::GridLayout::LEADING, views::GridLayout::CENTER);
    300 }
    301 
    302 // A view to display text with an expandable details section.
    303 class ExpandableContainerView : public views::View,
    304                                 public views::ButtonListener,
    305                                 public views::LinkListener,
    306                                 public gfx::AnimationDelegate {
    307  public:
    308   ExpandableContainerView(ExtensionInstallDialogView* owner,
    309                           const base::string16& description,
    310                           const PermissionDetails& details,
    311                           int horizontal_space,
    312                           bool parent_bulleted,
    313                           bool show_expand_link,
    314                           bool lighter_color_details);
    315   virtual ~ExpandableContainerView();
    316 
    317   // views::View:
    318   virtual void ChildPreferredSizeChanged(views::View* child) OVERRIDE;
    319 
    320   // views::ButtonListener:
    321   virtual void ButtonPressed(views::Button* sender,
    322                              const ui::Event& event) OVERRIDE;
    323 
    324   // views::LinkListener:
    325   virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE;
    326 
    327   // gfx::AnimationDelegate:
    328   virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE;
    329   virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE;
    330 
    331   // Expand/Collapse the detail section for this ExpandableContainerView.
    332   void ToggleDetailLevel();
    333 
    334   // Expand the detail section without any animation.
    335   // TODO(meacer): Remove once the experiment is completed.
    336   void ExpandWithoutAnimation();
    337 
    338  private:
    339   // A view which displays all the details of an IssueAdviceInfoEntry.
    340   class DetailsView : public views::View {
    341    public:
    342     explicit DetailsView(int horizontal_space, bool parent_bulleted,
    343                          bool lighter_color);
    344     virtual ~DetailsView() {}
    345 
    346     // views::View:
    347     virtual gfx::Size GetPreferredSize() const OVERRIDE;
    348 
    349     void AddDetail(const base::string16& detail);
    350 
    351     // Animates this to be a height proportional to |state|.
    352     void AnimateToState(double state);
    353 
    354    private:
    355     views::GridLayout* layout_;
    356     double state_;
    357 
    358     // Whether the detail text should be shown with a lighter color.
    359     bool lighter_color_;
    360 
    361     DISALLOW_COPY_AND_ASSIGN(DetailsView);
    362   };
    363 
    364   // The dialog that owns |this|. It's also an ancestor in the View hierarchy.
    365   ExtensionInstallDialogView* owner_;
    366 
    367   // A view for showing |issue_advice.details|.
    368   DetailsView* details_view_;
    369 
    370   // The 'more details' link shown under the heading (changes to 'hide details'
    371   // when the details section is expanded).
    372   views::Link* more_details_;
    373 
    374   gfx::SlideAnimation slide_animation_;
    375 
    376   // The up/down arrow next to the 'more detail' link (points up/down depending
    377   // on whether the details section is expanded).
    378   views::ImageButton* arrow_toggle_;
    379 
    380   // Whether the details section is expanded.
    381   bool expanded_;
    382 
    383   DISALLOW_COPY_AND_ASSIGN(ExpandableContainerView);
    384 };
    385 
    386 void ShowExtensionInstallDialogImpl(
    387     const ExtensionInstallPrompt::ShowParams& show_params,
    388     ExtensionInstallPrompt::Delegate* delegate,
    389     scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) {
    390   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    391   CreateBrowserModalDialogViews(
    392       new ExtensionInstallDialogView(show_params.navigator, delegate, prompt),
    393       show_params.parent_window)->Show();
    394 }
    395 
    396 }  // namespace
    397 
    398 CustomScrollableView::CustomScrollableView() {}
    399 CustomScrollableView::~CustomScrollableView() {}
    400 
    401 void CustomScrollableView::Layout() {
    402   SetBounds(x(), y(), width(), GetHeightForWidth(width()));
    403   views::View::Layout();
    404 }
    405 
    406 ExtensionInstallDialogView::ExtensionInstallDialogView(
    407     content::PageNavigator* navigator,
    408     ExtensionInstallPrompt::Delegate* delegate,
    409     scoped_refptr<ExtensionInstallPrompt::Prompt> prompt)
    410     : navigator_(navigator),
    411       delegate_(delegate),
    412       prompt_(prompt),
    413       scroll_view_(NULL),
    414       scrollable_(NULL),
    415       scrollable_header_only_(NULL),
    416       show_details_link_(NULL),
    417       checkbox_info_label_(NULL),
    418       unchecked_boxes_(0) {
    419   // Possible grid layouts without ExtensionPermissionDialog experiment:
    420   // Inline install
    421   //      w/ permissions                 no permissions
    422   // +--------------------+------+  +--------------+------+
    423   // | heading            | icon |  | heading      | icon |
    424   // +--------------------|      |  +--------------|      |
    425   // | rating             |      |  | rating       |      |
    426   // +--------------------|      |  +--------------+      |
    427   // | user_count         |      |  | user_count   |      |
    428   // +--------------------|      |  +--------------|      |
    429   // | store_link         |      |  | store_link   |      |
    430   // +--------------------+------+  +--------------+------+
    431   // |      separator            |
    432   // +--------------------+------+
    433   // | permissions_header |      |
    434   // +--------------------+------+
    435   // | permission1        |      |
    436   // +--------------------+------+
    437   // | permission2        |      |
    438   // +--------------------+------+
    439   //
    440   // Regular install
    441   // w/ permissions                     no permissions
    442   // +--------------------+------+  +--------------+------+
    443   // | heading            | icon |  | heading      | icon |
    444   // +--------------------|      |  +--------------+------+
    445   // | permissions_header |      |
    446   // +--------------------|      |
    447   // | permission1        |      |
    448   // +--------------------|      |
    449   // | permission2        |      |
    450   // +--------------------+------+
    451   //
    452   // If the ExtensionPermissionDialog is on, the layout is modified depending
    453   // on the experiment group. For text only experiment, a footer is added at the
    454   // bottom of the layouts. For others, inline details are added below some of
    455   // the permissions.
    456   //
    457   // Regular install w/ permissions and footer (experiment):
    458   // +--------------------+------+
    459   // | heading            | icon |
    460   // +--------------------|      |
    461   // | permissions_header |      |
    462   // +--------------------|      |
    463   // | permission1        |      |
    464   // +--------------------|      |
    465   // | permission2        |      |
    466   // +--------------------+------+
    467   // | footer text        |      |
    468   // +--------------------+------+
    469   //
    470   // Regular install w/ permissions and inline explanations (experiment):
    471   // +--------------------+------+
    472   // | heading            | icon |
    473   // +--------------------|      |
    474   // | permissions_header |      |
    475   // +--------------------|      |
    476   // | permission1        |      |
    477   // +--------------------|      |
    478   // | explanation1       |      |
    479   // +--------------------|      |
    480   // | permission2        |      |
    481   // +--------------------|      |
    482   // | explanation2       |      |
    483   // +--------------------+------+
    484   //
    485   // Regular install w/ permissions and inline explanations (experiment):
    486   // +--------------------+------+
    487   // | heading            | icon |
    488   // +--------------------|      |
    489   // | permissions_header |      |
    490   // +--------------------|      |
    491   // |checkbox|permission1|      |
    492   // +--------------------|      |
    493   // |checkbox|permission2|      |
    494   // +--------------------+------+
    495   //
    496   // Additionally, links or informational text is added to non-client areas of
    497   // the dialog depending on the experiment group.
    498 
    499   int left_column_width =
    500       (prompt->ShouldShowPermissions() + prompt->GetRetainedFileCount()) > 0
    501           ? kPermissionsLeftColumnWidth
    502           : kNoPermissionsLeftColumnWidth;
    503   if (is_bundle_install())
    504     left_column_width = kBundleLeftColumnWidth;
    505   if (is_external_install())
    506     left_column_width = kExternalInstallLeftColumnWidth;
    507 
    508   scroll_view_ = new views::ScrollView();
    509   scroll_view_->set_hide_horizontal_scrollbar(true);
    510   AddChildView(scroll_view_);
    511 
    512   int column_set_id = 0;
    513   // Create the full scrollable view which will contain all the information
    514   // including the permissions.
    515   scrollable_ = new CustomScrollableView();
    516   views::GridLayout* layout = CreateLayout(
    517       scrollable_, left_column_width, column_set_id, false);
    518   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    519 
    520   if (prompt->ShouldShowPermissions() &&
    521       prompt->experiment()->should_show_expandable_permission_list()) {
    522     // If the experiment should hide the permission list initially, create a
    523     // simple layout that contains only the header, extension name and icon.
    524     scrollable_header_only_ = new CustomScrollableView();
    525     CreateLayout(scrollable_header_only_, left_column_width,
    526                  column_set_id, true);
    527     scroll_view_->SetContents(scrollable_header_only_);
    528   } else {
    529     scroll_view_->SetContents(scrollable_);
    530   }
    531 
    532   int dialog_width = left_column_width + 2 * views::kPanelHorizMargin;
    533   if (!is_bundle_install())
    534     dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset;
    535 
    536   // Widen the dialog for experiment with checkboxes so that the information
    537   // label fits the area to the left of the buttons.
    538   if (prompt->experiment()->show_checkboxes())
    539     dialog_width += 4 * views::kPanelHorizMargin;
    540 
    541   if (prompt->has_webstore_data()) {
    542     layout->StartRow(0, column_set_id);
    543     views::View* rating = new views::View();
    544     rating->SetLayoutManager(new views::BoxLayout(
    545         views::BoxLayout::kHorizontal, 0, 0, 0));
    546     layout->AddView(rating);
    547     prompt->AppendRatingStars(AddResourceIcon, rating);
    548 
    549     const gfx::FontList& small_font_list =
    550         rb.GetFontList(ui::ResourceBundle::SmallFont);
    551     views::Label* rating_count =
    552         new views::Label(prompt->GetRatingCount(), small_font_list);
    553     // Add some space between the stars and the rating count.
    554     rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0));
    555     rating->AddChildView(rating_count);
    556 
    557     layout->StartRow(0, column_set_id);
    558     views::Label* user_count =
    559         new views::Label(prompt->GetUserCount(), small_font_list);
    560     user_count->SetAutoColorReadabilityEnabled(false);
    561     user_count->SetEnabledColor(SK_ColorGRAY);
    562     layout->AddView(user_count);
    563 
    564     layout->StartRow(0, column_set_id);
    565     views::Link* store_link = new views::Link(
    566         l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK));
    567     store_link->SetFontList(small_font_list);
    568     store_link->set_listener(this);
    569     layout->AddView(store_link);
    570   }
    571 
    572   if (is_bundle_install()) {
    573     BundleInstaller::ItemList items = prompt->bundle()->GetItemsWithState(
    574         BundleInstaller::Item::STATE_PENDING);
    575     for (size_t i = 0; i < items.size(); ++i) {
    576       base::string16 extension_name =
    577           base::UTF8ToUTF16(items[i].localized_name);
    578       base::i18n::AdjustStringForLocaleDirection(&extension_name);
    579       layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    580       layout->StartRow(0, column_set_id);
    581       views::Label* extension_label = new views::Label(
    582           PrepareForDisplay(extension_name, true));
    583       extension_label->SetMultiLine(true);
    584       extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    585       extension_label->SizeToFit(left_column_width);
    586       layout->AddView(extension_label);
    587     }
    588   }
    589 
    590   if (prompt->ShouldShowPermissions()) {
    591     layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    592 
    593     if (prompt->GetPermissionCount() > 0) {
    594       if (is_inline_install()) {
    595         layout->StartRow(0, column_set_id);
    596         layout->AddView(new views::Separator(views::Separator::HORIZONTAL),
    597                         3, 1, views::GridLayout::FILL, views::GridLayout::FILL);
    598         layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    599       }
    600 
    601       layout->StartRow(0, column_set_id);
    602       views::Label* permissions_header = NULL;
    603       if (is_bundle_install()) {
    604         // We need to pass the FontList in the constructor, rather than calling
    605         // SetFontList later, because otherwise SizeToFit mis-judges the width
    606         // of the line.
    607         permissions_header =
    608             new views::Label(prompt->GetPermissionsHeading(),
    609                              rb.GetFontList(ui::ResourceBundle::MediumFont));
    610       } else {
    611         permissions_header = new views::Label(prompt->GetPermissionsHeading());
    612       }
    613       permissions_header->SetMultiLine(true);
    614       permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    615       permissions_header->SizeToFit(left_column_width);
    616       layout->AddView(permissions_header);
    617 
    618       for (size_t i = 0; i < prompt->GetPermissionCount(); ++i) {
    619         layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    620         layout->StartRow(0, column_set_id);
    621         views::Label* permission_label =
    622             new views::Label(prompt->GetPermission(i));
    623 
    624         const SkColor kTextHighlight = SK_ColorRED;
    625         const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3);
    626         if (prompt->experiment()->ShouldHighlightText(
    627                 prompt->GetPermission(i))) {
    628           permission_label->SetAutoColorReadabilityEnabled(false);
    629           permission_label->SetEnabledColor(kTextHighlight);
    630         } else if (prompt->experiment()->ShouldHighlightBackground(
    631                        prompt->GetPermission(i))) {
    632           permission_label->SetLineHeight(18);
    633           permission_label->set_background(
    634               views::Background::CreateSolidBackground(kBackgroundHighlight));
    635         }
    636 
    637         permission_label->SetMultiLine(true);
    638         permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    639         permission_label->SizeToFit(left_column_width);
    640 
    641         if (prompt->experiment()->show_checkboxes()) {
    642           layout->AddView(new CheckboxedView(permission_label, this));
    643           ++unchecked_boxes_;
    644         } else {
    645           layout->AddView(new BulletedView(permission_label));
    646         }
    647         // If we have more details to provide, show them in collapsed form.
    648         if (!prompt->GetPermissionsDetails(i).empty()) {
    649           layout->StartRow(0, column_set_id);
    650           PermissionDetails details;
    651           details.push_back(
    652               PrepareForDisplay(prompt->GetPermissionsDetails(i), false));
    653           ExpandableContainerView* details_container =
    654               new ExpandableContainerView(
    655                   this, base::string16(), details, left_column_width,
    656                   true, true, false);
    657           layout->AddView(details_container);
    658         }
    659 
    660         if (prompt->experiment()->should_show_inline_explanations()) {
    661           base::string16 explanation =
    662               prompt->experiment()->GetInlineExplanation(
    663                   prompt->GetPermission(i));
    664           if (!explanation.empty()) {
    665             PermissionDetails details;
    666             details.push_back(explanation);
    667             ExpandableContainerView* container =
    668                 new ExpandableContainerView(this, base::string16(), details,
    669                                             left_column_width,
    670                                             false, false, true);
    671             // Inline explanations are expanded by default if there is
    672             // no "Show details" link.
    673             if (!prompt->experiment()->show_details_link())
    674               container->ExpandWithoutAnimation();
    675             layout->StartRow(0, column_set_id);
    676             layout->AddView(container);
    677             inline_explanations_.push_back(container);
    678           }
    679         }
    680       }
    681     } else {
    682       layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    683       layout->StartRow(0, column_set_id);
    684       views::Label* permission_label = new views::Label(
    685           l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS));
    686       permission_label->SetMultiLine(true);
    687       permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    688       permission_label->SizeToFit(left_column_width);
    689       layout->AddView(permission_label);
    690     }
    691   }
    692 
    693   if (prompt->GetRetainedFileCount()) {
    694     // Slide in under the permissions, if there are any. If there are
    695     // either, the retained files prompt stretches all the way to the
    696     // right of the dialog. If there are no permissions, the retained
    697     // files prompt just takes up the left column.
    698     int space_for_files = left_column_width;
    699     if (prompt->GetPermissionCount()) {
    700       space_for_files += kIconSize;
    701       views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
    702       column_set->AddColumn(views::GridLayout::FILL,
    703                             views::GridLayout::FILL,
    704                             1,
    705                             views::GridLayout::USE_PREF,
    706                             0,  // no fixed width
    707                             space_for_files);
    708     }
    709 
    710     layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
    711 
    712     layout->StartRow(0, column_set_id);
    713     views::Label* retained_files_header = NULL;
    714     retained_files_header = new views::Label(prompt->GetRetainedFilesHeading());
    715     retained_files_header->SetMultiLine(true);
    716     retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    717     retained_files_header->SizeToFit(space_for_files);
    718     layout->AddView(retained_files_header);
    719 
    720     layout->StartRow(0, column_set_id);
    721     PermissionDetails details;
    722     for (size_t i = 0; i < prompt->GetRetainedFileCount(); ++i)
    723       details.push_back(prompt->GetRetainedFile(i));
    724     ExpandableContainerView* issue_advice_view =
    725         new ExpandableContainerView(
    726             this, base::string16(), details, space_for_files,
    727             false, true, false);
    728     layout->AddView(issue_advice_view);
    729   }
    730 
    731   DCHECK(prompt->type() >= 0);
    732   UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type",
    733                             prompt->type(),
    734                             ExtensionInstallPrompt::NUM_PROMPT_TYPES);
    735 
    736   if (prompt->ShouldShowPermissions()) {
    737     if (prompt->ShouldShowExplanationText()) {
    738       views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
    739       column_set->AddColumn(views::GridLayout::LEADING,
    740                             views::GridLayout::FILL,
    741                             1,
    742                             views::GridLayout::USE_PREF,
    743                             0,
    744                             0);
    745       // Add two rows of space so that the text stands out.
    746       layout->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing);
    747 
    748       layout->StartRow(0, column_set_id);
    749       views::Label* explanation =
    750           new views::Label(prompt->experiment()->GetExplanationText());
    751       explanation->SetMultiLine(true);
    752       explanation->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    753       explanation->SizeToFit(left_column_width + kIconSize);
    754       layout->AddView(explanation);
    755     }
    756 
    757     if (prompt->experiment()->should_show_expandable_permission_list() ||
    758         (prompt->experiment()->show_details_link() &&
    759          prompt->experiment()->should_show_inline_explanations() &&
    760          !inline_explanations_.empty())) {
    761       // Don't show the "Show details" link if there are retained
    762       // files.  These have their own "Show details" links and having
    763       // multiple levels of links is confusing.
    764       if (prompt->GetRetainedFileCount() == 0) {
    765         int text_id =
    766             prompt->experiment()->should_show_expandable_permission_list()
    767                 ? IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS
    768                 : IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_DETAILS;
    769         show_details_link_ = new views::Link(
    770             l10n_util::GetStringUTF16(text_id));
    771         show_details_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    772         show_details_link_->set_listener(this);
    773         UpdateLinkActionHistogram(LINK_SHOWN);
    774       } else {
    775         UpdateLinkActionHistogram(LINK_NOT_SHOWN);
    776       }
    777     }
    778 
    779     if (prompt->experiment()->show_checkboxes()) {
    780       checkbox_info_label_ = new views::Label(
    781           l10n_util::GetStringUTF16(
    782               IDS_EXTENSION_PROMPT_EXPERIMENT_CHECKBOX_INFO));
    783       checkbox_info_label_->SetMultiLine(true);
    784       checkbox_info_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    785       checkbox_info_label_->SetAutoColorReadabilityEnabled(false);
    786       checkbox_info_label_->SetEnabledColor(kLighterLabelColor);
    787     }
    788   }
    789 
    790   gfx::Size scrollable_size = scrollable_->GetPreferredSize();
    791   scrollable_->SetBoundsRect(gfx::Rect(scrollable_size));
    792   dialog_size_ = gfx::Size(
    793       dialog_width,
    794       std::min(scrollable_size.height(), kDialogMaxHeight));
    795 
    796   if (scrollable_header_only_) {
    797     gfx::Size header_only_size = scrollable_header_only_->GetPreferredSize();
    798     scrollable_header_only_->SetBoundsRect(gfx::Rect(header_only_size));
    799     dialog_size_ = gfx::Size(
    800         dialog_width, std::min(header_only_size.height(), kDialogMaxHeight));
    801   }
    802 }
    803 
    804 ExtensionInstallDialogView::~ExtensionInstallDialogView() {}
    805 
    806 views::GridLayout* ExtensionInstallDialogView::CreateLayout(
    807     views::View* parent,
    808     int left_column_width,
    809     int column_set_id,
    810     bool single_detail_row) const {
    811   views::GridLayout* layout = views::GridLayout::CreatePanel(parent);
    812   parent->SetLayoutManager(layout);
    813 
    814   views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
    815   column_set->AddColumn(views::GridLayout::LEADING,
    816                         views::GridLayout::FILL,
    817                         0,  // no resizing
    818                         views::GridLayout::USE_PREF,
    819                         0,  // no fixed width
    820                         left_column_width);
    821   if (!is_bundle_install()) {
    822     column_set->AddPaddingColumn(0, views::kPanelHorizMargin);
    823     column_set->AddColumn(views::GridLayout::TRAILING,
    824                           views::GridLayout::LEADING,
    825                           0,  // no resizing
    826                           views::GridLayout::USE_PREF,
    827                           0,  // no fixed width
    828                           kIconSize);
    829   }
    830 
    831   layout->StartRow(0, column_set_id);
    832 
    833   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    834 
    835   views::Label* heading = new views::Label(
    836       prompt_->GetHeading(), rb.GetFontList(ui::ResourceBundle::MediumFont));
    837   heading->SetMultiLine(true);
    838   heading->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    839   heading->SizeToFit(left_column_width);
    840   layout->AddView(heading);
    841 
    842   if (!is_bundle_install()) {
    843     // Scale down to icon size, but allow smaller icons (don't scale up).
    844     const gfx::ImageSkia* image = prompt_->icon().ToImageSkia();
    845     gfx::Size size(image->width(), image->height());
    846     if (size.width() > kIconSize || size.height() > kIconSize)
    847       size = gfx::Size(kIconSize, kIconSize);
    848     views::ImageView* icon = new views::ImageView();
    849     icon->SetImageSize(size);
    850     icon->SetImage(*image);
    851     icon->SetHorizontalAlignment(views::ImageView::CENTER);
    852     icon->SetVerticalAlignment(views::ImageView::CENTER);
    853     if (single_detail_row) {
    854       layout->AddView(icon);
    855     } else {
    856       int icon_row_span = 1;
    857       if (is_inline_install()) {
    858         // Also span the rating, user_count and store_link rows.
    859         icon_row_span = 4;
    860       } else if (prompt_->ShouldShowPermissions()) {
    861         size_t permission_count = prompt_->GetPermissionCount();
    862         // Also span the permission header and each of the permission rows (all
    863         // have a padding row above it). This also works for the 'no special
    864         // permissions' case.
    865         icon_row_span = 3 + permission_count * 2;
    866       } else if (prompt_->GetRetainedFileCount()) {
    867         // Also span the permission header and the retained files container.
    868         icon_row_span = 4;
    869       }
    870       layout->AddView(icon, 1, icon_row_span);
    871     }
    872   }
    873   return layout;
    874 }
    875 
    876 void ExtensionInstallDialogView::ContentsChanged() {
    877   Layout();
    878 }
    879 
    880 void ExtensionInstallDialogView::ViewHierarchyChanged(
    881     const ViewHierarchyChangedDetails& details) {
    882   // Since we want the links to show up in the same visual row as the accept
    883   // and cancel buttons, which is provided by the framework, we must add the
    884   // buttons to the non-client view, which is the parent of this view.
    885   // Similarly, when we're removed from the view hierarchy, we must take care
    886   // to clean up those items as well.
    887   if (details.child == this) {
    888     if (details.is_add) {
    889       if (show_details_link_)
    890         details.parent->AddChildView(show_details_link_);
    891       if (checkbox_info_label_)
    892         details.parent->AddChildView(checkbox_info_label_);
    893     } else {
    894       if (show_details_link_)
    895         details.parent->RemoveChildView(show_details_link_);
    896       if (checkbox_info_label_)
    897         details.parent->RemoveChildView(checkbox_info_label_);
    898     }
    899   }
    900 }
    901 
    902 int ExtensionInstallDialogView::GetDialogButtons() const {
    903   int buttons = prompt_->GetDialogButtons();
    904   // Simply having just an OK button is *not* supported. See comment on function
    905   // GetDialogButtons in dialog_delegate.h for reasons.
    906   DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0);
    907   return buttons;
    908 }
    909 
    910 base::string16 ExtensionInstallDialogView::GetDialogButtonLabel(
    911     ui::DialogButton button) const {
    912   switch (button) {
    913     case ui::DIALOG_BUTTON_OK:
    914       return prompt_->GetAcceptButtonLabel();
    915     case ui::DIALOG_BUTTON_CANCEL:
    916       return prompt_->HasAbortButtonLabel()
    917                  ? prompt_->GetAbortButtonLabel()
    918                  : l10n_util::GetStringUTF16(IDS_CANCEL);
    919     default:
    920       NOTREACHED();
    921       return base::string16();
    922   }
    923 }
    924 
    925 int ExtensionInstallDialogView::GetDefaultDialogButton() const {
    926   return ui::DIALOG_BUTTON_CANCEL;
    927 }
    928 
    929 bool ExtensionInstallDialogView::Cancel() {
    930   UpdateInstallResultHistogram(false);
    931   delegate_->InstallUIAbort(true);
    932   return true;
    933 }
    934 
    935 bool ExtensionInstallDialogView::Accept() {
    936   UpdateInstallResultHistogram(true);
    937   delegate_->InstallUIProceed();
    938   return true;
    939 }
    940 
    941 ui::ModalType ExtensionInstallDialogView::GetModalType() const {
    942   return ui::MODAL_TYPE_WINDOW;
    943 }
    944 
    945 base::string16 ExtensionInstallDialogView::GetWindowTitle() const {
    946   return prompt_->GetDialogTitle();
    947 }
    948 
    949 void ExtensionInstallDialogView::LinkClicked(views::Link* source,
    950                                              int event_flags) {
    951   if (source == show_details_link_) {
    952     UpdateLinkActionHistogram(LINK_CLICKED);
    953     // Show details link is used to either reveal whole permission list or to
    954     // reveal inline explanations.
    955     if (prompt_->experiment()->should_show_expandable_permission_list()) {
    956       gfx::Rect bounds = GetWidget()->GetWindowBoundsInScreen();
    957       int spacing = bounds.height() -
    958           scrollable_header_only_->GetPreferredSize().height();
    959       int content_height = std::min(scrollable_->GetPreferredSize().height(),
    960                                     kDialogMaxHeight);
    961       bounds.set_height(spacing + content_height);
    962       scroll_view_->SetContents(scrollable_);
    963       GetWidget()->SetBoundsConstrained(bounds);
    964       ContentsChanged();
    965     } else {
    966       ToggleInlineExplanations();
    967     }
    968     show_details_link_->SetVisible(false);
    969   } else {
    970     GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
    971                    prompt_->extension()->id());
    972     OpenURLParams params(
    973         store_url, Referrer(), NEW_FOREGROUND_TAB,
    974         content::PAGE_TRANSITION_LINK,
    975         false);
    976     navigator_->OpenURL(params);
    977     GetWidget()->Close();
    978   }
    979 }
    980 
    981 void ExtensionInstallDialogView::ToggleInlineExplanations() {
    982   for (InlineExplanations::iterator it = inline_explanations_.begin();
    983       it != inline_explanations_.end(); ++it)
    984     (*it)->ToggleDetailLevel();
    985 }
    986 
    987 void ExtensionInstallDialogView::Layout() {
    988   scroll_view_->SetBounds(0, 0, width(), height());
    989 
    990   if (show_details_link_ || checkbox_info_label_) {
    991     views::LabelButton* cancel_button = GetDialogClientView()->cancel_button();
    992     gfx::Rect parent_bounds = parent()->GetContentsBounds();
    993     // By default, layouts have an inset of kButtonHEdgeMarginNew. In order to
    994     // align the link horizontally with the left side of the contents of the
    995     // layout, put a horizontal margin with this amount.
    996     const int horizontal_margin = views::kButtonHEdgeMarginNew;
    997     const int vertical_margin = views::kButtonVEdgeMarginNew;
    998     int y_buttons = parent_bounds.bottom() -
    999         cancel_button->GetPreferredSize().height() - vertical_margin;
   1000     int max_width = dialog_size_.width() - cancel_button->width() * 2 -
   1001         horizontal_margin * 2 - views::kRelatedButtonHSpacing;
   1002     if (show_details_link_) {
   1003       gfx::Size link_size = show_details_link_->GetPreferredSize();
   1004       show_details_link_->SetBounds(
   1005           horizontal_margin,
   1006           y_buttons + (cancel_button->height() - link_size.height()) / 2,
   1007           link_size.width(), link_size.height());
   1008     }
   1009     if (checkbox_info_label_) {
   1010       gfx::Size label_size = checkbox_info_label_->GetPreferredSize();
   1011       checkbox_info_label_->SetBounds(
   1012           horizontal_margin,
   1013           y_buttons + (cancel_button->height() - label_size.height()) / 2,
   1014           label_size.width(), label_size.height());
   1015       checkbox_info_label_->SizeToFit(max_width);
   1016     }
   1017   }
   1018   // Disable accept button if there are unchecked boxes and
   1019   // the experiment is on.
   1020   if (prompt_->experiment()->show_checkboxes())
   1021     GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
   1022 
   1023   DialogDelegateView::Layout();
   1024 }
   1025 
   1026 gfx::Size ExtensionInstallDialogView::GetPreferredSize() const {
   1027   return dialog_size_;
   1028 }
   1029 
   1030 void ExtensionInstallDialogView::ButtonPressed(views::Button* sender,
   1031                                                const ui::Event& event) {
   1032   if (std::string(views::Checkbox::kViewClassName) == sender->GetClassName()) {
   1033     views::Checkbox* checkbox = static_cast<views::Checkbox*>(sender);
   1034     if (checkbox->checked())
   1035       --unchecked_boxes_;
   1036     else
   1037       ++unchecked_boxes_;
   1038 
   1039     GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
   1040     checkbox_info_label_->SetVisible(unchecked_boxes_ > 0);
   1041   }
   1042 }
   1043 
   1044 void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted)
   1045     const {
   1046   if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT)
   1047     UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted);
   1048 }
   1049 
   1050 void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type)
   1051     const {
   1052   if (prompt_->experiment()->should_show_expandable_permission_list()) {
   1053     // The clickable link in the UI is "Show Permissions".
   1054     UMA_HISTOGRAM_ENUMERATION(
   1055         "Extensions.InstallPromptExperiment.ShowPermissions",
   1056         action_type,
   1057         NUM_LINK_ACTIONS);
   1058   } else {
   1059     // The clickable link in the UI is "Show Details".
   1060     UMA_HISTOGRAM_ENUMERATION(
   1061         "Extensions.InstallPromptExperiment.ShowDetails",
   1062         action_type,
   1063         NUM_LINK_ACTIONS);
   1064   }
   1065 }
   1066 
   1067 // static
   1068 ExtensionInstallPrompt::ShowDialogCallback
   1069 ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
   1070   return base::Bind(&ShowExtensionInstallDialogImpl);
   1071 }
   1072 
   1073 // ExpandableContainerView::DetailsView ----------------------------------------
   1074 
   1075 ExpandableContainerView::DetailsView::DetailsView(int horizontal_space,
   1076                                                   bool parent_bulleted,
   1077                                                   bool lighter_color)
   1078     : layout_(new views::GridLayout(this)),
   1079       state_(0),
   1080       lighter_color_(lighter_color) {
   1081   SetLayoutManager(layout_);
   1082   views::ColumnSet* column_set = layout_->AddColumnSet(0);
   1083   // If the parent is using bullets for its items, then a padding of one unit
   1084   // will make the child item (which has no bullet) look like a sibling of its
   1085   // parent. Therefore increase the indentation by one more unit to show that it
   1086   // is in fact a child item (with no missing bullet) and not a sibling.
   1087   int padding =
   1088       views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1);
   1089   column_set->AddPaddingColumn(0, padding);
   1090   column_set->AddColumn(views::GridLayout::LEADING,
   1091                         views::GridLayout::LEADING,
   1092                         0,
   1093                         views::GridLayout::FIXED,
   1094                         horizontal_space - padding,
   1095                         0);
   1096 }
   1097 
   1098 void ExpandableContainerView::DetailsView::AddDetail(
   1099     const base::string16& detail) {
   1100   layout_->StartRowWithPadding(0, 0,
   1101                                0, views::kRelatedControlSmallVerticalSpacing);
   1102   views::Label* detail_label =
   1103       new views::Label(PrepareForDisplay(detail, false));
   1104   detail_label->SetMultiLine(true);
   1105   detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
   1106   if (lighter_color_) {
   1107     detail_label->SetEnabledColor(kLighterLabelColor);
   1108     detail_label->SetAutoColorReadabilityEnabled(false);
   1109   }
   1110   layout_->AddView(detail_label);
   1111 }
   1112 
   1113 gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const {
   1114   gfx::Size size = views::View::GetPreferredSize();
   1115   return gfx::Size(size.width(), size.height() * state_);
   1116 }
   1117 
   1118 void ExpandableContainerView::DetailsView::AnimateToState(double state) {
   1119   state_ = state;
   1120   PreferredSizeChanged();
   1121   SchedulePaint();
   1122 }
   1123 
   1124 // ExpandableContainerView -----------------------------------------------------
   1125 
   1126 ExpandableContainerView::ExpandableContainerView(
   1127     ExtensionInstallDialogView* owner,
   1128     const base::string16& description,
   1129     const PermissionDetails& details,
   1130     int horizontal_space,
   1131     bool parent_bulleted,
   1132     bool show_expand_link,
   1133     bool lighter_color_details)
   1134     : owner_(owner),
   1135       details_view_(NULL),
   1136       more_details_(NULL),
   1137       slide_animation_(this),
   1138       arrow_toggle_(NULL),
   1139       expanded_(false) {
   1140   views::GridLayout* layout = new views::GridLayout(this);
   1141   SetLayoutManager(layout);
   1142   int column_set_id = 0;
   1143   views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
   1144   column_set->AddColumn(views::GridLayout::LEADING,
   1145                         views::GridLayout::LEADING,
   1146                         0,
   1147                         views::GridLayout::USE_PREF,
   1148                         0,
   1149                         0);
   1150   if (!description.empty()) {
   1151     layout->StartRow(0, column_set_id);
   1152 
   1153     views::Label* description_label = new views::Label(description);
   1154     description_label->SetMultiLine(true);
   1155     description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
   1156     description_label->SizeToFit(horizontal_space);
   1157     layout->AddView(new BulletedView(description_label));
   1158   }
   1159 
   1160   if (details.empty())
   1161     return;
   1162 
   1163   details_view_ = new DetailsView(horizontal_space, parent_bulleted,
   1164                                   lighter_color_details);
   1165 
   1166   layout->StartRow(0, column_set_id);
   1167   layout->AddView(details_view_);
   1168 
   1169   for (size_t i = 0; i < details.size(); ++i)
   1170     details_view_->AddDetail(details[i]);
   1171 
   1172   // TODO(meacer): Remove show_expand_link when the experiment is completed.
   1173   if (show_expand_link) {
   1174     views::Link* link = new views::Link(
   1175         l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
   1176 
   1177     // Make sure the link width column is as wide as needed for both Show and
   1178     // Hide details, so that the arrow doesn't shift horizontally when we
   1179     // toggle.
   1180     int link_col_width =
   1181         views::kRelatedControlHorizontalSpacing +
   1182         std::max(gfx::GetStringWidth(
   1183                      l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS),
   1184                      link->font_list()),
   1185                  gfx::GetStringWidth(
   1186                      l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS),
   1187                      link->font_list()));
   1188 
   1189     column_set = layout->AddColumnSet(++column_set_id);
   1190     // Padding to the left of the More Details column. If the parent is using
   1191     // bullets for its items, then a padding of one unit will make the child
   1192     // item (which has no bullet) look like a sibling of its parent. Therefore
   1193     // increase the indentation by one more unit to show that it is in fact a
   1194     // child item (with no missing bullet) and not a sibling.
   1195     column_set->AddPaddingColumn(
   1196         0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1));
   1197     // The More Details column.
   1198     column_set->AddColumn(views::GridLayout::LEADING,
   1199                           views::GridLayout::LEADING,
   1200                           0,
   1201                           views::GridLayout::FIXED,
   1202                           link_col_width,
   1203                           link_col_width);
   1204     // The Up/Down arrow column.
   1205     column_set->AddColumn(views::GridLayout::LEADING,
   1206                           views::GridLayout::LEADING,
   1207                           0,
   1208                           views::GridLayout::USE_PREF,
   1209                           0,
   1210                           0);
   1211 
   1212     // Add the More Details link.
   1213     layout->StartRow(0, column_set_id);
   1214     more_details_ = link;
   1215     more_details_->set_listener(this);
   1216     more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
   1217     layout->AddView(more_details_);
   1218 
   1219     // Add the arrow after the More Details link.
   1220     ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
   1221     arrow_toggle_ = new views::ImageButton(this);
   1222     arrow_toggle_->SetImage(views::Button::STATE_NORMAL,
   1223                             rb.GetImageSkiaNamed(IDR_DOWN_ARROW));
   1224     layout->AddView(arrow_toggle_);
   1225   }
   1226 }
   1227 
   1228 ExpandableContainerView::~ExpandableContainerView() {
   1229 }
   1230 
   1231 void ExpandableContainerView::ButtonPressed(
   1232     views::Button* sender, const ui::Event& event) {
   1233   ToggleDetailLevel();
   1234 }
   1235 
   1236 void ExpandableContainerView::LinkClicked(
   1237     views::Link* source, int event_flags) {
   1238   ToggleDetailLevel();
   1239 }
   1240 
   1241 void ExpandableContainerView::AnimationProgressed(
   1242     const gfx::Animation* animation) {
   1243   DCHECK_EQ(&slide_animation_, animation);
   1244   if (details_view_)
   1245     details_view_->AnimateToState(animation->GetCurrentValue());
   1246 }
   1247 
   1248 void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) {
   1249   if (arrow_toggle_) {
   1250     if (animation->GetCurrentValue() != 0.0) {
   1251       arrow_toggle_->SetImage(
   1252           views::Button::STATE_NORMAL,
   1253           ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
   1254               IDR_UP_ARROW));
   1255     } else {
   1256       arrow_toggle_->SetImage(
   1257           views::Button::STATE_NORMAL,
   1258           ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
   1259               IDR_DOWN_ARROW));
   1260     }
   1261   }
   1262   if (more_details_) {
   1263     more_details_->SetText(expanded_ ?
   1264         l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) :
   1265         l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
   1266   }
   1267 }
   1268 
   1269 void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) {
   1270   owner_->ContentsChanged();
   1271 }
   1272 
   1273 void ExpandableContainerView::ToggleDetailLevel() {
   1274   expanded_ = !expanded_;
   1275 
   1276   if (slide_animation_.IsShowing())
   1277     slide_animation_.Hide();
   1278   else
   1279     slide_animation_.Show();
   1280 }
   1281 
   1282 void ExpandableContainerView::ExpandWithoutAnimation() {
   1283   expanded_ = true;
   1284   details_view_->AnimateToState(1.0);
   1285 }
   1286