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.
5 #include "chrome/browser/ui/views/extensions/extension_install_dialog_view.h"
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/browser_process.h"
17 #include "chrome/browser/extensions/api/experience_sampling_private/experience_sampling.h"
18 #include "chrome/browser/extensions/bundle_installer.h"
19 #include "chrome/browser/extensions/extension_install_prompt_experiment.h"
20 #include "chrome/browser/extensions/extension_install_prompt_show_params.h"
21 #include "chrome/browser/profiles/profile.h"
22 #include "chrome/browser/profiles/profile_manager.h"
23 #include "chrome/browser/ui/browser.h"
24 #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
25 #include "chrome/common/extensions/extension_constants.h"
26 #include "chrome/grit/generated_resources.h"
27 #include "chrome/installer/util/browser_distribution.h"
28 #include "components/constrained_window/constrained_window_views.h"
29 #include "content/public/browser/page_navigator.h"
30 #include "content/public/browser/web_contents.h"
31 #include "extensions/common/extension.h"
32 #include "extensions/common/extension_urls.h"
33 #include "grit/theme_resources.h"
34 #include "ui/base/l10n/l10n_util.h"
35 #include "ui/base/resource/resource_bundle.h"
36 #include "ui/gfx/text_utils.h"
37 #include "ui/views/background.h"
38 #include "ui/views/border.h"
39 #include "ui/views/controls/button/checkbox.h"
40 #include "ui/views/controls/button/image_button.h"
41 #include "ui/views/controls/button/label_button.h"
42 #include "ui/views/controls/image_view.h"
43 #include "ui/views/controls/label.h"
44 #include "ui/views/controls/link.h"
45 #include "ui/views/controls/scroll_view.h"
46 #include "ui/views/controls/separator.h"
47 #include "ui/views/layout/box_layout.h"
48 #include "ui/views/layout/grid_layout.h"
49 #include "ui/views/layout/layout_constants.h"
50 #include "ui/views/widget/widget.h"
51 #include "ui/views/window/dialog_client_view.h"
53 using content::OpenURLParams;
54 using content::Referrer;
55 using extensions::BundleInstaller;
56 using extensions::ExperienceSamplingEvent;
60 // Width of the bullet column in BulletedView.
61 const int kBulletWidth = 20;
63 // Size of extension icon in top left of dialog.
64 const int kIconSize = 64;
66 // We offset the icon a little bit from the right edge of the dialog, to make it
67 // align with the button below it.
68 const int kIconOffset = 16;
70 // The dialog will resize based on its content, but this sets a maximum height
71 // before overflowing a scrollbar.
72 const int kDialogMaxHeight = 300;
74 // Width of the left column of the dialog when the extension requests
76 const int kPermissionsLeftColumnWidth = 250;
78 // Width of the left column of the dialog when the extension requests no
80 const int kNoPermissionsLeftColumnWidth = 200;
82 // Width of the left column for bundle install prompts. There's only one column
83 // in this case, so make it wider than normal.
84 const int kBundleLeftColumnWidth = 300;
86 // Width of the left column for external install prompts. The text is long in
87 // this case, so make it wider than normal.
88 const int kExternalInstallLeftColumnWidth = 350;
90 // Lighter color for labels.
91 const SkColor kLighterLabelColor = SkColorSetRGB(0x99, 0x99, 0x99);
93 // Represents an action on a clickable link created by the install prompt
94 // experiment. This is used to group the actions in UMA histograms named
95 // Extensions.InstallPromptExperiment.ShowDetails and
96 // Extensions.InstallPromptExperiment.ShowPermissions.
97 enum ExperimentLinkAction {
104 void AddResourceIcon(const gfx::ImageSkia* skia_image, void* data) {
105 views::View* parent = static_cast<views::View*>(data);
106 views::ImageView* image_view = new views::ImageView();
107 image_view->SetImage(*skia_image);
108 parent->AddChildView(image_view);
111 // Creates a string for displaying |message| to the user. If it has to look
112 // like a entry in a bullet point list, one is added.
113 base::string16 PrepareForDisplay(const base::string16& message,
115 return bullet_point ? l10n_util::GetStringFUTF16(
116 IDS_EXTENSION_PERMISSION_LINE,
122 BulletedView::BulletedView(views::View* view) {
123 views::GridLayout* layout = new views::GridLayout(this);
124 SetLayoutManager(layout);
125 views::ColumnSet* column_set = layout->AddColumnSet(0);
126 column_set->AddColumn(views::GridLayout::CENTER,
127 views::GridLayout::LEADING,
129 views::GridLayout::FIXED,
132 column_set->AddColumn(views::GridLayout::LEADING,
133 views::GridLayout::LEADING,
135 views::GridLayout::USE_PREF,
136 0, // No fixed width.
138 layout->StartRow(0, 0);
139 layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true)));
140 layout->AddView(view);
143 CheckboxedView::CheckboxedView(views::View* view,
144 views::ButtonListener* listener) {
145 views::GridLayout* layout = new views::GridLayout(this);
146 SetLayoutManager(layout);
147 views::ColumnSet* column_set = layout->AddColumnSet(0);
148 column_set->AddColumn(views::GridLayout::LEADING,
149 views::GridLayout::LEADING,
151 views::GridLayout::USE_PREF,
152 0, // No fixed width.
154 column_set->AddColumn(views::GridLayout::LEADING,
155 views::GridLayout::LEADING,
157 views::GridLayout::USE_PREF,
158 0, // No fixed width.
160 layout->StartRow(0, 0);
161 views::Checkbox* checkbox = new views::Checkbox(base::string16());
162 checkbox->set_listener(listener);
163 // Alignment needs to be explicitly set again here, otherwise the views are
164 // not vertically centered.
165 layout->AddView(checkbox, 1, 1,
166 views::GridLayout::LEADING, views::GridLayout::CENTER);
167 layout->AddView(view, 1, 1,
168 views::GridLayout::LEADING, views::GridLayout::CENTER);
171 void ShowExtensionInstallDialogImpl(
172 ExtensionInstallPromptShowParams* show_params,
173 ExtensionInstallPrompt::Delegate* delegate,
174 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) {
175 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
176 ExtensionInstallDialogView* dialog =
177 new ExtensionInstallDialogView(show_params->profile(),
178 show_params->GetParentWebContents(),
181 CreateBrowserModalDialogViews(dialog, show_params->GetParentWindow())->Show();
184 CustomScrollableView::CustomScrollableView() {}
185 CustomScrollableView::~CustomScrollableView() {}
187 void CustomScrollableView::Layout() {
188 SetBounds(x(), y(), width(), GetHeightForWidth(width()));
189 views::View::Layout();
192 ExtensionInstallDialogView::ExtensionInstallDialogView(
194 content::PageNavigator* navigator,
195 ExtensionInstallPrompt::Delegate* delegate,
196 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt)
198 navigator_(navigator),
203 scrollable_header_only_(NULL),
204 show_details_link_(NULL),
205 checkbox_info_label_(NULL),
207 handled_result_(false) {
211 ExtensionInstallDialogView::~ExtensionInstallDialogView() {
212 if (!handled_result_)
213 delegate_->InstallUIAbort(true);
216 void ExtensionInstallDialogView::InitView() {
217 // Possible grid layouts without ExtensionPermissionDialog experiment:
219 // w/ permissions no permissions
220 // +--------------------+------+ +--------------+------+
221 // | heading | icon | | heading | icon |
222 // +--------------------| | +--------------| |
223 // | rating | | | rating | |
224 // +--------------------| | +--------------+ |
225 // | user_count | | | user_count | |
226 // +--------------------| | +--------------| |
227 // | store_link | | | store_link | |
228 // +--------------------+------+ +--------------+------+
230 // +--------------------+------+
231 // | permissions_header | |
232 // +--------------------+------+
234 // +--------------------+------+
236 // +--------------------+------+
239 // w/ permissions no permissions
240 // +--------------------+------+ +--------------+------+
241 // | heading | icon | | heading | icon |
242 // +--------------------| | +--------------+------+
243 // | permissions_header | |
244 // +--------------------| |
246 // +--------------------| |
248 // +--------------------+------+
250 // If the ExtensionPermissionDialog is on, the layout is modified depending
251 // on the experiment group. For text only experiment, a footer is added at the
252 // bottom of the layouts. For others, inline details are added below some of
255 // Regular install w/ permissions and footer (experiment):
256 // +--------------------+------+
257 // | heading | icon |
258 // +--------------------| |
259 // | permissions_header | |
260 // +--------------------| |
262 // +--------------------| |
264 // +--------------------+------+
266 // +--------------------+------+
268 // Regular install w/ permissions and inline explanations (experiment):
269 // +--------------------+------+
270 // | heading | icon |
271 // +--------------------| |
272 // | permissions_header | |
273 // +--------------------| |
275 // +--------------------| |
276 // | explanation1 | |
277 // +--------------------| |
279 // +--------------------| |
280 // | explanation2 | |
281 // +--------------------+------+
283 // Regular install w/ permissions and inline explanations (experiment):
284 // +--------------------+------+
285 // | heading | icon |
286 // +--------------------| |
287 // | permissions_header | |
288 // +--------------------| |
289 // |checkbox|permission1| |
290 // +--------------------| |
291 // |checkbox|permission2| |
292 // +--------------------+------+
294 // Additionally, links or informational text is added to non-client areas of
295 // the dialog depending on the experiment group.
297 int left_column_width =
298 (prompt_->ShouldShowPermissions() + prompt_->GetRetainedFileCount()) > 0
299 ? kPermissionsLeftColumnWidth
300 : kNoPermissionsLeftColumnWidth;
301 if (is_bundle_install())
302 left_column_width = kBundleLeftColumnWidth;
303 if (is_external_install())
304 left_column_width = kExternalInstallLeftColumnWidth;
306 scroll_view_ = new views::ScrollView();
307 scroll_view_->set_hide_horizontal_scrollbar(true);
308 AddChildView(scroll_view_);
310 int column_set_id = 0;
311 // Create the full scrollable view which will contain all the information
312 // including the permissions.
313 scrollable_ = new CustomScrollableView();
314 views::GridLayout* layout = CreateLayout(
315 scrollable_, left_column_width, column_set_id, false);
316 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
318 if (prompt_->ShouldShowPermissions() &&
319 prompt_->experiment()->should_show_expandable_permission_list()) {
320 // If the experiment should hide the permission list initially, create a
321 // simple layout that contains only the header, extension name and icon.
322 scrollable_header_only_ = new CustomScrollableView();
323 CreateLayout(scrollable_header_only_, left_column_width,
324 column_set_id, true);
325 scroll_view_->SetContents(scrollable_header_only_);
327 scroll_view_->SetContents(scrollable_);
330 int dialog_width = left_column_width + 2 * views::kPanelHorizMargin;
331 if (!is_bundle_install())
332 dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset;
334 // Widen the dialog for experiment with checkboxes so that the information
335 // label fits the area to the left of the buttons.
336 if (prompt_->experiment()->show_checkboxes())
337 dialog_width += 4 * views::kPanelHorizMargin;
339 if (prompt_->has_webstore_data()) {
340 layout->StartRow(0, column_set_id);
341 views::View* rating = new views::View();
342 rating->SetLayoutManager(new views::BoxLayout(
343 views::BoxLayout::kHorizontal, 0, 0, 0));
344 layout->AddView(rating);
345 prompt_->AppendRatingStars(AddResourceIcon, rating);
347 const gfx::FontList& small_font_list =
348 rb.GetFontList(ui::ResourceBundle::SmallFont);
349 views::Label* rating_count =
350 new views::Label(prompt_->GetRatingCount(), small_font_list);
351 // Add some space between the stars and the rating count.
352 rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0));
353 rating->AddChildView(rating_count);
355 layout->StartRow(0, column_set_id);
356 views::Label* user_count =
357 new views::Label(prompt_->GetUserCount(), small_font_list);
358 user_count->SetAutoColorReadabilityEnabled(false);
359 user_count->SetEnabledColor(SK_ColorGRAY);
360 layout->AddView(user_count);
362 layout->StartRow(0, column_set_id);
363 views::Link* store_link = new views::Link(
364 l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK));
365 store_link->SetFontList(small_font_list);
366 store_link->set_listener(this);
367 layout->AddView(store_link);
370 if (is_bundle_install()) {
371 BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState(
372 BundleInstaller::Item::STATE_PENDING);
373 for (size_t i = 0; i < items.size(); ++i) {
374 base::string16 extension_name =
375 base::UTF8ToUTF16(items[i].localized_name);
376 base::i18n::AdjustStringForLocaleDirection(&extension_name);
377 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
378 layout->StartRow(0, column_set_id);
379 views::Label* extension_label = new views::Label(
380 PrepareForDisplay(extension_name, true));
381 extension_label->SetMultiLine(true);
382 extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
383 extension_label->SizeToFit(left_column_width);
384 layout->AddView(extension_label);
388 bool has_permissions =
389 prompt_->GetPermissionCount(
390 ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS) > 0;
391 if (prompt_->ShouldShowPermissions()) {
397 ExtensionInstallPrompt::PermissionsType::REGULAR_PERMISSIONS);
403 ExtensionInstallPrompt::PermissionsType::WITHHELD_PERMISSIONS);
404 if (!has_permissions) {
405 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
406 layout->StartRow(0, column_set_id);
407 views::Label* permission_label = new views::Label(
408 l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS));
409 permission_label->SetMultiLine(true);
410 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
411 permission_label->SizeToFit(left_column_width);
412 layout->AddView(permission_label);
416 int space_for_files_and_devices = left_column_width;
417 if (prompt_->GetRetainedFileCount() || prompt_->GetRetainedDeviceCount()) {
418 // Slide in under the permissions, if there are any. If there are either,
419 // the retained files and devices prompts stretch all the way to the right
420 // of the dialog. If there are no permissions, the retained files and
421 // devices prompts just take up the left column.
423 if (has_permissions) {
424 space_for_files_and_devices += kIconSize;
425 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
426 column_set->AddColumn(views::GridLayout::FILL,
427 views::GridLayout::FILL,
429 views::GridLayout::USE_PREF,
431 space_for_files_and_devices);
435 if (prompt_->GetRetainedFileCount()) {
436 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
438 layout->StartRow(0, column_set_id);
439 views::Label* retained_files_header =
440 new views::Label(prompt_->GetRetainedFilesHeading());
441 retained_files_header->SetMultiLine(true);
442 retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
443 retained_files_header->SizeToFit(space_for_files_and_devices);
444 layout->AddView(retained_files_header);
446 layout->StartRow(0, column_set_id);
447 PermissionDetails details;
448 for (size_t i = 0; i < prompt_->GetRetainedFileCount(); ++i) {
449 details.push_back(prompt_->GetRetainedFile(i));
451 ExpandableContainerView* issue_advice_view =
452 new ExpandableContainerView(this,
455 space_for_files_and_devices,
459 layout->AddView(issue_advice_view);
462 if (prompt_->GetRetainedDeviceCount()) {
463 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
465 layout->StartRow(0, column_set_id);
466 views::Label* retained_devices_header =
467 new views::Label(prompt_->GetRetainedDevicesHeading());
468 retained_devices_header->SetMultiLine(true);
469 retained_devices_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
470 retained_devices_header->SizeToFit(space_for_files_and_devices);
471 layout->AddView(retained_devices_header);
473 layout->StartRow(0, column_set_id);
474 PermissionDetails details;
475 for (size_t i = 0; i < prompt_->GetRetainedDeviceCount(); ++i) {
476 details.push_back(prompt_->GetRetainedDeviceMessageString(i));
478 ExpandableContainerView* issue_advice_view =
479 new ExpandableContainerView(this,
482 space_for_files_and_devices,
486 layout->AddView(issue_advice_view);
489 DCHECK(prompt_->type() >= 0);
490 UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type",
492 ExtensionInstallPrompt::NUM_PROMPT_TYPES);
494 if (prompt_->ShouldShowPermissions()) {
495 if (prompt_->ShouldShowExplanationText()) {
496 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
497 column_set->AddColumn(views::GridLayout::LEADING,
498 views::GridLayout::FILL,
500 views::GridLayout::USE_PREF,
503 // Add two rows of space so that the text stands out.
504 layout->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing);
506 layout->StartRow(0, column_set_id);
507 views::Label* explanation =
508 new views::Label(prompt_->experiment()->GetExplanationText());
509 explanation->SetMultiLine(true);
510 explanation->SetHorizontalAlignment(gfx::ALIGN_LEFT);
511 explanation->SizeToFit(left_column_width + kIconSize);
512 layout->AddView(explanation);
515 if (prompt_->experiment()->should_show_expandable_permission_list() ||
516 (prompt_->experiment()->show_details_link() &&
517 prompt_->experiment()->should_show_inline_explanations() &&
518 !inline_explanations_.empty())) {
519 // Don't show the "Show details" link if there are retained
520 // files. These have their own "Show details" links and having
521 // multiple levels of links is confusing.
522 if (prompt_->GetRetainedFileCount() == 0) {
524 prompt_->experiment()->should_show_expandable_permission_list()
525 ? IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS
526 : IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_DETAILS;
527 show_details_link_ = new views::Link(
528 l10n_util::GetStringUTF16(text_id));
529 show_details_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
530 show_details_link_->set_listener(this);
531 UpdateLinkActionHistogram(LINK_SHOWN);
533 UpdateLinkActionHistogram(LINK_NOT_SHOWN);
537 if (prompt_->experiment()->show_checkboxes()) {
538 checkbox_info_label_ = new views::Label(
539 l10n_util::GetStringUTF16(
540 IDS_EXTENSION_PROMPT_EXPERIMENT_CHECKBOX_INFO));
541 checkbox_info_label_->SetMultiLine(true);
542 checkbox_info_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
543 checkbox_info_label_->SetAutoColorReadabilityEnabled(false);
544 checkbox_info_label_->SetEnabledColor(kLighterLabelColor);
548 gfx::Size scrollable_size = scrollable_->GetPreferredSize();
549 scrollable_->SetBoundsRect(gfx::Rect(scrollable_size));
550 dialog_size_ = gfx::Size(
552 std::min(scrollable_size.height(), kDialogMaxHeight));
554 if (scrollable_header_only_) {
555 gfx::Size header_only_size = scrollable_header_only_->GetPreferredSize();
556 scrollable_header_only_->SetBoundsRect(gfx::Rect(header_only_size));
557 dialog_size_ = gfx::Size(
558 dialog_width, std::min(header_only_size.height(), kDialogMaxHeight));
561 std::string event_name = ExperienceSamplingEvent::kExtensionInstallDialog;
563 ExtensionInstallPrompt::PromptTypeToString(prompt_->type()));
564 sampling_event_ = ExperienceSamplingEvent::Create(event_name);
567 bool ExtensionInstallDialogView::AddPermissions(
568 views::GridLayout* layout,
569 ui::ResourceBundle& rb,
571 int left_column_width,
572 ExtensionInstallPrompt::PermissionsType perm_type) {
573 if (prompt_->GetPermissionCount(perm_type) == 0)
576 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
577 if (is_inline_install()) {
578 layout->StartRow(0, column_set_id);
579 layout->AddView(new views::Separator(views::Separator::HORIZONTAL),
582 views::GridLayout::FILL,
583 views::GridLayout::FILL);
584 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
587 layout->StartRow(0, column_set_id);
588 views::Label* permissions_header = NULL;
589 if (is_bundle_install()) {
590 // We need to pass the FontList in the constructor, rather than calling
591 // SetFontList later, because otherwise SizeToFit mis-judges the width
594 new views::Label(prompt_->GetPermissionsHeading(perm_type),
595 rb.GetFontList(ui::ResourceBundle::MediumFont));
598 new views::Label(prompt_->GetPermissionsHeading(perm_type));
600 permissions_header->SetMultiLine(true);
601 permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
602 permissions_header->SizeToFit(left_column_width);
603 layout->AddView(permissions_header);
605 for (size_t i = 0; i < prompt_->GetPermissionCount(perm_type); ++i) {
606 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
607 layout->StartRow(0, column_set_id);
608 views::Label* permission_label =
609 new views::Label(prompt_->GetPermission(i, perm_type));
611 const SkColor kTextHighlight = SK_ColorRED;
612 const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3);
613 if (prompt_->experiment()->ShouldHighlightText(
614 prompt_->GetPermission(i, perm_type))) {
615 permission_label->SetAutoColorReadabilityEnabled(false);
616 permission_label->SetEnabledColor(kTextHighlight);
617 } else if (prompt_->experiment()->ShouldHighlightBackground(
618 prompt_->GetPermission(i, perm_type))) {
619 permission_label->SetLineHeight(18);
620 permission_label->set_background(
621 views::Background::CreateSolidBackground(kBackgroundHighlight));
624 permission_label->SetMultiLine(true);
625 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
627 if (prompt_->experiment()->show_checkboxes()) {
628 permission_label->SizeToFit(left_column_width);
629 layout->AddView(new CheckboxedView(permission_label, this));
632 permission_label->SizeToFit(left_column_width - kBulletWidth);
633 layout->AddView(new BulletedView(permission_label));
636 // If we have more details to provide, show them in collapsed form.
637 if (!prompt_->GetPermissionsDetails(i, perm_type).empty()) {
638 layout->StartRow(0, column_set_id);
639 PermissionDetails details;
640 details.push_back(PrepareForDisplay(
641 prompt_->GetPermissionsDetails(i, perm_type), false));
642 ExpandableContainerView* details_container =
643 new ExpandableContainerView(this,
650 layout->AddView(details_container);
653 if (prompt_->experiment()->should_show_inline_explanations()) {
654 base::string16 explanation = prompt_->experiment()->GetInlineExplanation(
655 prompt_->GetPermission(i, perm_type));
656 if (!explanation.empty()) {
657 PermissionDetails details;
658 details.push_back(explanation);
659 ExpandableContainerView* container =
660 new ExpandableContainerView(this,
667 // Inline explanations are expanded by default if there is
668 // no "Show details" link.
669 if (!prompt_->experiment()->show_details_link())
670 container->ExpandWithoutAnimation();
671 layout->StartRow(0, column_set_id);
672 layout->AddView(container);
673 inline_explanations_.push_back(container);
680 views::GridLayout* ExtensionInstallDialogView::CreateLayout(
682 int left_column_width,
684 bool single_detail_row) const {
685 views::GridLayout* layout = views::GridLayout::CreatePanel(parent);
686 parent->SetLayoutManager(layout);
688 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
689 column_set->AddColumn(views::GridLayout::LEADING,
690 views::GridLayout::FILL,
692 views::GridLayout::USE_PREF,
695 if (!is_bundle_install()) {
696 column_set->AddPaddingColumn(0, views::kPanelHorizMargin);
697 column_set->AddColumn(views::GridLayout::TRAILING,
698 views::GridLayout::LEADING,
700 views::GridLayout::USE_PREF,
705 layout->StartRow(0, column_set_id);
707 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
709 views::Label* heading = new views::Label(
710 prompt_->GetHeading(), rb.GetFontList(ui::ResourceBundle::MediumFont));
711 heading->SetMultiLine(true);
712 heading->SetHorizontalAlignment(gfx::ALIGN_LEFT);
713 heading->SizeToFit(left_column_width);
714 layout->AddView(heading);
716 if (!is_bundle_install()) {
717 // Scale down to icon size, but allow smaller icons (don't scale up).
718 const gfx::ImageSkia* image = prompt_->icon().ToImageSkia();
719 gfx::Size size(image->width(), image->height());
720 if (size.width() > kIconSize || size.height() > kIconSize)
721 size = gfx::Size(kIconSize, kIconSize);
722 views::ImageView* icon = new views::ImageView();
723 icon->SetImageSize(size);
724 icon->SetImage(*image);
725 icon->SetHorizontalAlignment(views::ImageView::CENTER);
726 icon->SetVerticalAlignment(views::ImageView::CENTER);
727 if (single_detail_row) {
728 layout->AddView(icon);
730 int icon_row_span = 1;
731 if (is_inline_install()) {
732 // Also span the rating, user_count and store_link rows.
734 } else if (prompt_->ShouldShowPermissions()) {
735 size_t permission_count = prompt_->GetPermissionCount(
736 ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS);
737 // Also span the permission header and each of the permission rows (all
738 // have a padding row above it). This also works for the 'no special
739 // permissions' case.
740 icon_row_span = 3 + permission_count * 2;
741 } else if (prompt_->GetRetainedFileCount()) {
742 // Also span the permission header and the retained files container.
745 layout->AddView(icon, 1, icon_row_span);
751 void ExtensionInstallDialogView::ContentsChanged() {
755 void ExtensionInstallDialogView::ViewHierarchyChanged(
756 const ViewHierarchyChangedDetails& details) {
757 views::DialogDelegateView::ViewHierarchyChanged(details);
758 // Since we want the links to show up in the same visual row as the accept
759 // and cancel buttons, which is provided by the framework, we must add the
760 // buttons to the non-client view, which is the parent of this view.
761 // Similarly, when we're removed from the view hierarchy, we must take care
762 // to clean up those items as well.
763 if (details.child == this) {
764 if (details.is_add) {
765 if (show_details_link_)
766 details.parent->AddChildView(show_details_link_);
767 if (checkbox_info_label_)
768 details.parent->AddChildView(checkbox_info_label_);
770 if (show_details_link_)
771 details.parent->RemoveChildView(show_details_link_);
772 if (checkbox_info_label_)
773 details.parent->RemoveChildView(checkbox_info_label_);
778 int ExtensionInstallDialogView::GetDialogButtons() const {
779 int buttons = prompt_->GetDialogButtons();
780 // Simply having just an OK button is *not* supported. See comment on function
781 // GetDialogButtons in dialog_delegate.h for reasons.
782 DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0);
786 base::string16 ExtensionInstallDialogView::GetDialogButtonLabel(
787 ui::DialogButton button) const {
789 case ui::DIALOG_BUTTON_OK:
790 return prompt_->GetAcceptButtonLabel();
791 case ui::DIALOG_BUTTON_CANCEL:
792 return prompt_->HasAbortButtonLabel()
793 ? prompt_->GetAbortButtonLabel()
794 : l10n_util::GetStringUTF16(IDS_CANCEL);
797 return base::string16();
801 int ExtensionInstallDialogView::GetDefaultDialogButton() const {
802 return ui::DIALOG_BUTTON_CANCEL;
805 bool ExtensionInstallDialogView::Cancel() {
809 handled_result_ = true;
810 UpdateInstallResultHistogram(false);
812 sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kDeny);
813 delegate_->InstallUIAbort(true);
817 bool ExtensionInstallDialogView::Accept() {
818 DCHECK(!handled_result_);
820 handled_result_ = true;
821 UpdateInstallResultHistogram(true);
823 sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kProceed);
824 delegate_->InstallUIProceed();
828 ui::ModalType ExtensionInstallDialogView::GetModalType() const {
829 return ui::MODAL_TYPE_WINDOW;
832 base::string16 ExtensionInstallDialogView::GetWindowTitle() const {
833 return prompt_->GetDialogTitle();
836 void ExtensionInstallDialogView::LinkClicked(views::Link* source,
838 if (source == show_details_link_) {
839 UpdateLinkActionHistogram(LINK_CLICKED);
840 // Show details link is used to either reveal whole permission list or to
841 // reveal inline explanations.
842 if (prompt_->experiment()->should_show_expandable_permission_list()) {
843 gfx::Rect bounds = GetWidget()->GetWindowBoundsInScreen();
844 int spacing = bounds.height() -
845 scrollable_header_only_->GetPreferredSize().height();
846 int content_height = std::min(scrollable_->GetPreferredSize().height(),
848 bounds.set_height(spacing + content_height);
849 scroll_view_->SetContents(scrollable_);
850 GetWidget()->SetBoundsConstrained(bounds);
853 ToggleInlineExplanations();
855 show_details_link_->SetVisible(false);
857 GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
858 prompt_->extension()->id());
859 OpenURLParams params(
860 store_url, Referrer(), NEW_FOREGROUND_TAB,
861 ui::PAGE_TRANSITION_LINK,
865 navigator_->OpenURL(params);
867 chrome::ScopedTabbedBrowserDisplayer displayer(
868 profile_, chrome::GetActiveDesktop());
869 displayer.browser()->OpenURL(params);
871 GetWidget()->Close();
875 void ExtensionInstallDialogView::ToggleInlineExplanations() {
876 for (InlineExplanations::iterator it = inline_explanations_.begin();
877 it != inline_explanations_.end(); ++it)
878 (*it)->ToggleDetailLevel();
881 void ExtensionInstallDialogView::Layout() {
882 scroll_view_->SetBounds(0, 0, width(), height());
884 if (show_details_link_ || checkbox_info_label_) {
885 views::LabelButton* cancel_button = GetDialogClientView()->cancel_button();
886 gfx::Rect parent_bounds = parent()->GetContentsBounds();
887 // By default, layouts have an inset of kButtonHEdgeMarginNew. In order to
888 // align the link horizontally with the left side of the contents of the
889 // layout, put a horizontal margin with this amount.
890 const int horizontal_margin = views::kButtonHEdgeMarginNew;
891 const int vertical_margin = views::kButtonVEdgeMarginNew;
892 int y_buttons = parent_bounds.bottom() -
893 cancel_button->GetPreferredSize().height() - vertical_margin;
894 int max_width = dialog_size_.width() - cancel_button->width() * 2 -
895 horizontal_margin * 2 - views::kRelatedButtonHSpacing;
896 if (show_details_link_) {
897 gfx::Size link_size = show_details_link_->GetPreferredSize();
898 show_details_link_->SetBounds(
900 y_buttons + (cancel_button->height() - link_size.height()) / 2,
901 link_size.width(), link_size.height());
903 if (checkbox_info_label_) {
904 gfx::Size label_size = checkbox_info_label_->GetPreferredSize();
905 checkbox_info_label_->SetBounds(
907 y_buttons + (cancel_button->height() - label_size.height()) / 2,
908 label_size.width(), label_size.height());
909 checkbox_info_label_->SizeToFit(max_width);
912 // Disable accept button if there are unchecked boxes and
913 // the experiment is on.
914 if (prompt_->experiment()->show_checkboxes())
915 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
917 DialogDelegateView::Layout();
920 gfx::Size ExtensionInstallDialogView::GetPreferredSize() const {
924 void ExtensionInstallDialogView::ButtonPressed(views::Button* sender,
925 const ui::Event& event) {
926 if (std::string(views::Checkbox::kViewClassName) == sender->GetClassName()) {
927 views::Checkbox* checkbox = static_cast<views::Checkbox*>(sender);
928 if (checkbox->checked())
933 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
934 checkbox_info_label_->SetVisible(unchecked_boxes_ > 0);
938 void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted)
940 if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT)
941 UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted);
944 void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type)
946 if (prompt_->experiment()->should_show_expandable_permission_list()) {
947 // The clickable link in the UI is "Show Permissions".
948 UMA_HISTOGRAM_ENUMERATION(
949 "Extensions.InstallPromptExperiment.ShowPermissions",
953 // The clickable link in the UI is "Show Details".
954 UMA_HISTOGRAM_ENUMERATION(
955 "Extensions.InstallPromptExperiment.ShowDetails",
961 // ExpandableContainerView::DetailsView ----------------------------------------
963 ExpandableContainerView::DetailsView::DetailsView(int horizontal_space,
964 bool parent_bulleted,
966 : layout_(new views::GridLayout(this)),
968 lighter_color_(lighter_color) {
969 SetLayoutManager(layout_);
970 views::ColumnSet* column_set = layout_->AddColumnSet(0);
971 // If the parent is using bullets for its items, then a padding of one unit
972 // will make the child item (which has no bullet) look like a sibling of its
973 // parent. Therefore increase the indentation by one more unit to show that it
974 // is in fact a child item (with no missing bullet) and not a sibling.
976 views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1);
977 column_set->AddPaddingColumn(0, padding);
978 column_set->AddColumn(views::GridLayout::LEADING,
979 views::GridLayout::LEADING,
981 views::GridLayout::FIXED,
982 horizontal_space - padding,
986 void ExpandableContainerView::DetailsView::AddDetail(
987 const base::string16& detail) {
988 layout_->StartRowWithPadding(0, 0,
989 0, views::kRelatedControlSmallVerticalSpacing);
990 views::Label* detail_label =
991 new views::Label(PrepareForDisplay(detail, false));
992 detail_label->SetMultiLine(true);
993 detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
994 if (lighter_color_) {
995 detail_label->SetEnabledColor(kLighterLabelColor);
996 detail_label->SetAutoColorReadabilityEnabled(false);
998 layout_->AddView(detail_label);
1001 gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const {
1002 gfx::Size size = views::View::GetPreferredSize();
1003 return gfx::Size(size.width(), size.height() * state_);
1006 void ExpandableContainerView::DetailsView::AnimateToState(double state) {
1008 PreferredSizeChanged();
1012 // ExpandableContainerView -----------------------------------------------------
1014 ExpandableContainerView::ExpandableContainerView(
1015 ExtensionInstallDialogView* owner,
1016 const base::string16& description,
1017 const PermissionDetails& details,
1018 int horizontal_space,
1019 bool parent_bulleted,
1020 bool show_expand_link,
1021 bool lighter_color_details)
1023 details_view_(NULL),
1024 more_details_(NULL),
1025 slide_animation_(this),
1026 arrow_toggle_(NULL),
1028 views::GridLayout* layout = new views::GridLayout(this);
1029 SetLayoutManager(layout);
1030 int column_set_id = 0;
1031 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
1032 column_set->AddColumn(views::GridLayout::LEADING,
1033 views::GridLayout::LEADING,
1035 views::GridLayout::USE_PREF,
1038 if (!description.empty()) {
1039 layout->StartRow(0, column_set_id);
1041 views::Label* description_label = new views::Label(description);
1042 description_label->SetMultiLine(true);
1043 description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1044 description_label->SizeToFit(horizontal_space);
1045 layout->AddView(new BulletedView(description_label));
1048 if (details.empty())
1051 details_view_ = new DetailsView(horizontal_space, parent_bulleted,
1052 lighter_color_details);
1054 layout->StartRow(0, column_set_id);
1055 layout->AddView(details_view_);
1057 for (size_t i = 0; i < details.size(); ++i)
1058 details_view_->AddDetail(details[i]);
1060 // TODO(meacer): Remove show_expand_link when the experiment is completed.
1061 if (show_expand_link) {
1062 views::Link* link = new views::Link(
1063 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
1065 // Make sure the link width column is as wide as needed for both Show and
1066 // Hide details, so that the arrow doesn't shift horizontally when we
1068 int link_col_width =
1069 views::kRelatedControlHorizontalSpacing +
1070 std::max(gfx::GetStringWidth(
1071 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS),
1073 gfx::GetStringWidth(
1074 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS),
1075 link->font_list()));
1077 column_set = layout->AddColumnSet(++column_set_id);
1078 // Padding to the left of the More Details column. If the parent is using
1079 // bullets for its items, then a padding of one unit will make the child
1080 // item (which has no bullet) look like a sibling of its parent. Therefore
1081 // increase the indentation by one more unit to show that it is in fact a
1082 // child item (with no missing bullet) and not a sibling.
1083 column_set->AddPaddingColumn(
1084 0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1));
1085 // The More Details column.
1086 column_set->AddColumn(views::GridLayout::LEADING,
1087 views::GridLayout::LEADING,
1089 views::GridLayout::FIXED,
1092 // The Up/Down arrow column.
1093 column_set->AddColumn(views::GridLayout::LEADING,
1094 views::GridLayout::LEADING,
1096 views::GridLayout::USE_PREF,
1100 // Add the More Details link.
1101 layout->StartRow(0, column_set_id);
1102 more_details_ = link;
1103 more_details_->set_listener(this);
1104 more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1105 layout->AddView(more_details_);
1107 // Add the arrow after the More Details link.
1108 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
1109 arrow_toggle_ = new views::ImageButton(this);
1110 arrow_toggle_->SetImage(views::Button::STATE_NORMAL,
1111 rb.GetImageSkiaNamed(IDR_DOWN_ARROW));
1112 layout->AddView(arrow_toggle_);
1116 ExpandableContainerView::~ExpandableContainerView() {
1119 void ExpandableContainerView::ButtonPressed(
1120 views::Button* sender, const ui::Event& event) {
1121 ToggleDetailLevel();
1124 void ExpandableContainerView::LinkClicked(
1125 views::Link* source, int event_flags) {
1126 ToggleDetailLevel();
1129 void ExpandableContainerView::AnimationProgressed(
1130 const gfx::Animation* animation) {
1131 DCHECK_EQ(&slide_animation_, animation);
1133 details_view_->AnimateToState(animation->GetCurrentValue());
1136 void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) {
1137 if (arrow_toggle_) {
1138 if (animation->GetCurrentValue() != 0.0) {
1139 arrow_toggle_->SetImage(
1140 views::Button::STATE_NORMAL,
1141 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
1144 arrow_toggle_->SetImage(
1145 views::Button::STATE_NORMAL,
1146 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
1150 if (more_details_) {
1151 more_details_->SetText(expanded_ ?
1152 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) :
1153 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
1157 void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) {
1158 owner_->ContentsChanged();
1161 void ExpandableContainerView::ToggleDetailLevel() {
1162 expanded_ = !expanded_;
1164 if (slide_animation_.IsShowing())
1165 slide_animation_.Hide();
1167 slide_animation_.Show();
1170 void ExpandableContainerView::ExpandWithoutAnimation() {
1172 details_view_->AnimateToState(1.0);
1176 ExtensionInstallPrompt::ShowDialogCallback
1177 ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
1178 return base::Bind(&ShowExtensionInstallDialogImpl);