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