extension_install_dialog_view.cc revision 1e9bf3e0803691d0a228da41fc608347b6db4340
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/strings/string_util.h"
12#include "base/strings/utf_string_conversions.h"
13#include "chrome/browser/extensions/bundle_installer.h"
14#include "chrome/browser/extensions/extension_install_prompt.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/ui/views/constrained_window_views.h"
17#include "chrome/common/chrome_switches.h"
18#include "chrome/common/extensions/extension.h"
19#include "chrome/common/extensions/extension_constants.h"
20#include "chrome/installer/util/browser_distribution.h"
21#include "content/public/browser/page_navigator.h"
22#include "content/public/browser/web_contents.h"
23#include "grit/chromium_strings.h"
24#include "grit/generated_resources.h"
25#include "grit/google_chrome_strings.h"
26#include "grit/theme_resources.h"
27#include "ui/base/l10n/l10n_util.h"
28#include "ui/base/resource/resource_bundle.h"
29#include "ui/gfx/animation/animation_delegate.h"
30#include "ui/gfx/animation/slide_animation.h"
31#include "ui/gfx/transform.h"
32#include "ui/views/border.h"
33#include "ui/views/controls/button/image_button.h"
34#include "ui/views/controls/image_view.h"
35#include "ui/views/controls/label.h"
36#include "ui/views/controls/link.h"
37#include "ui/views/controls/link_listener.h"
38#include "ui/views/controls/scroll_view.h"
39#include "ui/views/controls/separator.h"
40#include "ui/views/layout/box_layout.h"
41#include "ui/views/layout/grid_layout.h"
42#include "ui/views/layout/layout_constants.h"
43#include "ui/views/view.h"
44#include "ui/views/widget/widget.h"
45#include "ui/views/window/dialog_delegate.h"
46
47using content::OpenURLParams;
48using content::Referrer;
49using extensions::BundleInstaller;
50
51namespace {
52
53// Size of extension icon in top left of dialog.
54const int kIconSize = 69;
55
56// We offset the icon a little bit from the right edge of the dialog, to make it
57// align with the button below it.
58const int kIconOffset = 16;
59
60// The dialog will resize based on its content, but this sets a maximum height
61// before overflowing a scrollbar.
62const int kDialogMaxHeight = 300;
63
64// Width of the left column of the dialog when the extension requests
65// permissions.
66const int kPermissionsLeftColumnWidth = 250;
67
68// Width of the left column of the dialog when the extension requests no
69// permissions.
70const int kNoPermissionsLeftColumnWidth = 200;
71
72// Width of the left column for bundle install prompts. There's only one column
73// in this case, so make it wider than normal.
74const int kBundleLeftColumnWidth = 300;
75
76// Width of the left column for external install prompts. The text is long in
77// this case, so make it wider than normal.
78const int kExternalInstallLeftColumnWidth = 350;
79
80typedef std::vector<string16> PermissionDetails;
81
82void AddResourceIcon(const gfx::ImageSkia* skia_image, void* data) {
83  views::View* parent = static_cast<views::View*>(data);
84  views::ImageView* image_view = new views::ImageView();
85  image_view->SetImage(*skia_image);
86  parent->AddChildView(image_view);
87}
88
89// Creates a string for displaying |message| to the user. If it has to look
90// like a entry in a bullet point list, one is added.
91string16 PrepareForDisplay(const string16& message, bool bullet_point) {
92  return bullet_point ? l10n_util::GetStringFUTF16(
93      IDS_EXTENSION_PERMISSION_LINE,
94      message) : message;
95}
96
97// A custom scrollable view implementation for the dialog.
98class CustomScrollableView : public views::View {
99 public:
100  CustomScrollableView();
101  virtual ~CustomScrollableView();
102
103 private:
104  virtual void Layout() OVERRIDE;
105
106  DISALLOW_COPY_AND_ASSIGN(CustomScrollableView);
107};
108
109// Implements the extension installation dialog for TOOLKIT_VIEWS.
110class ExtensionInstallDialogView : public views::DialogDelegateView,
111                                   public views::LinkListener {
112 public:
113  ExtensionInstallDialogView(content::PageNavigator* navigator,
114                             ExtensionInstallPrompt::Delegate* delegate,
115                             const ExtensionInstallPrompt::Prompt& prompt);
116  virtual ~ExtensionInstallDialogView();
117
118  // Called when one of the child elements has expanded/collapsed.
119  void ContentsChanged();
120
121 private:
122  // views::DialogDelegateView:
123  virtual int GetDialogButtons() const OVERRIDE;
124  virtual string16 GetDialogButtonLabel(ui::DialogButton button) const OVERRIDE;
125  virtual int GetDefaultDialogButton() const OVERRIDE;
126  virtual bool Cancel() OVERRIDE;
127  virtual bool Accept() OVERRIDE;
128  virtual ui::ModalType GetModalType() const OVERRIDE;
129  virtual string16 GetWindowTitle() const OVERRIDE;
130  virtual void Layout() OVERRIDE;
131  virtual gfx::Size GetPreferredSize() OVERRIDE;
132
133  // views::LinkListener:
134  virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE;
135
136  bool is_inline_install() const {
137    return prompt_.type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT;
138  }
139
140  bool is_bundle_install() const {
141    return prompt_.type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT;
142  }
143
144  bool is_external_install() const {
145    return prompt_.type() == ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT;
146  }
147
148  content::PageNavigator* navigator_;
149  ExtensionInstallPrompt::Delegate* delegate_;
150  ExtensionInstallPrompt::Prompt prompt_;
151
152  // The scroll view containing all the details for the dialog (including all
153  // collapsible/expandable sections).
154  views::ScrollView* scroll_view_;
155
156  // The container view for the scroll view.
157  CustomScrollableView* scrollable_;
158
159  // The preferred size of the dialog.
160  gfx::Size dialog_size_;
161
162  DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogView);
163};
164
165// A simple view that prepends a view with a bullet with the help of a grid
166// layout.
167class BulletedView : public views::View {
168 public:
169  explicit BulletedView(views::View* view);
170 private:
171  DISALLOW_COPY_AND_ASSIGN(BulletedView);
172};
173
174BulletedView::BulletedView(views::View* view) {
175  views::GridLayout* layout = new views::GridLayout(this);
176  SetLayoutManager(layout);
177  views::ColumnSet* column_set = layout->AddColumnSet(0);
178  column_set->AddColumn(views::GridLayout::LEADING,
179                        views::GridLayout::LEADING,
180                        0,
181                        views::GridLayout::USE_PREF,
182                        0, // no fixed width
183                        0);
184   column_set->AddColumn(views::GridLayout::LEADING,
185                         views::GridLayout::LEADING,
186                         0,
187                         views::GridLayout::USE_PREF,
188                         0,  // no fixed width
189                         0);
190  layout->StartRow(0, 0);
191  layout->AddView(new views::Label(PrepareForDisplay(string16(), true)));
192  layout->AddView(view);
193}
194
195// A view to display text with an expandable details section.
196class ExpandableContainerView : public views::View,
197                                public views::ButtonListener,
198                                public views::LinkListener,
199                                public gfx::AnimationDelegate {
200 public:
201  ExpandableContainerView(ExtensionInstallDialogView* owner,
202                          const string16& description,
203                          const PermissionDetails& details,
204                          int horizontal_space,
205                          bool parent_bulleted);
206  virtual ~ExpandableContainerView();
207
208  // views::View:
209  virtual void ChildPreferredSizeChanged(views::View* child) OVERRIDE;
210
211  // views::ButtonListener:
212  virtual void ButtonPressed(views::Button* sender,
213                             const ui::Event& event) OVERRIDE;
214
215  // views::LinkListener:
216  virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE;
217
218  // gfx::AnimationDelegate:
219  virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE;
220  virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE;
221
222 private:
223  // A view which displays all the details of an IssueAdviceInfoEntry.
224  class DetailsView : public views::View {
225   public:
226    explicit DetailsView(int horizontal_space, bool parent_bulleted);
227    virtual ~DetailsView() {}
228
229    // views::View:
230    virtual gfx::Size GetPreferredSize() OVERRIDE;
231
232    void AddDetail(const string16& detail);
233
234    // Animates this to be a height proportional to |state|.
235    void AnimateToState(double state);
236
237   private:
238    views::GridLayout* layout_;
239    double state_;
240
241    // Whether the parent item is showing bullets. This will determine how much
242    // extra indentation is needed.
243    bool parent_bulleted_;
244
245    DISALLOW_COPY_AND_ASSIGN(DetailsView);
246  };
247
248  // Expand/Collapse the detail section for this ExpandableContainerView.
249  void ToggleDetailLevel();
250
251  // The dialog that owns |this|. It's also an ancestor in the View hierarchy.
252  ExtensionInstallDialogView* owner_;
253
254  // A view for showing |issue_advice.details|.
255  DetailsView* details_view_;
256
257  // The '>' zippy control.
258  views::ImageView* arrow_view_;
259
260  gfx::SlideAnimation slide_animation_;
261
262  // The 'more details' link shown under the heading (changes to 'hide details'
263  // when the details section is expanded).
264  views::Link* more_details_;
265
266  // The up/down arrow next to the 'more detail' link (points up/down depending
267  // on whether the details section is expanded).
268  views::ImageButton* arrow_toggle_;
269
270  // Whether the details section is expanded.
271  bool expanded_;
272
273  DISALLOW_COPY_AND_ASSIGN(ExpandableContainerView);
274};
275
276void ShowExtensionInstallDialogImpl(
277    const ExtensionInstallPrompt::ShowParams& show_params,
278    ExtensionInstallPrompt::Delegate* delegate,
279    const ExtensionInstallPrompt::Prompt& prompt) {
280  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
281  CreateBrowserModalDialogViews(
282      new ExtensionInstallDialogView(show_params.navigator, delegate, prompt),
283      show_params.parent_window)->Show();
284}
285
286}  // namespace
287
288CustomScrollableView::CustomScrollableView() {}
289CustomScrollableView::~CustomScrollableView() {}
290
291void CustomScrollableView::Layout() {
292  SetBounds(x(), y(), width(), GetHeightForWidth(width()));
293  views::View::Layout();
294}
295
296ExtensionInstallDialogView::ExtensionInstallDialogView(
297    content::PageNavigator* navigator,
298    ExtensionInstallPrompt::Delegate* delegate,
299    const ExtensionInstallPrompt::Prompt& prompt)
300    : navigator_(navigator),
301      delegate_(delegate),
302      prompt_(prompt) {
303  // Possible grid layouts:
304  // Inline install
305  //      w/ permissions                 no permissions
306  // +--------------------+------+  +--------------+------+
307  // | heading            | icon |  | heading      | icon |
308  // +--------------------|      |  +--------------|      |
309  // | rating             |      |  | rating       |      |
310  // +--------------------|      |  +--------------+      |
311  // | user_count         |      |  | user_count   |      |
312  // +--------------------|      |  +--------------|      |
313  // | store_link         |      |  | store_link   |      |
314  // +--------------------+------+  +--------------+------+
315  // |      separator            |
316  // +--------------------+------+
317  // | permissions_header |      |
318  // +--------------------+------+
319  // | permission1        |      |
320  // +--------------------+------+
321  // | permission2        |      |
322  // +--------------------+------+
323  //
324  // Regular install
325  // w/ permissions XOR oauth issues    no permissions
326  // +--------------------+------+  +--------------+------+
327  // | heading            | icon |  | heading      | icon |
328  // +--------------------|      |  +--------------+------+
329  // | permissions_header |      |
330  // +--------------------|      |
331  // | permission1        |      |
332  // +--------------------|      |
333  // | permission2        |      |
334  // +--------------------+------+
335  //
336  // w/ permissions AND oauth issues
337  // +--------------------+------+
338  // | heading            | icon |
339  // +--------------------|      |
340  // | permissions_header |      |
341  // +--------------------|      |
342  // | permission1        |      |
343  // +--------------------|      |
344  // | permission2        |      |
345  // +--------------------+------+
346  // | oauth header              |
347  // +---------------------------+
348  // | oauth issue 1             |
349  // +---------------------------+
350  // | oauth issue 2             |
351  // +---------------------------+
352
353  scroll_view_ = new views::ScrollView();
354  scroll_view_->set_hide_horizontal_scrollbar(true);
355  AddChildView(scroll_view_);
356  scrollable_ = new CustomScrollableView();
357  scroll_view_->SetContents(scrollable_);
358
359  views::GridLayout* layout = views::GridLayout::CreatePanel(scrollable_);
360  scrollable_->SetLayoutManager(layout);
361
362  int column_set_id = 0;
363  views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
364  int left_column_width =
365      (prompt.ShouldShowPermissions() + prompt.GetOAuthIssueCount() +
366       prompt.GetRetainedFileCount()) > 0 ?
367          kPermissionsLeftColumnWidth : kNoPermissionsLeftColumnWidth;
368  if (is_bundle_install())
369    left_column_width = kBundleLeftColumnWidth;
370  if (is_external_install())
371    left_column_width = kExternalInstallLeftColumnWidth;
372
373  int dialog_width = left_column_width + 2 * views::kPanelHorizMargin;
374
375  column_set->AddColumn(views::GridLayout::LEADING,
376                        views::GridLayout::FILL,
377                        0,  // no resizing
378                        views::GridLayout::USE_PREF,
379                        0,  // no fixed width
380                        left_column_width);
381  if (!is_bundle_install()) {
382    column_set->AddPaddingColumn(0, views::kPanelHorizMargin);
383    column_set->AddColumn(views::GridLayout::TRAILING,
384                          views::GridLayout::LEADING,
385                          0,  // no resizing
386                          views::GridLayout::USE_PREF,
387                          0,  // no fixed width
388                          kIconSize);
389
390    dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset;
391  }
392
393  layout->StartRow(0, column_set_id);
394
395  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
396
397  views::Label* heading = new views::Label(prompt.GetHeading());
398  heading->SetFont(rb.GetFont(ui::ResourceBundle::MediumFont));
399  heading->SetMultiLine(true);
400  heading->SetHorizontalAlignment(gfx::ALIGN_LEFT);
401  heading->SizeToFit(left_column_width);
402  layout->AddView(heading);
403
404  if (!is_bundle_install()) {
405    // Scale down to icon size, but allow smaller icons (don't scale up).
406    const gfx::ImageSkia* image = prompt.icon().ToImageSkia();
407    gfx::Size size(image->width(), image->height());
408    if (size.width() > kIconSize || size.height() > kIconSize)
409      size = gfx::Size(kIconSize, kIconSize);
410    views::ImageView* icon = new views::ImageView();
411    icon->SetImageSize(size);
412    icon->SetImage(*image);
413    icon->SetHorizontalAlignment(views::ImageView::CENTER);
414    icon->SetVerticalAlignment(views::ImageView::CENTER);
415    int icon_row_span = 1;
416    if (is_inline_install()) {
417      // Also span the rating, user_count and store_link rows.
418      icon_row_span = 4;
419    } else if (prompt.ShouldShowPermissions()) {
420      size_t permission_count = prompt.GetPermissionCount();
421      // Also span the permission header and each of the permission rows (all
422      // have a padding row above it). This also works for the 'no special
423      // permissions' case.
424      icon_row_span = 3 + permission_count * 2;
425    } else if (prompt.GetOAuthIssueCount()) {
426      // Also span the permission header and each of the permission rows (all
427      // have a padding row above it).
428      icon_row_span = 3 + prompt.GetOAuthIssueCount() * 2;
429    } else if (prompt.GetRetainedFileCount()) {
430      // Also span the permission header and the retained files container.
431      icon_row_span = 4;
432    }
433    layout->AddView(icon, 1, icon_row_span);
434  }
435
436  if (is_inline_install()) {
437    layout->StartRow(0, column_set_id);
438    views::View* rating = new views::View();
439    rating->SetLayoutManager(new views::BoxLayout(
440        views::BoxLayout::kHorizontal, 0, 0, 0));
441    layout->AddView(rating);
442    prompt.AppendRatingStars(AddResourceIcon, rating);
443
444    views::Label* rating_count = new views::Label(prompt.GetRatingCount());
445    rating_count->SetFont(rb.GetFont(ui::ResourceBundle::SmallFont));
446    // Add some space between the stars and the rating count.
447    rating_count->set_border(views::Border::CreateEmptyBorder(0, 2, 0, 0));
448    rating->AddChildView(rating_count);
449
450    layout->StartRow(0, column_set_id);
451    views::Label* user_count = new views::Label(prompt.GetUserCount());
452    user_count->SetAutoColorReadabilityEnabled(false);
453    user_count->SetEnabledColor(SK_ColorGRAY);
454    user_count->SetFont(rb.GetFont(ui::ResourceBundle::SmallFont));
455    layout->AddView(user_count);
456
457    layout->StartRow(0, column_set_id);
458    views::Link* store_link = new views::Link(
459        l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK));
460    store_link->SetFont(rb.GetFont(ui::ResourceBundle::SmallFont));
461    store_link->set_listener(this);
462    layout->AddView(store_link);
463  }
464
465  if (is_bundle_install()) {
466    BundleInstaller::ItemList items = prompt.bundle()->GetItemsWithState(
467        BundleInstaller::Item::STATE_PENDING);
468    for (size_t i = 0; i < items.size(); ++i) {
469      string16 extension_name = UTF8ToUTF16(items[i].localized_name);
470      base::i18n::AdjustStringForLocaleDirection(&extension_name);
471      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
472      layout->StartRow(0, column_set_id);
473      views::Label* extension_label = new views::Label(
474          PrepareForDisplay(extension_name, true));
475      extension_label->SetMultiLine(true);
476      extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
477      extension_label->SizeToFit(left_column_width);
478      layout->AddView(extension_label);
479    }
480  }
481
482  if (prompt.ShouldShowPermissions()) {
483    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
484
485    if (prompt.GetPermissionCount() > 0) {
486      if (is_inline_install()) {
487        layout->StartRow(0, column_set_id);
488        layout->AddView(new views::Separator(views::Separator::HORIZONTAL),
489                        3, 1, views::GridLayout::FILL, views::GridLayout::FILL);
490        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
491      }
492
493      layout->StartRow(0, column_set_id);
494      views::Label* permissions_header = NULL;
495      if (is_bundle_install()) {
496        // We need to pass the Font in the constructor, rather than calling
497        // SetFont later, because otherwise SizeToFit mis-judges the width
498        // of the line.
499        permissions_header = new views::Label(
500            prompt.GetPermissionsHeading(),
501            rb.GetFont(ui::ResourceBundle::MediumFont));
502      } else {
503        permissions_header = new views::Label(prompt.GetPermissionsHeading());
504      }
505      permissions_header->SetMultiLine(true);
506      permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
507      permissions_header->SizeToFit(left_column_width);
508      layout->AddView(permissions_header);
509
510      for (size_t i = 0; i < prompt.GetPermissionCount(); ++i) {
511        layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
512        layout->StartRow(0, column_set_id);
513        views::Label* permission_label =
514            new views::Label(prompt.GetPermission(i));
515        permission_label->SetMultiLine(true);
516        permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
517        permission_label->SizeToFit(left_column_width);
518        layout->AddView(new BulletedView(permission_label));
519
520        // If we have more details to provide, show them in collapsed form.
521        if (!prompt.GetPermissionsDetails(i).empty()) {
522          layout->StartRow(0, column_set_id);
523          PermissionDetails details;
524          details.push_back(
525              PrepareForDisplay(prompt.GetPermissionsDetails(i), false));
526          ExpandableContainerView* details_container =
527              new ExpandableContainerView(
528                  this, string16(), details, left_column_width, true);
529          layout->AddView(details_container);
530        }
531      }
532    } else {
533      layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
534      layout->StartRow(0, column_set_id);
535      views::Label* permission_label = new views::Label(
536          l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS));
537      permission_label->SetMultiLine(true);
538      permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
539      permission_label->SizeToFit(left_column_width);
540      layout->AddView(permission_label);
541    }
542  }
543
544  if (prompt.GetOAuthIssueCount()) {
545    // Slide in under the permissions, if there are any. If there are
546    // permissions, the OAuth prompt stretches all the way to the right of the
547    // dialog. If there are no permissions, the OAuth prompt just takes up the
548    // left column.
549    int space_for_oauth = left_column_width;
550    if (prompt.GetPermissionCount()) {
551      space_for_oauth += kIconSize;
552      column_set = layout->AddColumnSet(++column_set_id);
553      column_set->AddColumn(views::GridLayout::FILL,
554                            views::GridLayout::FILL,
555                            1,
556                            views::GridLayout::USE_PREF,
557                            0,  // no fixed width
558                            space_for_oauth);
559    }
560
561    layout->StartRowWithPadding(0, column_set_id,
562                                0, views::kRelatedControlVerticalSpacing);
563    views::Label* oauth_header = new views::Label(prompt.GetOAuthHeading());
564    oauth_header->SetMultiLine(true);
565    oauth_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
566    oauth_header->SizeToFit(left_column_width);
567    layout->AddView(oauth_header);
568
569    for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) {
570      layout->StartRowWithPadding(
571          0, column_set_id,
572          0, views::kRelatedControlVerticalSpacing);
573
574      PermissionDetails details;
575      const IssueAdviceInfoEntry& entry = prompt.GetOAuthIssue(i);
576      for (size_t x = 0; x < entry.details.size(); ++x)
577        details.push_back(entry.details[x]);
578      ExpandableContainerView* issue_advice_view =
579          new ExpandableContainerView(
580              this, entry.description, details, space_for_oauth, true);
581      layout->AddView(issue_advice_view);
582    }
583  }
584  if (prompt.GetRetainedFileCount()) {
585    // Slide in under the permissions or OAuth, if there are any. If there are
586    // either, the retained files prompt stretches all the way to the right of
587    // the dialog. If there are no permissions or OAuth, the retained files
588    // prompt just takes up the left column.
589    int space_for_files = left_column_width;
590    if (prompt.GetPermissionCount() || prompt.GetOAuthIssueCount()) {
591      space_for_files += kIconSize;
592      column_set = layout->AddColumnSet(++column_set_id);
593      column_set->AddColumn(views::GridLayout::FILL,
594                            views::GridLayout::FILL,
595                            1,
596                            views::GridLayout::USE_PREF,
597                            0,  // no fixed width
598                            space_for_files);
599    }
600
601    layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
602
603    layout->StartRow(0, column_set_id);
604    views::Label* retained_files_header = NULL;
605    retained_files_header =
606        new views::Label(prompt.GetRetainedFilesHeading());
607    retained_files_header->SetMultiLine(true);
608    retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
609    retained_files_header->SizeToFit(space_for_files);
610    layout->AddView(retained_files_header);
611
612    layout->StartRow(0, column_set_id);
613    PermissionDetails details;
614    for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i)
615      details.push_back(prompt.GetRetainedFile(i));
616    ExpandableContainerView* issue_advice_view =
617        new ExpandableContainerView(
618            this, string16(), details, space_for_files, false);
619    layout->AddView(issue_advice_view);
620  }
621
622  gfx::Size scrollable_size = scrollable_->GetPreferredSize();
623  scrollable_->SetBoundsRect(gfx::Rect(scrollable_size));
624  dialog_size_ = gfx::Size(
625      dialog_width,
626      std::min(scrollable_size.height(), kDialogMaxHeight));
627}
628
629ExtensionInstallDialogView::~ExtensionInstallDialogView() {}
630
631void ExtensionInstallDialogView::ContentsChanged() {
632  Layout();
633}
634
635int ExtensionInstallDialogView::GetDialogButtons() const {
636  int buttons = prompt_.GetDialogButtons();
637  // Simply having just an OK button is *not* supported. See comment on function
638  // GetDialogButtons in dialog_delegate.h for reasons.
639  DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0);
640  return buttons;
641}
642
643string16 ExtensionInstallDialogView::GetDialogButtonLabel(
644    ui::DialogButton button) const {
645  switch (button) {
646    case ui::DIALOG_BUTTON_OK:
647      return prompt_.GetAcceptButtonLabel();
648    case ui::DIALOG_BUTTON_CANCEL:
649      return prompt_.HasAbortButtonLabel() ?
650          prompt_.GetAbortButtonLabel() :
651          l10n_util::GetStringUTF16(IDS_CANCEL);
652    default:
653      NOTREACHED();
654      return string16();
655  }
656}
657
658int ExtensionInstallDialogView::GetDefaultDialogButton() const {
659  return ui::DIALOG_BUTTON_CANCEL;
660}
661
662bool ExtensionInstallDialogView::Cancel() {
663  delegate_->InstallUIAbort(true);
664  return true;
665}
666
667bool ExtensionInstallDialogView::Accept() {
668  delegate_->InstallUIProceed();
669  return true;
670}
671
672ui::ModalType ExtensionInstallDialogView::GetModalType() const {
673  return ui::MODAL_TYPE_WINDOW;
674}
675
676string16 ExtensionInstallDialogView::GetWindowTitle() const {
677  return prompt_.GetDialogTitle();
678}
679
680void ExtensionInstallDialogView::LinkClicked(views::Link* source,
681                                             int event_flags) {
682  GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
683                 prompt_.extension()->id());
684  OpenURLParams params(
685      store_url, Referrer(), NEW_FOREGROUND_TAB, content::PAGE_TRANSITION_LINK,
686      false);
687  navigator_->OpenURL(params);
688  GetWidget()->Close();
689}
690
691void ExtensionInstallDialogView::Layout() {
692  scroll_view_->SetBounds(0, 0, width(), height());
693  DialogDelegateView::Layout();
694}
695
696gfx::Size ExtensionInstallDialogView::GetPreferredSize() {
697  return dialog_size_;
698}
699
700// static
701ExtensionInstallPrompt::ShowDialogCallback
702ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
703  return base::Bind(&ShowExtensionInstallDialogImpl);
704}
705
706// ExpandableContainerView::DetailsView ----------------------------------------
707
708ExpandableContainerView::DetailsView::DetailsView(int horizontal_space,
709                                                  bool parent_bulleted)
710    : layout_(new views::GridLayout(this)),
711      state_(0),
712      parent_bulleted_(parent_bulleted) {
713  SetLayoutManager(layout_);
714  views::ColumnSet* column_set = layout_->AddColumnSet(0);
715  // If the parent is using bullets for its items, then a padding of one unit
716  // will make the child item (which has no bullet) look like a sibling of its
717  // parent. Therefore increase the indentation by one more unit to show that it
718  // is in fact a child item (with no missing bullet) and not a sibling.
719  int padding =
720      views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1);
721  column_set->AddPaddingColumn(0, padding);
722  column_set->AddColumn(views::GridLayout::LEADING,
723                        views::GridLayout::LEADING,
724                        0,
725                        views::GridLayout::FIXED,
726                        horizontal_space - padding,
727                        0);
728}
729
730void ExpandableContainerView::DetailsView::AddDetail(const string16& detail) {
731  layout_->StartRowWithPadding(0, 0,
732                               0, views::kRelatedControlSmallVerticalSpacing);
733  views::Label* detail_label =
734      new views::Label(PrepareForDisplay(detail, false));
735  detail_label->SetMultiLine(true);
736  detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
737  layout_->AddView(detail_label);
738}
739
740gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() {
741  gfx::Size size = views::View::GetPreferredSize();
742  return gfx::Size(size.width(), size.height() * state_);
743}
744
745void ExpandableContainerView::DetailsView::AnimateToState(double state) {
746  state_ = state;
747  PreferredSizeChanged();
748  SchedulePaint();
749}
750
751// ExpandableContainerView -----------------------------------------------------
752
753ExpandableContainerView::ExpandableContainerView(
754    ExtensionInstallDialogView* owner,
755    const string16& description,
756    const PermissionDetails& details,
757    int horizontal_space,
758    bool parent_bulleted)
759    : owner_(owner),
760      details_view_(NULL),
761      arrow_view_(NULL),
762      slide_animation_(this),
763      expanded_(false) {
764  views::GridLayout* layout = new views::GridLayout(this);
765  SetLayoutManager(layout);
766  int column_set_id = 0;
767  views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
768  column_set->AddColumn(views::GridLayout::LEADING,
769                        views::GridLayout::LEADING,
770                        0,
771                        views::GridLayout::USE_PREF,
772                        0,
773                        0);
774  if (!description.empty()) {
775    layout->StartRow(0, column_set_id);
776
777    views::Label* description_label = new views::Label(description);
778    description_label->SetMultiLine(true);
779    description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
780    description_label->SizeToFit(horizontal_space);
781    layout->AddView(new BulletedView(description_label));
782  }
783
784  if (details.empty())
785    return;
786
787  details_view_ = new DetailsView(horizontal_space, parent_bulleted);
788
789  layout->StartRow(0, column_set_id);
790  layout->AddView(details_view_);
791
792  for (size_t i = 0; i < details.size(); ++i)
793    details_view_->AddDetail(details[i]);
794
795  views::Link* link = new views::Link(
796      l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
797
798  // Make sure the link width column is as wide as needed for both Show and
799  // Hide details, so that the arrow doesn't shift horizontally when we toggle.
800  int link_col_width =
801      views::kRelatedControlHorizontalSpacing +
802      std::max(link->font_list().GetStringWidth(
803                   l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS)),
804               link->font_list().GetStringWidth(
805                   l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS)));
806
807  column_set = layout->AddColumnSet(++column_set_id);
808  // Padding to the left of the More Details column. If the parent is using
809  // bullets for its items, then a padding of one unit will make the child item
810  // (which has no bullet) look like a sibling of its parent. Therefore increase
811  // the indentation by one more unit to show that it is in fact a child item
812  // (with no missing bullet) and not a sibling.
813  column_set->AddPaddingColumn(
814      0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1));
815  // The More Details column.
816  column_set->AddColumn(views::GridLayout::LEADING,
817                        views::GridLayout::LEADING,
818                        0,
819                        views::GridLayout::FIXED,
820                        link_col_width,
821                        link_col_width);
822  // The Up/Down arrow column.
823  column_set->AddColumn(views::GridLayout::LEADING,
824                       views::GridLayout::LEADING,
825                       0,
826                       views::GridLayout::USE_PREF,
827                       0,
828                       0);
829
830  // Add the More Details link.
831  layout->StartRow(0, column_set_id);
832  more_details_ = link;
833  more_details_->set_listener(this);
834  more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
835  layout->AddView(more_details_);
836
837  // Add the arrow after the More Details link.
838  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
839  arrow_toggle_ = new views::ImageButton(this);
840  arrow_toggle_->SetImage(views::Button::STATE_NORMAL,
841                          rb.GetImageSkiaNamed(IDR_DOWN_ARROW));
842  layout->AddView(arrow_toggle_);
843}
844
845ExpandableContainerView::~ExpandableContainerView() {
846}
847
848void ExpandableContainerView::ButtonPressed(
849    views::Button* sender, const ui::Event& event) {
850  ToggleDetailLevel();
851}
852
853void ExpandableContainerView::LinkClicked(
854    views::Link* source, int event_flags) {
855  ToggleDetailLevel();
856}
857
858void ExpandableContainerView::AnimationProgressed(
859    const gfx::Animation* animation) {
860  DCHECK_EQ(&slide_animation_, animation);
861  if (details_view_)
862    details_view_->AnimateToState(animation->GetCurrentValue());
863}
864
865void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) {
866  if (animation->GetCurrentValue() != 0.0) {
867    arrow_toggle_->SetImage(
868        views::Button::STATE_NORMAL,
869        ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
870            IDR_UP_ARROW));
871  } else {
872    arrow_toggle_->SetImage(
873        views::Button::STATE_NORMAL,
874        ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
875            IDR_DOWN_ARROW));
876  }
877
878  more_details_->SetText(expanded_ ?
879      l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) :
880      l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
881}
882
883void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) {
884  owner_->ContentsChanged();
885}
886
887void ExpandableContainerView::ToggleDetailLevel() {
888  expanded_ = !expanded_;
889
890  if (slide_animation_.IsShowing())
891    slide_animation_.Hide();
892  else
893    slide_animation_.Show();
894}
895