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