extension_install_dialog_view.cc revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
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 = 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.
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() const 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() const 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_CURRENTLY_ON(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                     no permissions
441  // +--------------------+------+  +--------------+------+
442  // | heading            | icon |  | heading      | icon |
443  // +--------------------|      |  +--------------+------+
444  // | permissions_header |      |
445  // +--------------------|      |
446  // | permission1        |      |
447  // +--------------------|      |
448  // | permission2        |      |
449  // +--------------------+------+
450  //
451  // If the ExtensionPermissionDialog is on, the layout is modified depending
452  // on the experiment group. For text only experiment, a footer is added at the
453  // bottom of the layouts. For others, inline details are added below some of
454  // the permissions.
455  //
456  // Regular install w/ permissions and footer (experiment):
457  // +--------------------+------+
458  // | heading            | icon |
459  // +--------------------|      |
460  // | permissions_header |      |
461  // +--------------------|      |
462  // | permission1        |      |
463  // +--------------------|      |
464  // | permission2        |      |
465  // +--------------------+------+
466  // | footer text        |      |
467  // +--------------------+------+
468  //
469  // Regular install w/ permissions and inline explanations (experiment):
470  // +--------------------+------+
471  // | heading            | icon |
472  // +--------------------|      |
473  // | permissions_header |      |
474  // +--------------------|      |
475  // | permission1        |      |
476  // +--------------------|      |
477  // | explanation1       |      |
478  // +--------------------|      |
479  // | permission2        |      |
480  // +--------------------|      |
481  // | explanation2       |      |
482  // +--------------------+------+
483  //
484  // Regular install w/ permissions and inline explanations (experiment):
485  // +--------------------+------+
486  // | heading            | icon |
487  // +--------------------|      |
488  // | permissions_header |      |
489  // +--------------------|      |
490  // |checkbox|permission1|      |
491  // +--------------------|      |
492  // |checkbox|permission2|      |
493  // +--------------------+------+
494  //
495  // Additionally, links or informational text is added to non-client areas of
496  // the dialog depending on the experiment group.
497
498  int left_column_width =
499      (prompt.ShouldShowPermissions() +
500       prompt.GetRetainedFileCount()) > 0 ?
501          kPermissionsLeftColumnWidth : kNoPermissionsLeftColumnWidth;
502  if (is_bundle_install())
503    left_column_width = kBundleLeftColumnWidth;
504  if (is_external_install())
505    left_column_width = kExternalInstallLeftColumnWidth;
506
507  scroll_view_ = new views::ScrollView();
508  scroll_view_->set_hide_horizontal_scrollbar(true);
509  AddChildView(scroll_view_);
510
511  int column_set_id = 0;
512  // Create the full scrollable view which will contain all the information
513  // including the permissions.
514  scrollable_ = new CustomScrollableView();
515  views::GridLayout* layout = CreateLayout(
516      scrollable_, left_column_width, column_set_id, false);
517  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
518
519  if (prompt.ShouldShowPermissions() &&
520      prompt.experiment()->should_show_expandable_permission_list()) {
521    // If the experiment should hide the permission list initially, create a
522    // simple layout that contains only the header, extension name and icon.
523    scrollable_header_only_ = new CustomScrollableView();
524    CreateLayout(scrollable_header_only_, left_column_width,
525                 column_set_id, true);
526    scroll_view_->SetContents(scrollable_header_only_);
527  } else {
528    scroll_view_->SetContents(scrollable_);
529  }
530
531  int dialog_width = left_column_width + 2 * views::kPanelHorizMargin;
532  if (!is_bundle_install())
533    dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset;
534
535  // Widen the dialog for experiment with checkboxes so that the information
536  // label fits the area to the left of the buttons.
537  if (prompt.experiment()->show_checkboxes())
538    dialog_width += 4 * views::kPanelHorizMargin;
539
540  if (prompt.has_webstore_data()) {
541    layout->StartRow(0, column_set_id);
542    views::View* rating = new views::View();
543    rating->SetLayoutManager(new views::BoxLayout(
544        views::BoxLayout::kHorizontal, 0, 0, 0));
545    layout->AddView(rating);
546    prompt.AppendRatingStars(AddResourceIcon, rating);
547
548    const gfx::FontList& small_font_list =
549        rb.GetFontList(ui::ResourceBundle::SmallFont);
550    views::Label* rating_count =
551        new views::Label(prompt.GetRatingCount(), small_font_list);
552    // Add some space between the stars and the rating count.
553    rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0));
554    rating->AddChildView(rating_count);
555
556    layout->StartRow(0, column_set_id);
557    views::Label* user_count =
558        new views::Label(prompt.GetUserCount(), small_font_list);
559    user_count->SetAutoColorReadabilityEnabled(false);
560    user_count->SetEnabledColor(SK_ColorGRAY);
561    layout->AddView(user_count);
562
563    layout->StartRow(0, column_set_id);
564    views::Link* store_link = new views::Link(
565        l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK));
566    store_link->SetFontList(small_font_list);
567    store_link->set_listener(this);
568    layout->AddView(store_link);
569  }
570
571  if (is_bundle_install()) {
572    BundleInstaller::ItemList items = prompt.bundle()->GetItemsWithState(
573        BundleInstaller::Item::STATE_PENDING);
574    for (size_t i = 0; i < items.size(); ++i) {
575      base::string16 extension_name =
576          base::UTF8ToUTF16(items[i].localized_name);
577      base::i18n::AdjustStringForLocaleDirection(&extension_name);
578      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
579      layout->StartRow(0, column_set_id);
580      views::Label* extension_label = new views::Label(
581          PrepareForDisplay(extension_name, true));
582      extension_label->SetMultiLine(true);
583      extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
584      extension_label->SizeToFit(left_column_width);
585      layout->AddView(extension_label);
586    }
587  }
588
589  if (prompt.ShouldShowPermissions()) {
590    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
591
592    if (prompt.GetPermissionCount() > 0) {
593      if (is_inline_install()) {
594        layout->StartRow(0, column_set_id);
595        layout->AddView(new views::Separator(views::Separator::HORIZONTAL),
596                        3, 1, views::GridLayout::FILL, views::GridLayout::FILL);
597        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
598      }
599
600      layout->StartRow(0, column_set_id);
601      views::Label* permissions_header = NULL;
602      if (is_bundle_install()) {
603        // We need to pass the FontList in the constructor, rather than calling
604        // SetFontList later, because otherwise SizeToFit mis-judges the width
605        // of the line.
606        permissions_header = new views::Label(
607            prompt.GetPermissionsHeading(),
608            rb.GetFontList(ui::ResourceBundle::MediumFont));
609      } else {
610        permissions_header = new views::Label(prompt.GetPermissionsHeading());
611      }
612      permissions_header->SetMultiLine(true);
613      permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
614      permissions_header->SizeToFit(left_column_width);
615      layout->AddView(permissions_header);
616
617      for (size_t i = 0; i < prompt.GetPermissionCount(); ++i) {
618        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
619        layout->StartRow(0, column_set_id);
620        views::Label* permission_label =
621            new views::Label(prompt.GetPermission(i));
622
623        const SkColor kTextHighlight = SK_ColorRED;
624        const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3);
625        if (prompt.experiment()->ShouldHighlightText(
626            prompt.GetPermission(i))) {
627          permission_label->SetAutoColorReadabilityEnabled(false);
628          permission_label->SetEnabledColor(kTextHighlight);
629        } else if (prompt.experiment()->ShouldHighlightBackground(
630            prompt.GetPermission(i))) {
631          permission_label->SetLineHeight(18);
632          permission_label->set_background(
633              views::Background::CreateSolidBackground(kBackgroundHighlight));
634        }
635
636        permission_label->SetMultiLine(true);
637        permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
638        permission_label->SizeToFit(left_column_width);
639
640        if (prompt.experiment()->show_checkboxes()) {
641          layout->AddView(new CheckboxedView(permission_label, this));
642          ++unchecked_boxes_;
643        } else {
644          layout->AddView(new BulletedView(permission_label));
645        }
646        // If we have more details to provide, show them in collapsed form.
647        if (!prompt.GetPermissionsDetails(i).empty()) {
648          layout->StartRow(0, column_set_id);
649          PermissionDetails details;
650          details.push_back(
651              PrepareForDisplay(prompt.GetPermissionsDetails(i), false));
652          ExpandableContainerView* details_container =
653              new ExpandableContainerView(
654                  this, base::string16(), details, left_column_width,
655                  true, true, false);
656          layout->AddView(details_container);
657        }
658
659        if (prompt.experiment()->should_show_inline_explanations()) {
660          base::string16 explanation =
661              prompt.experiment()->GetInlineExplanation(
662                  prompt.GetPermission(i));
663          if (!explanation.empty()) {
664            PermissionDetails details;
665            details.push_back(explanation);
666            ExpandableContainerView* container =
667                new ExpandableContainerView(this, base::string16(), details,
668                                            left_column_width,
669                                            false, false, true);
670            // Inline explanations are expanded by default if there is
671            // no "Show details" link.
672            if (!prompt.experiment()->show_details_link())
673              container->ExpandWithoutAnimation();
674            layout->StartRow(0, column_set_id);
675            layout->AddView(container);
676            inline_explanations_.push_back(container);
677          }
678        }
679      }
680    } else {
681      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
682      layout->StartRow(0, column_set_id);
683      views::Label* permission_label = new views::Label(
684          l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS));
685      permission_label->SetMultiLine(true);
686      permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
687      permission_label->SizeToFit(left_column_width);
688      layout->AddView(permission_label);
689    }
690  }
691
692  if (prompt.GetRetainedFileCount()) {
693    // Slide in under the permissions, if there are any. If there are
694    // either, the retained files prompt stretches all the way to the
695    // right of the dialog. If there are no permissions, the retained
696    // files prompt just takes up the left column.
697    int space_for_files = left_column_width;
698    if (prompt.GetPermissionCount()) {
699      space_for_files += kIconSize;
700      views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
701      column_set->AddColumn(views::GridLayout::FILL,
702                            views::GridLayout::FILL,
703                            1,
704                            views::GridLayout::USE_PREF,
705                            0,  // no fixed width
706                            space_for_files);
707    }
708
709    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
710
711    layout->StartRow(0, column_set_id);
712    views::Label* retained_files_header = NULL;
713    retained_files_header =
714        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 = new views::Label(
750          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
804ExtensionInstallDialogView::~ExtensionInstallDialogView() {}
805
806views::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
876void ExtensionInstallDialogView::ContentsChanged() {
877  Layout();
878}
879
880void 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
902int 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
910base::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
925int ExtensionInstallDialogView::GetDefaultDialogButton() const {
926  return ui::DIALOG_BUTTON_CANCEL;
927}
928
929bool ExtensionInstallDialogView::Cancel() {
930  UpdateInstallResultHistogram(false);
931  delegate_->InstallUIAbort(true);
932  return true;
933}
934
935bool ExtensionInstallDialogView::Accept() {
936  UpdateInstallResultHistogram(true);
937  delegate_->InstallUIProceed();
938  return true;
939}
940
941ui::ModalType ExtensionInstallDialogView::GetModalType() const {
942  return ui::MODAL_TYPE_WINDOW;
943}
944
945base::string16 ExtensionInstallDialogView::GetWindowTitle() const {
946  return prompt_.GetDialogTitle();
947}
948
949void 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
981void ExtensionInstallDialogView::ToggleInlineExplanations() {
982  for (InlineExplanations::iterator it = inline_explanations_.begin();
983      it != inline_explanations_.end(); ++it)
984    (*it)->ToggleDetailLevel();
985}
986
987void 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
1026gfx::Size ExtensionInstallDialogView::GetPreferredSize() const {
1027  return dialog_size_;
1028}
1029
1030void 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
1044void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted)
1045    const {
1046  if (prompt_.type() == ExtensionInstallPrompt::INSTALL_PROMPT)
1047    UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted);
1048}
1049
1050void 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
1068ExtensionInstallPrompt::ShowDialogCallback
1069ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
1070  return base::Bind(&ShowExtensionInstallDialogImpl);
1071}
1072
1073// ExpandableContainerView::DetailsView ----------------------------------------
1074
1075ExpandableContainerView::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
1098void 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
1113gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const {
1114  gfx::Size size = views::View::GetPreferredSize();
1115  return gfx::Size(size.width(), size.height() * state_);
1116}
1117
1118void ExpandableContainerView::DetailsView::AnimateToState(double state) {
1119  state_ = state;
1120  PreferredSizeChanged();
1121  SchedulePaint();
1122}
1123
1124// ExpandableContainerView -----------------------------------------------------
1125
1126ExpandableContainerView::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
1228ExpandableContainerView::~ExpandableContainerView() {
1229}
1230
1231void ExpandableContainerView::ButtonPressed(
1232    views::Button* sender, const ui::Event& event) {
1233  ToggleDetailLevel();
1234}
1235
1236void ExpandableContainerView::LinkClicked(
1237    views::Link* source, int event_flags) {
1238  ToggleDetailLevel();
1239}
1240
1241void 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
1248void 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
1269void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) {
1270  owner_->ContentsChanged();
1271}
1272
1273void ExpandableContainerView::ToggleDetailLevel() {
1274  expanded_ = !expanded_;
1275
1276  if (slide_animation_.IsShowing())
1277    slide_animation_.Hide();
1278  else
1279    slide_animation_.Show();
1280}
1281
1282void ExpandableContainerView::ExpandWithoutAnimation() {
1283  expanded_ = true;
1284  details_view_->AnimateToState(1.0);
1285}
1286