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/profiles/profile.h"
21 #include "chrome/browser/profiles/profile_manager.h"
22 #include "chrome/browser/ui/views/constrained_window_views.h"
23 #include "chrome/common/chrome_switches.h"
24 #include "chrome/common/extensions/extension_constants.h"
25 #include "chrome/installer/util/browser_distribution.h"
26 #include "content/public/browser/page_navigator.h"
27 #include "content/public/browser/web_contents.h"
28 #include "extensions/common/extension.h"
29 #include "grit/chromium_strings.h"
30 #include "grit/generated_resources.h"
31 #include "grit/google_chrome_strings.h"
32 #include "grit/theme_resources.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/base/resource/resource_bundle.h"
35 #include "ui/gfx/text_utils.h"
36 #include "ui/views/background.h"
37 #include "ui/views/border.h"
38 #include "ui/views/controls/button/checkbox.h"
39 #include "ui/views/controls/button/image_button.h"
40 #include "ui/views/controls/button/label_button.h"
41 #include "ui/views/controls/image_view.h"
42 #include "ui/views/controls/label.h"
43 #include "ui/views/controls/link.h"
44 #include "ui/views/controls/scroll_view.h"
45 #include "ui/views/controls/separator.h"
46 #include "ui/views/layout/box_layout.h"
47 #include "ui/views/layout/grid_layout.h"
48 #include "ui/views/layout/layout_constants.h"
49 #include "ui/views/widget/widget.h"
50 #include "ui/views/window/dialog_client_view.h"
52 using content::OpenURLParams;
53 using content::Referrer;
54 using extensions::BundleInstaller;
55 using extensions::ExperienceSamplingEvent;
59 // Width of the bullet column in BulletedView.
60 const int kBulletWidth = 20;
62 // Size of extension icon in top left of dialog.
63 const int kIconSize = 64;
65 // We offset the icon a little bit from the right edge of the dialog, to make it
66 // align with the button below it.
67 const int kIconOffset = 16;
69 // The dialog will resize based on its content, but this sets a maximum height
70 // before overflowing a scrollbar.
71 const int kDialogMaxHeight = 300;
73 // Width of the left column of the dialog when the extension requests
75 const int kPermissionsLeftColumnWidth = 250;
77 // Width of the left column of the dialog when the extension requests no
79 const int kNoPermissionsLeftColumnWidth = 200;
81 // Width of the left column for bundle install prompts. There's only one column
82 // in this case, so make it wider than normal.
83 const int kBundleLeftColumnWidth = 300;
85 // Width of the left column for external install prompts. The text is long in
86 // this case, so make it wider than normal.
87 const int kExternalInstallLeftColumnWidth = 350;
89 // Lighter color for labels.
90 const SkColor kLighterLabelColor = SkColorSetRGB(0x99, 0x99, 0x99);
92 // Represents an action on a clickable link created by the install prompt
93 // experiment. This is used to group the actions in UMA histograms named
94 // Extensions.InstallPromptExperiment.ShowDetails and
95 // Extensions.InstallPromptExperiment.ShowPermissions.
96 enum ExperimentLinkAction {
103 void AddResourceIcon(const gfx::ImageSkia* skia_image, void* data) {
104 views::View* parent = static_cast<views::View*>(data);
105 views::ImageView* image_view = new views::ImageView();
106 image_view->SetImage(*skia_image);
107 parent->AddChildView(image_view);
110 // Creates a string for displaying |message| to the user. If it has to look
111 // like a entry in a bullet point list, one is added.
112 base::string16 PrepareForDisplay(const base::string16& message,
114 return bullet_point ? l10n_util::GetStringFUTF16(
115 IDS_EXTENSION_PERMISSION_LINE,
121 BulletedView::BulletedView(views::View* view) {
122 views::GridLayout* layout = new views::GridLayout(this);
123 SetLayoutManager(layout);
124 views::ColumnSet* column_set = layout->AddColumnSet(0);
125 column_set->AddColumn(views::GridLayout::CENTER,
126 views::GridLayout::LEADING,
128 views::GridLayout::FIXED,
131 column_set->AddColumn(views::GridLayout::LEADING,
132 views::GridLayout::LEADING,
134 views::GridLayout::USE_PREF,
135 0, // No fixed width.
137 layout->StartRow(0, 0);
138 layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true)));
139 layout->AddView(view);
142 CheckboxedView::CheckboxedView(views::View* view,
143 views::ButtonListener* listener) {
144 views::GridLayout* layout = new views::GridLayout(this);
145 SetLayoutManager(layout);
146 views::ColumnSet* column_set = layout->AddColumnSet(0);
147 column_set->AddColumn(views::GridLayout::LEADING,
148 views::GridLayout::LEADING,
150 views::GridLayout::USE_PREF,
151 0, // No fixed width.
153 column_set->AddColumn(views::GridLayout::LEADING,
154 views::GridLayout::LEADING,
156 views::GridLayout::USE_PREF,
157 0, // No fixed width.
159 layout->StartRow(0, 0);
160 views::Checkbox* checkbox = new views::Checkbox(base::string16());
161 checkbox->set_listener(listener);
162 // Alignment needs to be explicitly set again here, otherwise the views are
163 // not vertically centered.
164 layout->AddView(checkbox, 1, 1,
165 views::GridLayout::LEADING, views::GridLayout::CENTER);
166 layout->AddView(view, 1, 1,
167 views::GridLayout::LEADING, views::GridLayout::CENTER);
170 void ShowExtensionInstallDialogImpl(
171 const ExtensionInstallPrompt::ShowParams& show_params,
172 ExtensionInstallPrompt::Delegate* delegate,
173 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) {
174 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
175 CreateBrowserModalDialogViews(
176 new ExtensionInstallDialogView(show_params.navigator, delegate, prompt),
177 show_params.parent_window)->Show();
180 CustomScrollableView::CustomScrollableView() {}
181 CustomScrollableView::~CustomScrollableView() {}
183 void CustomScrollableView::Layout() {
184 SetBounds(x(), y(), width(), GetHeightForWidth(width()));
185 views::View::Layout();
188 ExtensionInstallDialogView::ExtensionInstallDialogView(
189 content::PageNavigator* navigator,
190 ExtensionInstallPrompt::Delegate* delegate,
191 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt)
192 : navigator_(navigator),
197 scrollable_header_only_(NULL),
198 show_details_link_(NULL),
199 checkbox_info_label_(NULL),
200 unchecked_boxes_(0) {
201 // Possible grid layouts without ExtensionPermissionDialog experiment:
203 // w/ permissions no permissions
204 // +--------------------+------+ +--------------+------+
205 // | heading | icon | | heading | icon |
206 // +--------------------| | +--------------| |
207 // | rating | | | rating | |
208 // +--------------------| | +--------------+ |
209 // | user_count | | | user_count | |
210 // +--------------------| | +--------------| |
211 // | store_link | | | store_link | |
212 // +--------------------+------+ +--------------+------+
214 // +--------------------+------+
215 // | permissions_header | |
216 // +--------------------+------+
218 // +--------------------+------+
220 // +--------------------+------+
223 // w/ permissions no permissions
224 // +--------------------+------+ +--------------+------+
225 // | heading | icon | | heading | icon |
226 // +--------------------| | +--------------+------+
227 // | permissions_header | |
228 // +--------------------| |
230 // +--------------------| |
232 // +--------------------+------+
234 // If the ExtensionPermissionDialog is on, the layout is modified depending
235 // on the experiment group. For text only experiment, a footer is added at the
236 // bottom of the layouts. For others, inline details are added below some of
239 // Regular install w/ permissions and footer (experiment):
240 // +--------------------+------+
241 // | heading | icon |
242 // +--------------------| |
243 // | permissions_header | |
244 // +--------------------| |
246 // +--------------------| |
248 // +--------------------+------+
250 // +--------------------+------+
252 // Regular install w/ permissions and inline explanations (experiment):
253 // +--------------------+------+
254 // | heading | icon |
255 // +--------------------| |
256 // | permissions_header | |
257 // +--------------------| |
259 // +--------------------| |
260 // | explanation1 | |
261 // +--------------------| |
263 // +--------------------| |
264 // | explanation2 | |
265 // +--------------------+------+
267 // Regular install w/ permissions and inline explanations (experiment):
268 // +--------------------+------+
269 // | heading | icon |
270 // +--------------------| |
271 // | permissions_header | |
272 // +--------------------| |
273 // |checkbox|permission1| |
274 // +--------------------| |
275 // |checkbox|permission2| |
276 // +--------------------+------+
278 // Additionally, links or informational text is added to non-client areas of
279 // the dialog depending on the experiment group.
281 int left_column_width =
282 (prompt->ShouldShowPermissions() + prompt->GetRetainedFileCount()) > 0
283 ? kPermissionsLeftColumnWidth
284 : kNoPermissionsLeftColumnWidth;
285 if (is_bundle_install())
286 left_column_width = kBundleLeftColumnWidth;
287 if (is_external_install())
288 left_column_width = kExternalInstallLeftColumnWidth;
290 scroll_view_ = new views::ScrollView();
291 scroll_view_->set_hide_horizontal_scrollbar(true);
292 AddChildView(scroll_view_);
294 int column_set_id = 0;
295 // Create the full scrollable view which will contain all the information
296 // including the permissions.
297 scrollable_ = new CustomScrollableView();
298 views::GridLayout* layout = CreateLayout(
299 scrollable_, left_column_width, column_set_id, false);
300 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
302 if (prompt->ShouldShowPermissions() &&
303 prompt->experiment()->should_show_expandable_permission_list()) {
304 // If the experiment should hide the permission list initially, create a
305 // simple layout that contains only the header, extension name and icon.
306 scrollable_header_only_ = new CustomScrollableView();
307 CreateLayout(scrollable_header_only_, left_column_width,
308 column_set_id, true);
309 scroll_view_->SetContents(scrollable_header_only_);
311 scroll_view_->SetContents(scrollable_);
314 int dialog_width = left_column_width + 2 * views::kPanelHorizMargin;
315 if (!is_bundle_install())
316 dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset;
318 // Widen the dialog for experiment with checkboxes so that the information
319 // label fits the area to the left of the buttons.
320 if (prompt->experiment()->show_checkboxes())
321 dialog_width += 4 * views::kPanelHorizMargin;
323 if (prompt->has_webstore_data()) {
324 layout->StartRow(0, column_set_id);
325 views::View* rating = new views::View();
326 rating->SetLayoutManager(new views::BoxLayout(
327 views::BoxLayout::kHorizontal, 0, 0, 0));
328 layout->AddView(rating);
329 prompt->AppendRatingStars(AddResourceIcon, rating);
331 const gfx::FontList& small_font_list =
332 rb.GetFontList(ui::ResourceBundle::SmallFont);
333 views::Label* rating_count =
334 new views::Label(prompt->GetRatingCount(), small_font_list);
335 // Add some space between the stars and the rating count.
336 rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0));
337 rating->AddChildView(rating_count);
339 layout->StartRow(0, column_set_id);
340 views::Label* user_count =
341 new views::Label(prompt->GetUserCount(), small_font_list);
342 user_count->SetAutoColorReadabilityEnabled(false);
343 user_count->SetEnabledColor(SK_ColorGRAY);
344 layout->AddView(user_count);
346 layout->StartRow(0, column_set_id);
347 views::Link* store_link = new views::Link(
348 l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK));
349 store_link->SetFontList(small_font_list);
350 store_link->set_listener(this);
351 layout->AddView(store_link);
354 if (is_bundle_install()) {
355 BundleInstaller::ItemList items = prompt->bundle()->GetItemsWithState(
356 BundleInstaller::Item::STATE_PENDING);
357 for (size_t i = 0; i < items.size(); ++i) {
358 base::string16 extension_name =
359 base::UTF8ToUTF16(items[i].localized_name);
360 base::i18n::AdjustStringForLocaleDirection(&extension_name);
361 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
362 layout->StartRow(0, column_set_id);
363 views::Label* extension_label = new views::Label(
364 PrepareForDisplay(extension_name, true));
365 extension_label->SetMultiLine(true);
366 extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
367 extension_label->SizeToFit(left_column_width);
368 layout->AddView(extension_label);
372 if (prompt->ShouldShowPermissions()) {
373 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
375 if (prompt->GetPermissionCount() > 0) {
376 if (is_inline_install()) {
377 layout->StartRow(0, column_set_id);
378 layout->AddView(new views::Separator(views::Separator::HORIZONTAL),
379 3, 1, views::GridLayout::FILL, views::GridLayout::FILL);
380 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
383 layout->StartRow(0, column_set_id);
384 views::Label* permissions_header = NULL;
385 if (is_bundle_install()) {
386 // We need to pass the FontList in the constructor, rather than calling
387 // SetFontList later, because otherwise SizeToFit mis-judges the width
390 new views::Label(prompt->GetPermissionsHeading(),
391 rb.GetFontList(ui::ResourceBundle::MediumFont));
393 permissions_header = new views::Label(prompt->GetPermissionsHeading());
395 permissions_header->SetMultiLine(true);
396 permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
397 permissions_header->SizeToFit(left_column_width);
398 layout->AddView(permissions_header);
400 for (size_t i = 0; i < prompt->GetPermissionCount(); ++i) {
401 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
402 layout->StartRow(0, column_set_id);
403 views::Label* permission_label =
404 new views::Label(prompt->GetPermission(i));
406 const SkColor kTextHighlight = SK_ColorRED;
407 const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3);
408 if (prompt->experiment()->ShouldHighlightText(
409 prompt->GetPermission(i))) {
410 permission_label->SetAutoColorReadabilityEnabled(false);
411 permission_label->SetEnabledColor(kTextHighlight);
412 } else if (prompt->experiment()->ShouldHighlightBackground(
413 prompt->GetPermission(i))) {
414 permission_label->SetLineHeight(18);
415 permission_label->set_background(
416 views::Background::CreateSolidBackground(kBackgroundHighlight));
419 permission_label->SetMultiLine(true);
420 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
422 if (prompt->experiment()->show_checkboxes()) {
423 permission_label->SizeToFit(left_column_width);
424 layout->AddView(new CheckboxedView(permission_label, this));
427 permission_label->SizeToFit(left_column_width - kBulletWidth);
428 layout->AddView(new BulletedView(permission_label));
431 // If we have more details to provide, show them in collapsed form.
432 if (!prompt->GetPermissionsDetails(i).empty()) {
433 layout->StartRow(0, column_set_id);
434 PermissionDetails details;
436 PrepareForDisplay(prompt->GetPermissionsDetails(i), false));
437 ExpandableContainerView* details_container =
438 new ExpandableContainerView(
439 this, base::string16(), details, left_column_width,
441 layout->AddView(details_container);
444 if (prompt->experiment()->should_show_inline_explanations()) {
445 base::string16 explanation =
446 prompt->experiment()->GetInlineExplanation(
447 prompt->GetPermission(i));
448 if (!explanation.empty()) {
449 PermissionDetails details;
450 details.push_back(explanation);
451 ExpandableContainerView* container =
452 new ExpandableContainerView(this, base::string16(), details,
455 // Inline explanations are expanded by default if there is
456 // no "Show details" link.
457 if (!prompt->experiment()->show_details_link())
458 container->ExpandWithoutAnimation();
459 layout->StartRow(0, column_set_id);
460 layout->AddView(container);
461 inline_explanations_.push_back(container);
466 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
467 layout->StartRow(0, column_set_id);
468 views::Label* permission_label = new views::Label(
469 l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS));
470 permission_label->SetMultiLine(true);
471 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
472 permission_label->SizeToFit(left_column_width);
473 layout->AddView(permission_label);
477 if (prompt->GetRetainedFileCount()) {
478 // Slide in under the permissions, if there are any. If there are
479 // either, the retained files prompt stretches all the way to the
480 // right of the dialog. If there are no permissions, the retained
481 // files prompt just takes up the left column.
482 int space_for_files = left_column_width;
483 if (prompt->GetPermissionCount()) {
484 space_for_files += kIconSize;
485 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
486 column_set->AddColumn(views::GridLayout::FILL,
487 views::GridLayout::FILL,
489 views::GridLayout::USE_PREF,
494 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing);
496 layout->StartRow(0, column_set_id);
497 views::Label* retained_files_header = NULL;
498 retained_files_header = new views::Label(prompt->GetRetainedFilesHeading());
499 retained_files_header->SetMultiLine(true);
500 retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
501 retained_files_header->SizeToFit(space_for_files);
502 layout->AddView(retained_files_header);
504 layout->StartRow(0, column_set_id);
505 PermissionDetails details;
506 for (size_t i = 0; i < prompt->GetRetainedFileCount(); ++i)
507 details.push_back(prompt->GetRetainedFile(i));
508 ExpandableContainerView* issue_advice_view =
509 new ExpandableContainerView(
510 this, base::string16(), details, space_for_files,
512 layout->AddView(issue_advice_view);
515 DCHECK(prompt->type() >= 0);
516 UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type",
518 ExtensionInstallPrompt::NUM_PROMPT_TYPES);
520 if (prompt->ShouldShowPermissions()) {
521 if (prompt->ShouldShowExplanationText()) {
522 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
523 column_set->AddColumn(views::GridLayout::LEADING,
524 views::GridLayout::FILL,
526 views::GridLayout::USE_PREF,
529 // Add two rows of space so that the text stands out.
530 layout->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing);
532 layout->StartRow(0, column_set_id);
533 views::Label* explanation =
534 new views::Label(prompt->experiment()->GetExplanationText());
535 explanation->SetMultiLine(true);
536 explanation->SetHorizontalAlignment(gfx::ALIGN_LEFT);
537 explanation->SizeToFit(left_column_width + kIconSize);
538 layout->AddView(explanation);
541 if (prompt->experiment()->should_show_expandable_permission_list() ||
542 (prompt->experiment()->show_details_link() &&
543 prompt->experiment()->should_show_inline_explanations() &&
544 !inline_explanations_.empty())) {
545 // Don't show the "Show details" link if there are retained
546 // files. These have their own "Show details" links and having
547 // multiple levels of links is confusing.
548 if (prompt->GetRetainedFileCount() == 0) {
550 prompt->experiment()->should_show_expandable_permission_list()
551 ? IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS
552 : IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_DETAILS;
553 show_details_link_ = new views::Link(
554 l10n_util::GetStringUTF16(text_id));
555 show_details_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
556 show_details_link_->set_listener(this);
557 UpdateLinkActionHistogram(LINK_SHOWN);
559 UpdateLinkActionHistogram(LINK_NOT_SHOWN);
563 if (prompt->experiment()->show_checkboxes()) {
564 checkbox_info_label_ = new views::Label(
565 l10n_util::GetStringUTF16(
566 IDS_EXTENSION_PROMPT_EXPERIMENT_CHECKBOX_INFO));
567 checkbox_info_label_->SetMultiLine(true);
568 checkbox_info_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
569 checkbox_info_label_->SetAutoColorReadabilityEnabled(false);
570 checkbox_info_label_->SetEnabledColor(kLighterLabelColor);
574 gfx::Size scrollable_size = scrollable_->GetPreferredSize();
575 scrollable_->SetBoundsRect(gfx::Rect(scrollable_size));
576 dialog_size_ = gfx::Size(
578 std::min(scrollable_size.height(), kDialogMaxHeight));
580 if (scrollable_header_only_) {
581 gfx::Size header_only_size = scrollable_header_only_->GetPreferredSize();
582 scrollable_header_only_->SetBoundsRect(gfx::Rect(header_only_size));
583 dialog_size_ = gfx::Size(
584 dialog_width, std::min(header_only_size.height(), kDialogMaxHeight));
587 std::string event_name = ExperienceSamplingEvent::kExtensionInstallDialog;
589 ExtensionInstallPrompt::PromptTypeToString(prompt_->type()));
590 sampling_event_ = ExperienceSamplingEvent::Create(event_name);
593 ExtensionInstallDialogView::~ExtensionInstallDialogView() {}
595 views::GridLayout* ExtensionInstallDialogView::CreateLayout(
597 int left_column_width,
599 bool single_detail_row) const {
600 views::GridLayout* layout = views::GridLayout::CreatePanel(parent);
601 parent->SetLayoutManager(layout);
603 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
604 column_set->AddColumn(views::GridLayout::LEADING,
605 views::GridLayout::FILL,
607 views::GridLayout::USE_PREF,
610 if (!is_bundle_install()) {
611 column_set->AddPaddingColumn(0, views::kPanelHorizMargin);
612 column_set->AddColumn(views::GridLayout::TRAILING,
613 views::GridLayout::LEADING,
615 views::GridLayout::USE_PREF,
620 layout->StartRow(0, column_set_id);
622 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
624 views::Label* heading = new views::Label(
625 prompt_->GetHeading(), rb.GetFontList(ui::ResourceBundle::MediumFont));
626 heading->SetMultiLine(true);
627 heading->SetHorizontalAlignment(gfx::ALIGN_LEFT);
628 heading->SizeToFit(left_column_width);
629 layout->AddView(heading);
631 if (!is_bundle_install()) {
632 // Scale down to icon size, but allow smaller icons (don't scale up).
633 const gfx::ImageSkia* image = prompt_->icon().ToImageSkia();
634 gfx::Size size(image->width(), image->height());
635 if (size.width() > kIconSize || size.height() > kIconSize)
636 size = gfx::Size(kIconSize, kIconSize);
637 views::ImageView* icon = new views::ImageView();
638 icon->SetImageSize(size);
639 icon->SetImage(*image);
640 icon->SetHorizontalAlignment(views::ImageView::CENTER);
641 icon->SetVerticalAlignment(views::ImageView::CENTER);
642 if (single_detail_row) {
643 layout->AddView(icon);
645 int icon_row_span = 1;
646 if (is_inline_install()) {
647 // Also span the rating, user_count and store_link rows.
649 } else if (prompt_->ShouldShowPermissions()) {
650 size_t permission_count = prompt_->GetPermissionCount();
651 // Also span the permission header and each of the permission rows (all
652 // have a padding row above it). This also works for the 'no special
653 // permissions' case.
654 icon_row_span = 3 + permission_count * 2;
655 } else if (prompt_->GetRetainedFileCount()) {
656 // Also span the permission header and the retained files container.
659 layout->AddView(icon, 1, icon_row_span);
665 void ExtensionInstallDialogView::ContentsChanged() {
669 void ExtensionInstallDialogView::ViewHierarchyChanged(
670 const ViewHierarchyChangedDetails& details) {
671 // Since we want the links to show up in the same visual row as the accept
672 // and cancel buttons, which is provided by the framework, we must add the
673 // buttons to the non-client view, which is the parent of this view.
674 // Similarly, when we're removed from the view hierarchy, we must take care
675 // to clean up those items as well.
676 if (details.child == this) {
677 if (details.is_add) {
678 if (show_details_link_)
679 details.parent->AddChildView(show_details_link_);
680 if (checkbox_info_label_)
681 details.parent->AddChildView(checkbox_info_label_);
683 if (show_details_link_)
684 details.parent->RemoveChildView(show_details_link_);
685 if (checkbox_info_label_)
686 details.parent->RemoveChildView(checkbox_info_label_);
691 int ExtensionInstallDialogView::GetDialogButtons() const {
692 int buttons = prompt_->GetDialogButtons();
693 // Simply having just an OK button is *not* supported. See comment on function
694 // GetDialogButtons in dialog_delegate.h for reasons.
695 DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0);
699 base::string16 ExtensionInstallDialogView::GetDialogButtonLabel(
700 ui::DialogButton button) const {
702 case ui::DIALOG_BUTTON_OK:
703 return prompt_->GetAcceptButtonLabel();
704 case ui::DIALOG_BUTTON_CANCEL:
705 return prompt_->HasAbortButtonLabel()
706 ? prompt_->GetAbortButtonLabel()
707 : l10n_util::GetStringUTF16(IDS_CANCEL);
710 return base::string16();
714 int ExtensionInstallDialogView::GetDefaultDialogButton() const {
715 return ui::DIALOG_BUTTON_CANCEL;
718 bool ExtensionInstallDialogView::Cancel() {
719 UpdateInstallResultHistogram(false);
720 if (sampling_event_.get())
721 sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kDeny);
722 delegate_->InstallUIAbort(true);
726 bool ExtensionInstallDialogView::Accept() {
727 UpdateInstallResultHistogram(true);
728 if (sampling_event_.get())
729 sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kProceed);
730 delegate_->InstallUIProceed();
734 ui::ModalType ExtensionInstallDialogView::GetModalType() const {
735 return ui::MODAL_TYPE_WINDOW;
738 base::string16 ExtensionInstallDialogView::GetWindowTitle() const {
739 return prompt_->GetDialogTitle();
742 void ExtensionInstallDialogView::LinkClicked(views::Link* source,
744 if (source == show_details_link_) {
745 UpdateLinkActionHistogram(LINK_CLICKED);
746 // Show details link is used to either reveal whole permission list or to
747 // reveal inline explanations.
748 if (prompt_->experiment()->should_show_expandable_permission_list()) {
749 gfx::Rect bounds = GetWidget()->GetWindowBoundsInScreen();
750 int spacing = bounds.height() -
751 scrollable_header_only_->GetPreferredSize().height();
752 int content_height = std::min(scrollable_->GetPreferredSize().height(),
754 bounds.set_height(spacing + content_height);
755 scroll_view_->SetContents(scrollable_);
756 GetWidget()->SetBoundsConstrained(bounds);
759 ToggleInlineExplanations();
761 show_details_link_->SetVisible(false);
763 GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
764 prompt_->extension()->id());
765 OpenURLParams params(
766 store_url, Referrer(), NEW_FOREGROUND_TAB,
767 content::PAGE_TRANSITION_LINK,
769 navigator_->OpenURL(params);
770 GetWidget()->Close();
774 void ExtensionInstallDialogView::ToggleInlineExplanations() {
775 for (InlineExplanations::iterator it = inline_explanations_.begin();
776 it != inline_explanations_.end(); ++it)
777 (*it)->ToggleDetailLevel();
780 void ExtensionInstallDialogView::Layout() {
781 scroll_view_->SetBounds(0, 0, width(), height());
783 if (show_details_link_ || checkbox_info_label_) {
784 views::LabelButton* cancel_button = GetDialogClientView()->cancel_button();
785 gfx::Rect parent_bounds = parent()->GetContentsBounds();
786 // By default, layouts have an inset of kButtonHEdgeMarginNew. In order to
787 // align the link horizontally with the left side of the contents of the
788 // layout, put a horizontal margin with this amount.
789 const int horizontal_margin = views::kButtonHEdgeMarginNew;
790 const int vertical_margin = views::kButtonVEdgeMarginNew;
791 int y_buttons = parent_bounds.bottom() -
792 cancel_button->GetPreferredSize().height() - vertical_margin;
793 int max_width = dialog_size_.width() - cancel_button->width() * 2 -
794 horizontal_margin * 2 - views::kRelatedButtonHSpacing;
795 if (show_details_link_) {
796 gfx::Size link_size = show_details_link_->GetPreferredSize();
797 show_details_link_->SetBounds(
799 y_buttons + (cancel_button->height() - link_size.height()) / 2,
800 link_size.width(), link_size.height());
802 if (checkbox_info_label_) {
803 gfx::Size label_size = checkbox_info_label_->GetPreferredSize();
804 checkbox_info_label_->SetBounds(
806 y_buttons + (cancel_button->height() - label_size.height()) / 2,
807 label_size.width(), label_size.height());
808 checkbox_info_label_->SizeToFit(max_width);
811 // Disable accept button if there are unchecked boxes and
812 // the experiment is on.
813 if (prompt_->experiment()->show_checkboxes())
814 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
816 DialogDelegateView::Layout();
819 gfx::Size ExtensionInstallDialogView::GetPreferredSize() const {
823 void ExtensionInstallDialogView::ButtonPressed(views::Button* sender,
824 const ui::Event& event) {
825 if (std::string(views::Checkbox::kViewClassName) == sender->GetClassName()) {
826 views::Checkbox* checkbox = static_cast<views::Checkbox*>(sender);
827 if (checkbox->checked())
832 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0);
833 checkbox_info_label_->SetVisible(unchecked_boxes_ > 0);
837 void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted)
839 if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT)
840 UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted);
843 void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type)
845 if (prompt_->experiment()->should_show_expandable_permission_list()) {
846 // The clickable link in the UI is "Show Permissions".
847 UMA_HISTOGRAM_ENUMERATION(
848 "Extensions.InstallPromptExperiment.ShowPermissions",
852 // The clickable link in the UI is "Show Details".
853 UMA_HISTOGRAM_ENUMERATION(
854 "Extensions.InstallPromptExperiment.ShowDetails",
860 // ExpandableContainerView::DetailsView ----------------------------------------
862 ExpandableContainerView::DetailsView::DetailsView(int horizontal_space,
863 bool parent_bulleted,
865 : layout_(new views::GridLayout(this)),
867 lighter_color_(lighter_color) {
868 SetLayoutManager(layout_);
869 views::ColumnSet* column_set = layout_->AddColumnSet(0);
870 // If the parent is using bullets for its items, then a padding of one unit
871 // will make the child item (which has no bullet) look like a sibling of its
872 // parent. Therefore increase the indentation by one more unit to show that it
873 // is in fact a child item (with no missing bullet) and not a sibling.
875 views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1);
876 column_set->AddPaddingColumn(0, padding);
877 column_set->AddColumn(views::GridLayout::LEADING,
878 views::GridLayout::LEADING,
880 views::GridLayout::FIXED,
881 horizontal_space - padding,
885 void ExpandableContainerView::DetailsView::AddDetail(
886 const base::string16& detail) {
887 layout_->StartRowWithPadding(0, 0,
888 0, views::kRelatedControlSmallVerticalSpacing);
889 views::Label* detail_label =
890 new views::Label(PrepareForDisplay(detail, false));
891 detail_label->SetMultiLine(true);
892 detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
893 if (lighter_color_) {
894 detail_label->SetEnabledColor(kLighterLabelColor);
895 detail_label->SetAutoColorReadabilityEnabled(false);
897 layout_->AddView(detail_label);
900 gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const {
901 gfx::Size size = views::View::GetPreferredSize();
902 return gfx::Size(size.width(), size.height() * state_);
905 void ExpandableContainerView::DetailsView::AnimateToState(double state) {
907 PreferredSizeChanged();
911 // ExpandableContainerView -----------------------------------------------------
913 ExpandableContainerView::ExpandableContainerView(
914 ExtensionInstallDialogView* owner,
915 const base::string16& description,
916 const PermissionDetails& details,
917 int horizontal_space,
918 bool parent_bulleted,
919 bool show_expand_link,
920 bool lighter_color_details)
924 slide_animation_(this),
927 views::GridLayout* layout = new views::GridLayout(this);
928 SetLayoutManager(layout);
929 int column_set_id = 0;
930 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id);
931 column_set->AddColumn(views::GridLayout::LEADING,
932 views::GridLayout::LEADING,
934 views::GridLayout::USE_PREF,
937 if (!description.empty()) {
938 layout->StartRow(0, column_set_id);
940 views::Label* description_label = new views::Label(description);
941 description_label->SetMultiLine(true);
942 description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
943 description_label->SizeToFit(horizontal_space);
944 layout->AddView(new BulletedView(description_label));
950 details_view_ = new DetailsView(horizontal_space, parent_bulleted,
951 lighter_color_details);
953 layout->StartRow(0, column_set_id);
954 layout->AddView(details_view_);
956 for (size_t i = 0; i < details.size(); ++i)
957 details_view_->AddDetail(details[i]);
959 // TODO(meacer): Remove show_expand_link when the experiment is completed.
960 if (show_expand_link) {
961 views::Link* link = new views::Link(
962 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
964 // Make sure the link width column is as wide as needed for both Show and
965 // Hide details, so that the arrow doesn't shift horizontally when we
968 views::kRelatedControlHorizontalSpacing +
969 std::max(gfx::GetStringWidth(
970 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS),
973 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS),
976 column_set = layout->AddColumnSet(++column_set_id);
977 // Padding to the left of the More Details column. If the parent is using
978 // bullets for its items, then a padding of one unit will make the child
979 // item (which has no bullet) look like a sibling of its parent. Therefore
980 // increase the indentation by one more unit to show that it is in fact a
981 // child item (with no missing bullet) and not a sibling.
982 column_set->AddPaddingColumn(
983 0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1));
984 // The More Details column.
985 column_set->AddColumn(views::GridLayout::LEADING,
986 views::GridLayout::LEADING,
988 views::GridLayout::FIXED,
991 // The Up/Down arrow column.
992 column_set->AddColumn(views::GridLayout::LEADING,
993 views::GridLayout::LEADING,
995 views::GridLayout::USE_PREF,
999 // Add the More Details link.
1000 layout->StartRow(0, column_set_id);
1001 more_details_ = link;
1002 more_details_->set_listener(this);
1003 more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
1004 layout->AddView(more_details_);
1006 // Add the arrow after the More Details link.
1007 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
1008 arrow_toggle_ = new views::ImageButton(this);
1009 arrow_toggle_->SetImage(views::Button::STATE_NORMAL,
1010 rb.GetImageSkiaNamed(IDR_DOWN_ARROW));
1011 layout->AddView(arrow_toggle_);
1015 ExpandableContainerView::~ExpandableContainerView() {
1018 void ExpandableContainerView::ButtonPressed(
1019 views::Button* sender, const ui::Event& event) {
1020 ToggleDetailLevel();
1023 void ExpandableContainerView::LinkClicked(
1024 views::Link* source, int event_flags) {
1025 ToggleDetailLevel();
1028 void ExpandableContainerView::AnimationProgressed(
1029 const gfx::Animation* animation) {
1030 DCHECK_EQ(&slide_animation_, animation);
1032 details_view_->AnimateToState(animation->GetCurrentValue());
1035 void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) {
1036 if (arrow_toggle_) {
1037 if (animation->GetCurrentValue() != 0.0) {
1038 arrow_toggle_->SetImage(
1039 views::Button::STATE_NORMAL,
1040 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
1043 arrow_toggle_->SetImage(
1044 views::Button::STATE_NORMAL,
1045 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
1049 if (more_details_) {
1050 more_details_->SetText(expanded_ ?
1051 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) :
1052 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS));
1056 void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) {
1057 owner_->ContentsChanged();
1060 void ExpandableContainerView::ToggleDetailLevel() {
1061 expanded_ = !expanded_;
1063 if (slide_animation_.IsShowing())
1064 slide_animation_.Hide();
1066 slide_animation_.Show();
1069 void ExpandableContainerView::ExpandWithoutAnimation() {
1071 details_view_->AnimateToState(1.0);
1075 ExtensionInstallPrompt::ShowDialogCallback
1076 ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
1077 return base::Bind(&ShowExtensionInstallDialogImpl);