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.
7 #include "base/basictypes.h"
8 #include "base/command_line.h"
9 #include "base/compiler_specific.h"
10 #include "base/i18n/rtl.h"
11 #include "base/metrics/histogram.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/extensions/bundle_installer.h"
15 #include "chrome/browser/extensions/extension_install_prompt.h"
16 #include "chrome/browser/extensions/extension_install_prompt_experiment.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/ui/views/constrained_window_views.h"
19 #include "chrome/common/chrome_switches.h"
20 #include "chrome/common/extensions/extension_constants.h"
21 #include "chrome/installer/util/browser_distribution.h"
22 #include "content/public/browser/page_navigator.h"
23 #include "content/public/browser/web_contents.h"
24 #include "extensions/common/extension.h"
25 #include "grit/chromium_strings.h"
26 #include "grit/generated_resources.h"
27 #include "grit/google_chrome_strings.h"
28 #include "grit/theme_resources.h"
29 #include "ui/base/l10n/l10n_util.h"
30 #include "ui/base/resource/resource_bundle.h"
31 #include "ui/gfx/animation/animation_delegate.h"
32 #include "ui/gfx/animation/slide_animation.h"
33 #include "ui/gfx/text_utils.h"
34 #include "ui/gfx/transform.h"
35 #include "ui/views/background.h"
36 #include "ui/views/border.h"
37 #include "ui/views/controls/button/checkbox.h"
38 #include "ui/views/controls/button/image_button.h"
39 #include "ui/views/controls/button/label_button.h"
40 #include "ui/views/controls/image_view.h"
41 #include "ui/views/controls/label.h"
42 #include "ui/views/controls/link.h"
43 #include "ui/views/controls/link_listener.h"
44 #include "ui/views/controls/scroll_view.h"
45 #include "ui/views/controls/separator.h"
46 #include "ui/views/layout/box_layout.h"
47 #include "ui/views/layout/grid_layout.h"
48 #include "ui/views/layout/layout_constants.h"
49 #include "ui/views/view.h"
50 #include "ui/views/widget/widget.h"
51 #include "ui/views/window/dialog_client_view.h"
52 #include "ui/views/window/dialog_delegate.h"
54 using content::OpenURLParams
;
55 using content::Referrer
;
56 using extensions::BundleInstaller
;
60 // Size of extension icon in top left of dialog.
61 const int kIconSize
= 64;
63 // We offset the icon a little bit from the right edge of the dialog, to make it
64 // align with the button below it.
65 const int kIconOffset
= 16;
67 // The dialog will resize based on its content, but this sets a maximum height
68 // before overflowing a scrollbar.
69 const int kDialogMaxHeight
= 300;
71 // Width of the left column of the dialog when the extension requests
73 const int kPermissionsLeftColumnWidth
= 250;
75 // Width of the left column of the dialog when the extension requests no
77 const int kNoPermissionsLeftColumnWidth
= 200;
79 // Width of the left column for bundle install prompts. There's only one column
80 // in this case, so make it wider than normal.
81 const int kBundleLeftColumnWidth
= 300;
83 // Width of the left column for external install prompts. The text is long in
84 // this case, so make it wider than normal.
85 const int kExternalInstallLeftColumnWidth
= 350;
87 // Lighter color for labels.
88 const SkColor kLighterLabelColor
= SkColorSetRGB(0x99, 0x99, 0x99);
90 // Represents an action on a clickable link created by the install prompt
91 // experiment. This is used to group the actions in UMA histograms named
92 // Extensions.InstallPromptExperiment.ShowDetails and
93 // Extensions.InstallPromptExperiment.ShowPermissions.
94 enum ExperimentLinkAction
{
101 typedef std::vector
<base::string16
> PermissionDetails
;
102 class ExpandableContainerView
;
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
,
120 // A custom scrollable view implementation for the dialog.
121 class CustomScrollableView
: public views::View
{
123 CustomScrollableView();
124 virtual ~CustomScrollableView();
127 virtual void Layout() OVERRIDE
;
129 DISALLOW_COPY_AND_ASSIGN(CustomScrollableView
);
132 // Implements the extension installation dialog for TOOLKIT_VIEWS.
133 class ExtensionInstallDialogView
: public views::DialogDelegateView
,
134 public views::LinkListener
,
135 public views::ButtonListener
{
137 ExtensionInstallDialogView(content::PageNavigator
* navigator
,
138 ExtensionInstallPrompt::Delegate
* delegate
,
139 const ExtensionInstallPrompt::Prompt
& prompt
);
140 virtual ~ExtensionInstallDialogView();
142 // Called when one of the child elements has expanded/collapsed.
143 void ContentsChanged();
146 // views::DialogDelegateView:
147 virtual int GetDialogButtons() const OVERRIDE
;
148 virtual base::string16
GetDialogButtonLabel(
149 ui::DialogButton button
) const OVERRIDE
;
150 virtual int GetDefaultDialogButton() const OVERRIDE
;
151 virtual bool Cancel() OVERRIDE
;
152 virtual bool Accept() OVERRIDE
;
153 virtual ui::ModalType
GetModalType() const OVERRIDE
;
154 virtual base::string16
GetWindowTitle() const OVERRIDE
;
155 virtual void Layout() OVERRIDE
;
156 virtual gfx::Size
GetPreferredSize() const OVERRIDE
;
157 virtual void ViewHierarchyChanged(
158 const ViewHierarchyChangedDetails
& details
) OVERRIDE
;
160 // views::LinkListener:
161 virtual void LinkClicked(views::Link
* source
, int event_flags
) OVERRIDE
;
163 // views::ButtonListener:
164 virtual void ButtonPressed(views::Button
* sender
,
165 const ui::Event
& event
) OVERRIDE
;
167 // Experimental: Toggles inline permission explanations with an animation.
168 void ToggleInlineExplanations();
170 // Creates a layout consisting of dialog header, extension name and icon.
171 views::GridLayout
* CreateLayout(
173 int left_column_width
,
175 bool single_detail_row
) const;
177 bool is_inline_install() const {
178 return prompt_
.type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT
;
181 bool is_bundle_install() const {
182 return prompt_
.type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT
;
185 bool is_external_install() const {
186 return prompt_
.type() == ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT
;
189 // Updates the histogram that holds installation accepted/aborted data.
190 void UpdateInstallResultHistogram(bool accepted
) const;
192 // Updates the histogram that holds data about whether "Show details" or
193 // "Show permissions" links were shown and/or clicked.
194 void UpdateLinkActionHistogram(int action_type
) const;
196 content::PageNavigator
* navigator_
;
197 ExtensionInstallPrompt::Delegate
* delegate_
;
198 const ExtensionInstallPrompt::Prompt
& prompt_
;
200 // The scroll view containing all the details for the dialog (including all
201 // collapsible/expandable sections).
202 views::ScrollView
* scroll_view_
;
204 // The container view for the scroll view.
205 CustomScrollableView
* scrollable_
;
207 // The container for the simpler view with only the dialog header and the
208 // extension icon. Used for the experiment where the permissions are
209 // initially hidden when the dialog shows.
210 CustomScrollableView
* scrollable_header_only_
;
212 // The preferred size of the dialog.
213 gfx::Size dialog_size_
;
215 // Experimental: "Show details" link to expand inline explanations and reveal
217 views::Link
* show_details_link_
;
219 // Experimental: Label for showing information about the checkboxes.
220 views::Label
* checkbox_info_label_
;
222 // Experimental: Contains pointers to inline explanation views.
223 typedef std::vector
<ExpandableContainerView
*> InlineExplanations
;
224 InlineExplanations inline_explanations_
;
226 // Experimental: Number of unchecked checkboxes in the permission list.
227 // If this becomes zero, the accept button is enabled, otherwise disabled.
228 int unchecked_boxes_
;
230 DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogView
);
233 // A simple view that prepends a view with a bullet with the help of a grid
235 class BulletedView
: public views::View
{
237 explicit BulletedView(views::View
* view
);
239 DISALLOW_COPY_AND_ASSIGN(BulletedView
);
242 BulletedView::BulletedView(views::View
* view
) {
243 views::GridLayout
* layout
= new views::GridLayout(this);
244 SetLayoutManager(layout
);
245 views::ColumnSet
* column_set
= layout
->AddColumnSet(0);
246 column_set
->AddColumn(views::GridLayout::LEADING
,
247 views::GridLayout::LEADING
,
249 views::GridLayout::USE_PREF
,
252 column_set
->AddColumn(views::GridLayout::LEADING
,
253 views::GridLayout::LEADING
,
255 views::GridLayout::USE_PREF
,
258 layout
->StartRow(0, 0);
259 layout
->AddView(new views::Label(PrepareForDisplay(base::string16(), true)));
260 layout
->AddView(view
);
263 // A simple view that prepends a view with a checkbox with the help of a grid
264 // layout. Used for the permission experiment.
265 // TODO(meacer): Remove once the experiment is completed.
266 class CheckboxedView
: public views::View
{
268 CheckboxedView(views::View
* view
, views::ButtonListener
* listener
);
270 DISALLOW_COPY_AND_ASSIGN(CheckboxedView
);
273 CheckboxedView::CheckboxedView(views::View
* view
,
274 views::ButtonListener
* listener
) {
275 views::GridLayout
* layout
= new views::GridLayout(this);
276 SetLayoutManager(layout
);
277 views::ColumnSet
* column_set
= layout
->AddColumnSet(0);
278 column_set
->AddColumn(views::GridLayout::LEADING
,
279 views::GridLayout::LEADING
,
281 views::GridLayout::USE_PREF
,
284 column_set
->AddColumn(views::GridLayout::LEADING
,
285 views::GridLayout::LEADING
,
287 views::GridLayout::USE_PREF
,
290 layout
->StartRow(0, 0);
291 views::Checkbox
* checkbox
= new views::Checkbox(base::string16());
292 checkbox
->set_listener(listener
);
293 // Alignment needs to be explicitly set again here, otherwise the views are
294 // not vertically centered.
295 layout
->AddView(checkbox
, 1, 1,
296 views::GridLayout::LEADING
, views::GridLayout::CENTER
);
297 layout
->AddView(view
, 1, 1,
298 views::GridLayout::LEADING
, views::GridLayout::CENTER
);
301 // A view to display text with an expandable details section.
302 class ExpandableContainerView
: public views::View
,
303 public views::ButtonListener
,
304 public views::LinkListener
,
305 public gfx::AnimationDelegate
{
307 ExpandableContainerView(ExtensionInstallDialogView
* owner
,
308 const base::string16
& description
,
309 const PermissionDetails
& details
,
310 int horizontal_space
,
311 bool parent_bulleted
,
312 bool show_expand_link
,
313 bool lighter_color_details
);
314 virtual ~ExpandableContainerView();
317 virtual void ChildPreferredSizeChanged(views::View
* child
) OVERRIDE
;
319 // views::ButtonListener:
320 virtual void ButtonPressed(views::Button
* sender
,
321 const ui::Event
& event
) OVERRIDE
;
323 // views::LinkListener:
324 virtual void LinkClicked(views::Link
* source
, int event_flags
) OVERRIDE
;
326 // gfx::AnimationDelegate:
327 virtual void AnimationProgressed(const gfx::Animation
* animation
) OVERRIDE
;
328 virtual void AnimationEnded(const gfx::Animation
* animation
) OVERRIDE
;
330 // Expand/Collapse the detail section for this ExpandableContainerView.
331 void ToggleDetailLevel();
333 // Expand the detail section without any animation.
334 // TODO(meacer): Remove once the experiment is completed.
335 void ExpandWithoutAnimation();
338 // A view which displays all the details of an IssueAdviceInfoEntry.
339 class DetailsView
: public views::View
{
341 explicit DetailsView(int horizontal_space
, bool parent_bulleted
,
343 virtual ~DetailsView() {}
346 virtual gfx::Size
GetPreferredSize() const OVERRIDE
;
348 void AddDetail(const base::string16
& detail
);
350 // Animates this to be a height proportional to |state|.
351 void AnimateToState(double state
);
354 views::GridLayout
* layout_
;
357 // Whether the detail text should be shown with a lighter color.
360 DISALLOW_COPY_AND_ASSIGN(DetailsView
);
363 // The dialog that owns |this|. It's also an ancestor in the View hierarchy.
364 ExtensionInstallDialogView
* owner_
;
366 // A view for showing |issue_advice.details|.
367 DetailsView
* details_view_
;
369 // The 'more details' link shown under the heading (changes to 'hide details'
370 // when the details section is expanded).
371 views::Link
* more_details_
;
373 gfx::SlideAnimation slide_animation_
;
375 // The up/down arrow next to the 'more detail' link (points up/down depending
376 // on whether the details section is expanded).
377 views::ImageButton
* arrow_toggle_
;
379 // Whether the details section is expanded.
382 DISALLOW_COPY_AND_ASSIGN(ExpandableContainerView
);
385 void ShowExtensionInstallDialogImpl(
386 const ExtensionInstallPrompt::ShowParams
& show_params
,
387 ExtensionInstallPrompt::Delegate
* delegate
,
388 const ExtensionInstallPrompt::Prompt
& prompt
) {
389 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
390 CreateBrowserModalDialogViews(
391 new ExtensionInstallDialogView(show_params
.navigator
, delegate
, prompt
),
392 show_params
.parent_window
)->Show();
397 CustomScrollableView::CustomScrollableView() {}
398 CustomScrollableView::~CustomScrollableView() {}
400 void CustomScrollableView::Layout() {
401 SetBounds(x(), y(), width(), GetHeightForWidth(width()));
402 views::View::Layout();
405 ExtensionInstallDialogView::ExtensionInstallDialogView(
406 content::PageNavigator
* navigator
,
407 ExtensionInstallPrompt::Delegate
* delegate
,
408 const ExtensionInstallPrompt::Prompt
& prompt
)
409 : navigator_(navigator
),
414 scrollable_header_only_(NULL
),
415 show_details_link_(NULL
),
416 checkbox_info_label_(NULL
),
417 unchecked_boxes_(0) {
418 // Possible grid layouts without ExtensionPermissionDialog experiment:
420 // w/ permissions no permissions
421 // +--------------------+------+ +--------------+------+
422 // | heading | icon | | heading | icon |
423 // +--------------------| | +--------------| |
424 // | rating | | | rating | |
425 // +--------------------| | +--------------+ |
426 // | user_count | | | user_count | |
427 // +--------------------| | +--------------| |
428 // | store_link | | | store_link | |
429 // +--------------------+------+ +--------------+------+
431 // +--------------------+------+
432 // | permissions_header | |
433 // +--------------------+------+
435 // +--------------------+------+
437 // +--------------------+------+
440 // w/ permissions no permissions
441 // +--------------------+------+ +--------------+------+
442 // | heading | icon | | heading | icon |
443 // +--------------------| | +--------------+------+
444 // | permissions_header | |
445 // +--------------------| |
447 // +--------------------| |
449 // +--------------------+------+
451 // If the ExtensionPermissionDialog is on, the layout is modified depending
452 // on the experiment group. For text only experiment, a footer is added at the
453 // bottom of the layouts. For others, inline details are added below some of
456 // Regular install w/ permissions and footer (experiment):
457 // +--------------------+------+
458 // | heading | icon |
459 // +--------------------| |
460 // | permissions_header | |
461 // +--------------------| |
463 // +--------------------| |
465 // +--------------------+------+
467 // +--------------------+------+
469 // Regular install w/ permissions and inline explanations (experiment):
470 // +--------------------+------+
471 // | heading | icon |
472 // +--------------------| |
473 // | permissions_header | |
474 // +--------------------| |
476 // +--------------------| |
477 // | explanation1 | |
478 // +--------------------| |
480 // +--------------------| |
481 // | explanation2 | |
482 // +--------------------+------+
484 // Regular install w/ permissions and inline explanations (experiment):
485 // +--------------------+------+
486 // | heading | icon |
487 // +--------------------| |
488 // | permissions_header | |
489 // +--------------------| |
490 // |checkbox|permission1| |
491 // +--------------------| |
492 // |checkbox|permission2| |
493 // +--------------------+------+
495 // Additionally, links or informational text is added to non-client areas of
496 // the dialog depending on the experiment group.
498 int left_column_width
=
499 (prompt
.ShouldShowPermissions() +
500 prompt
.GetRetainedFileCount()) > 0 ?
501 kPermissionsLeftColumnWidth
: kNoPermissionsLeftColumnWidth
;
502 if (is_bundle_install())
503 left_column_width
= kBundleLeftColumnWidth
;
504 if (is_external_install())
505 left_column_width
= kExternalInstallLeftColumnWidth
;
507 scroll_view_
= new views::ScrollView();
508 scroll_view_
->set_hide_horizontal_scrollbar(true);
509 AddChildView(scroll_view_
);
511 int column_set_id
= 0;
512 // Create the full scrollable view which will contain all the information
513 // including the permissions.
514 scrollable_
= new CustomScrollableView();
515 views::GridLayout
* layout
= CreateLayout(
516 scrollable_
, left_column_width
, column_set_id
, false);
517 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
519 if (prompt
.ShouldShowPermissions() &&
520 prompt
.experiment()->should_show_expandable_permission_list()) {
521 // If the experiment should hide the permission list initially, create a
522 // simple layout that contains only the header, extension name and icon.
523 scrollable_header_only_
= new CustomScrollableView();
524 CreateLayout(scrollable_header_only_
, left_column_width
,
525 column_set_id
, true);
526 scroll_view_
->SetContents(scrollable_header_only_
);
528 scroll_view_
->SetContents(scrollable_
);
531 int dialog_width
= left_column_width
+ 2 * views::kPanelHorizMargin
;
532 if (!is_bundle_install())
533 dialog_width
+= views::kPanelHorizMargin
+ kIconSize
+ kIconOffset
;
535 // Widen the dialog for experiment with checkboxes so that the information
536 // label fits the area to the left of the buttons.
537 if (prompt
.experiment()->show_checkboxes())
538 dialog_width
+= 4 * views::kPanelHorizMargin
;
540 if (prompt
.has_webstore_data()) {
541 layout
->StartRow(0, column_set_id
);
542 views::View
* rating
= new views::View();
543 rating
->SetLayoutManager(new views::BoxLayout(
544 views::BoxLayout::kHorizontal
, 0, 0, 0));
545 layout
->AddView(rating
);
546 prompt
.AppendRatingStars(AddResourceIcon
, rating
);
548 const gfx::FontList
& small_font_list
=
549 rb
.GetFontList(ui::ResourceBundle::SmallFont
);
550 views::Label
* rating_count
=
551 new views::Label(prompt
.GetRatingCount(), small_font_list
);
552 // Add some space between the stars and the rating count.
553 rating_count
->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0));
554 rating
->AddChildView(rating_count
);
556 layout
->StartRow(0, column_set_id
);
557 views::Label
* user_count
=
558 new views::Label(prompt
.GetUserCount(), small_font_list
);
559 user_count
->SetAutoColorReadabilityEnabled(false);
560 user_count
->SetEnabledColor(SK_ColorGRAY
);
561 layout
->AddView(user_count
);
563 layout
->StartRow(0, column_set_id
);
564 views::Link
* store_link
= new views::Link(
565 l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK
));
566 store_link
->SetFontList(small_font_list
);
567 store_link
->set_listener(this);
568 layout
->AddView(store_link
);
571 if (is_bundle_install()) {
572 BundleInstaller::ItemList items
= prompt
.bundle()->GetItemsWithState(
573 BundleInstaller::Item::STATE_PENDING
);
574 for (size_t i
= 0; i
< items
.size(); ++i
) {
575 base::string16 extension_name
=
576 base::UTF8ToUTF16(items
[i
].localized_name
);
577 base::i18n::AdjustStringForLocaleDirection(&extension_name
);
578 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
579 layout
->StartRow(0, column_set_id
);
580 views::Label
* extension_label
= new views::Label(
581 PrepareForDisplay(extension_name
, true));
582 extension_label
->SetMultiLine(true);
583 extension_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
584 extension_label
->SizeToFit(left_column_width
);
585 layout
->AddView(extension_label
);
589 if (prompt
.ShouldShowPermissions()) {
590 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
592 if (prompt
.GetPermissionCount() > 0) {
593 if (is_inline_install()) {
594 layout
->StartRow(0, column_set_id
);
595 layout
->AddView(new views::Separator(views::Separator::HORIZONTAL
),
596 3, 1, views::GridLayout::FILL
, views::GridLayout::FILL
);
597 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
600 layout
->StartRow(0, column_set_id
);
601 views::Label
* permissions_header
= NULL
;
602 if (is_bundle_install()) {
603 // We need to pass the FontList in the constructor, rather than calling
604 // SetFontList later, because otherwise SizeToFit mis-judges the width
606 permissions_header
= new views::Label(
607 prompt
.GetPermissionsHeading(),
608 rb
.GetFontList(ui::ResourceBundle::MediumFont
));
610 permissions_header
= new views::Label(prompt
.GetPermissionsHeading());
612 permissions_header
->SetMultiLine(true);
613 permissions_header
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
614 permissions_header
->SizeToFit(left_column_width
);
615 layout
->AddView(permissions_header
);
617 for (size_t i
= 0; i
< prompt
.GetPermissionCount(); ++i
) {
618 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
619 layout
->StartRow(0, column_set_id
);
620 views::Label
* permission_label
=
621 new views::Label(prompt
.GetPermission(i
));
623 const SkColor kTextHighlight
= SK_ColorRED
;
624 const SkColor kBackgroundHighlight
= SkColorSetRGB(0xFB, 0xF7, 0xA3);
625 if (prompt
.experiment()->ShouldHighlightText(
626 prompt
.GetPermission(i
))) {
627 permission_label
->SetAutoColorReadabilityEnabled(false);
628 permission_label
->SetEnabledColor(kTextHighlight
);
629 } else if (prompt
.experiment()->ShouldHighlightBackground(
630 prompt
.GetPermission(i
))) {
631 permission_label
->SetLineHeight(18);
632 permission_label
->set_background(
633 views::Background::CreateSolidBackground(kBackgroundHighlight
));
636 permission_label
->SetMultiLine(true);
637 permission_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
638 permission_label
->SizeToFit(left_column_width
);
640 if (prompt
.experiment()->show_checkboxes()) {
641 layout
->AddView(new CheckboxedView(permission_label
, this));
644 layout
->AddView(new BulletedView(permission_label
));
646 // If we have more details to provide, show them in collapsed form.
647 if (!prompt
.GetPermissionsDetails(i
).empty()) {
648 layout
->StartRow(0, column_set_id
);
649 PermissionDetails details
;
651 PrepareForDisplay(prompt
.GetPermissionsDetails(i
), false));
652 ExpandableContainerView
* details_container
=
653 new ExpandableContainerView(
654 this, base::string16(), details
, left_column_width
,
656 layout
->AddView(details_container
);
659 if (prompt
.experiment()->should_show_inline_explanations()) {
660 base::string16 explanation
=
661 prompt
.experiment()->GetInlineExplanation(
662 prompt
.GetPermission(i
));
663 if (!explanation
.empty()) {
664 PermissionDetails details
;
665 details
.push_back(explanation
);
666 ExpandableContainerView
* container
=
667 new ExpandableContainerView(this, base::string16(), details
,
670 // Inline explanations are expanded by default if there is
671 // no "Show details" link.
672 if (!prompt
.experiment()->show_details_link())
673 container
->ExpandWithoutAnimation();
674 layout
->StartRow(0, column_set_id
);
675 layout
->AddView(container
);
676 inline_explanations_
.push_back(container
);
681 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
682 layout
->StartRow(0, column_set_id
);
683 views::Label
* permission_label
= new views::Label(
684 l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS
));
685 permission_label
->SetMultiLine(true);
686 permission_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
687 permission_label
->SizeToFit(left_column_width
);
688 layout
->AddView(permission_label
);
692 if (prompt
.GetRetainedFileCount()) {
693 // Slide in under the permissions, if there are any. If there are
694 // either, the retained files prompt stretches all the way to the
695 // right of the dialog. If there are no permissions, the retained
696 // files prompt just takes up the left column.
697 int space_for_files
= left_column_width
;
698 if (prompt
.GetPermissionCount()) {
699 space_for_files
+= kIconSize
;
700 views::ColumnSet
* column_set
= layout
->AddColumnSet(++column_set_id
);
701 column_set
->AddColumn(views::GridLayout::FILL
,
702 views::GridLayout::FILL
,
704 views::GridLayout::USE_PREF
,
709 layout
->AddPaddingRow(0, views::kRelatedControlVerticalSpacing
);
711 layout
->StartRow(0, column_set_id
);
712 views::Label
* retained_files_header
= NULL
;
713 retained_files_header
=
714 new views::Label(prompt
.GetRetainedFilesHeading());
715 retained_files_header
->SetMultiLine(true);
716 retained_files_header
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
717 retained_files_header
->SizeToFit(space_for_files
);
718 layout
->AddView(retained_files_header
);
720 layout
->StartRow(0, column_set_id
);
721 PermissionDetails details
;
722 for (size_t i
= 0; i
< prompt
.GetRetainedFileCount(); ++i
)
723 details
.push_back(prompt
.GetRetainedFile(i
));
724 ExpandableContainerView
* issue_advice_view
=
725 new ExpandableContainerView(
726 this, base::string16(), details
, space_for_files
,
728 layout
->AddView(issue_advice_view
);
731 DCHECK(prompt
.type() >= 0);
732 UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type",
734 ExtensionInstallPrompt::NUM_PROMPT_TYPES
);
736 if (prompt
.ShouldShowPermissions()) {
737 if (prompt
.ShouldShowExplanationText()) {
738 views::ColumnSet
* column_set
= layout
->AddColumnSet(++column_set_id
);
739 column_set
->AddColumn(views::GridLayout::LEADING
,
740 views::GridLayout::FILL
,
742 views::GridLayout::USE_PREF
,
745 // Add two rows of space so that the text stands out.
746 layout
->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing
);
748 layout
->StartRow(0, column_set_id
);
749 views::Label
* explanation
= new views::Label(
750 prompt
.experiment()->GetExplanationText());
751 explanation
->SetMultiLine(true);
752 explanation
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
753 explanation
->SizeToFit(left_column_width
+ kIconSize
);
754 layout
->AddView(explanation
);
757 if (prompt
.experiment()->should_show_expandable_permission_list() ||
758 (prompt
.experiment()->show_details_link() &&
759 prompt
.experiment()->should_show_inline_explanations() &&
760 !inline_explanations_
.empty())) {
761 // Don't show the "Show details" link if there are retained
762 // files. These have their own "Show details" links and having
763 // multiple levels of links is confusing.
764 if (prompt
.GetRetainedFileCount() == 0) {
766 prompt
.experiment()->should_show_expandable_permission_list() ?
767 IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS
:
768 IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_DETAILS
;
769 show_details_link_
= new views::Link(
770 l10n_util::GetStringUTF16(text_id
));
771 show_details_link_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
772 show_details_link_
->set_listener(this);
773 UpdateLinkActionHistogram(LINK_SHOWN
);
775 UpdateLinkActionHistogram(LINK_NOT_SHOWN
);
779 if (prompt
.experiment()->show_checkboxes()) {
780 checkbox_info_label_
= new views::Label(
781 l10n_util::GetStringUTF16(
782 IDS_EXTENSION_PROMPT_EXPERIMENT_CHECKBOX_INFO
));
783 checkbox_info_label_
->SetMultiLine(true);
784 checkbox_info_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
785 checkbox_info_label_
->SetAutoColorReadabilityEnabled(false);
786 checkbox_info_label_
->SetEnabledColor(kLighterLabelColor
);
790 gfx::Size scrollable_size
= scrollable_
->GetPreferredSize();
791 scrollable_
->SetBoundsRect(gfx::Rect(scrollable_size
));
792 dialog_size_
= gfx::Size(
794 std::min(scrollable_size
.height(), kDialogMaxHeight
));
796 if (scrollable_header_only_
) {
797 gfx::Size header_only_size
= scrollable_header_only_
->GetPreferredSize();
798 scrollable_header_only_
->SetBoundsRect(gfx::Rect(header_only_size
));
799 dialog_size_
= gfx::Size(
800 dialog_width
, std::min(header_only_size
.height(), kDialogMaxHeight
));
804 ExtensionInstallDialogView::~ExtensionInstallDialogView() {}
806 views::GridLayout
* ExtensionInstallDialogView::CreateLayout(
808 int left_column_width
,
810 bool single_detail_row
) const {
811 views::GridLayout
* layout
= views::GridLayout::CreatePanel(parent
);
812 parent
->SetLayoutManager(layout
);
814 views::ColumnSet
* column_set
= layout
->AddColumnSet(column_set_id
);
815 column_set
->AddColumn(views::GridLayout::LEADING
,
816 views::GridLayout::FILL
,
818 views::GridLayout::USE_PREF
,
821 if (!is_bundle_install()) {
822 column_set
->AddPaddingColumn(0, views::kPanelHorizMargin
);
823 column_set
->AddColumn(views::GridLayout::TRAILING
,
824 views::GridLayout::LEADING
,
826 views::GridLayout::USE_PREF
,
831 layout
->StartRow(0, column_set_id
);
833 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
835 views::Label
* heading
= new views::Label(
836 prompt_
.GetHeading(), rb
.GetFontList(ui::ResourceBundle::MediumFont
));
837 heading
->SetMultiLine(true);
838 heading
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
839 heading
->SizeToFit(left_column_width
);
840 layout
->AddView(heading
);
842 if (!is_bundle_install()) {
843 // Scale down to icon size, but allow smaller icons (don't scale up).
844 const gfx::ImageSkia
* image
= prompt_
.icon().ToImageSkia();
845 gfx::Size
size(image
->width(), image
->height());
846 if (size
.width() > kIconSize
|| size
.height() > kIconSize
)
847 size
= gfx::Size(kIconSize
, kIconSize
);
848 views::ImageView
* icon
= new views::ImageView();
849 icon
->SetImageSize(size
);
850 icon
->SetImage(*image
);
851 icon
->SetHorizontalAlignment(views::ImageView::CENTER
);
852 icon
->SetVerticalAlignment(views::ImageView::CENTER
);
853 if (single_detail_row
) {
854 layout
->AddView(icon
);
856 int icon_row_span
= 1;
857 if (is_inline_install()) {
858 // Also span the rating, user_count and store_link rows.
860 } else if (prompt_
.ShouldShowPermissions()) {
861 size_t permission_count
= prompt_
.GetPermissionCount();
862 // Also span the permission header and each of the permission rows (all
863 // have a padding row above it). This also works for the 'no special
864 // permissions' case.
865 icon_row_span
= 3 + permission_count
* 2;
866 } else if (prompt_
.GetRetainedFileCount()) {
867 // Also span the permission header and the retained files container.
870 layout
->AddView(icon
, 1, icon_row_span
);
876 void ExtensionInstallDialogView::ContentsChanged() {
880 void ExtensionInstallDialogView::ViewHierarchyChanged(
881 const ViewHierarchyChangedDetails
& details
) {
882 // Since we want the links to show up in the same visual row as the accept
883 // and cancel buttons, which is provided by the framework, we must add the
884 // buttons to the non-client view, which is the parent of this view.
885 // Similarly, when we're removed from the view hierarchy, we must take care
886 // to clean up those items as well.
887 if (details
.child
== this) {
888 if (details
.is_add
) {
889 if (show_details_link_
)
890 details
.parent
->AddChildView(show_details_link_
);
891 if (checkbox_info_label_
)
892 details
.parent
->AddChildView(checkbox_info_label_
);
894 if (show_details_link_
)
895 details
.parent
->RemoveChildView(show_details_link_
);
896 if (checkbox_info_label_
)
897 details
.parent
->RemoveChildView(checkbox_info_label_
);
902 int ExtensionInstallDialogView::GetDialogButtons() const {
903 int buttons
= prompt_
.GetDialogButtons();
904 // Simply having just an OK button is *not* supported. See comment on function
905 // GetDialogButtons in dialog_delegate.h for reasons.
906 DCHECK_GT(buttons
& ui::DIALOG_BUTTON_CANCEL
, 0);
910 base::string16
ExtensionInstallDialogView::GetDialogButtonLabel(
911 ui::DialogButton button
) const {
913 case ui::DIALOG_BUTTON_OK
:
914 return prompt_
.GetAcceptButtonLabel();
915 case ui::DIALOG_BUTTON_CANCEL
:
916 return prompt_
.HasAbortButtonLabel() ?
917 prompt_
.GetAbortButtonLabel() :
918 l10n_util::GetStringUTF16(IDS_CANCEL
);
921 return base::string16();
925 int ExtensionInstallDialogView::GetDefaultDialogButton() const {
926 return ui::DIALOG_BUTTON_CANCEL
;
929 bool ExtensionInstallDialogView::Cancel() {
930 UpdateInstallResultHistogram(false);
931 delegate_
->InstallUIAbort(true);
935 bool ExtensionInstallDialogView::Accept() {
936 UpdateInstallResultHistogram(true);
937 delegate_
->InstallUIProceed();
941 ui::ModalType
ExtensionInstallDialogView::GetModalType() const {
942 return ui::MODAL_TYPE_WINDOW
;
945 base::string16
ExtensionInstallDialogView::GetWindowTitle() const {
946 return prompt_
.GetDialogTitle();
949 void ExtensionInstallDialogView::LinkClicked(views::Link
* source
,
951 if (source
== show_details_link_
) {
952 UpdateLinkActionHistogram(LINK_CLICKED
);
953 // Show details link is used to either reveal whole permission list or to
954 // reveal inline explanations.
955 if (prompt_
.experiment()->should_show_expandable_permission_list()) {
956 gfx::Rect bounds
= GetWidget()->GetWindowBoundsInScreen();
957 int spacing
= bounds
.height() -
958 scrollable_header_only_
->GetPreferredSize().height();
959 int content_height
= std::min(scrollable_
->GetPreferredSize().height(),
961 bounds
.set_height(spacing
+ content_height
);
962 scroll_view_
->SetContents(scrollable_
);
963 GetWidget()->SetBoundsConstrained(bounds
);
966 ToggleInlineExplanations();
968 show_details_link_
->SetVisible(false);
970 GURL
store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
971 prompt_
.extension()->id());
972 OpenURLParams
params(
973 store_url
, Referrer(), NEW_FOREGROUND_TAB
,
974 content::PAGE_TRANSITION_LINK
,
976 navigator_
->OpenURL(params
);
977 GetWidget()->Close();
981 void ExtensionInstallDialogView::ToggleInlineExplanations() {
982 for (InlineExplanations::iterator it
= inline_explanations_
.begin();
983 it
!= inline_explanations_
.end(); ++it
)
984 (*it
)->ToggleDetailLevel();
987 void ExtensionInstallDialogView::Layout() {
988 scroll_view_
->SetBounds(0, 0, width(), height());
990 if (show_details_link_
|| checkbox_info_label_
) {
991 views::LabelButton
* cancel_button
= GetDialogClientView()->cancel_button();
992 gfx::Rect parent_bounds
= parent()->GetContentsBounds();
993 // By default, layouts have an inset of kButtonHEdgeMarginNew. In order to
994 // align the link horizontally with the left side of the contents of the
995 // layout, put a horizontal margin with this amount.
996 const int horizontal_margin
= views::kButtonHEdgeMarginNew
;
997 const int vertical_margin
= views::kButtonVEdgeMarginNew
;
998 int y_buttons
= parent_bounds
.bottom() -
999 cancel_button
->GetPreferredSize().height() - vertical_margin
;
1000 int max_width
= dialog_size_
.width() - cancel_button
->width() * 2 -
1001 horizontal_margin
* 2 - views::kRelatedButtonHSpacing
;
1002 if (show_details_link_
) {
1003 gfx::Size link_size
= show_details_link_
->GetPreferredSize();
1004 show_details_link_
->SetBounds(
1006 y_buttons
+ (cancel_button
->height() - link_size
.height()) / 2,
1007 link_size
.width(), link_size
.height());
1009 if (checkbox_info_label_
) {
1010 gfx::Size label_size
= checkbox_info_label_
->GetPreferredSize();
1011 checkbox_info_label_
->SetBounds(
1013 y_buttons
+ (cancel_button
->height() - label_size
.height()) / 2,
1014 label_size
.width(), label_size
.height());
1015 checkbox_info_label_
->SizeToFit(max_width
);
1018 // Disable accept button if there are unchecked boxes and
1019 // the experiment is on.
1020 if (prompt_
.experiment()->show_checkboxes())
1021 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_
== 0);
1023 DialogDelegateView::Layout();
1026 gfx::Size
ExtensionInstallDialogView::GetPreferredSize() const {
1027 return dialog_size_
;
1030 void ExtensionInstallDialogView::ButtonPressed(views::Button
* sender
,
1031 const ui::Event
& event
) {
1032 if (std::string(views::Checkbox::kViewClassName
) == sender
->GetClassName()) {
1033 views::Checkbox
* checkbox
= static_cast<views::Checkbox
*>(sender
);
1034 if (checkbox
->checked())
1039 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_
== 0);
1040 checkbox_info_label_
->SetVisible(unchecked_boxes_
> 0);
1044 void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted
)
1046 if (prompt_
.type() == ExtensionInstallPrompt::INSTALL_PROMPT
)
1047 UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted
);
1050 void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type
)
1052 if (prompt_
.experiment()->should_show_expandable_permission_list()) {
1053 // The clickable link in the UI is "Show Permissions".
1054 UMA_HISTOGRAM_ENUMERATION(
1055 "Extensions.InstallPromptExperiment.ShowPermissions",
1059 // The clickable link in the UI is "Show Details".
1060 UMA_HISTOGRAM_ENUMERATION(
1061 "Extensions.InstallPromptExperiment.ShowDetails",
1068 ExtensionInstallPrompt::ShowDialogCallback
1069 ExtensionInstallPrompt::GetDefaultShowDialogCallback() {
1070 return base::Bind(&ShowExtensionInstallDialogImpl
);
1073 // ExpandableContainerView::DetailsView ----------------------------------------
1075 ExpandableContainerView::DetailsView::DetailsView(int horizontal_space
,
1076 bool parent_bulleted
,
1078 : layout_(new views::GridLayout(this)),
1080 lighter_color_(lighter_color
) {
1081 SetLayoutManager(layout_
);
1082 views::ColumnSet
* column_set
= layout_
->AddColumnSet(0);
1083 // If the parent is using bullets for its items, then a padding of one unit
1084 // will make the child item (which has no bullet) look like a sibling of its
1085 // parent. Therefore increase the indentation by one more unit to show that it
1086 // is in fact a child item (with no missing bullet) and not a sibling.
1088 views::kRelatedControlHorizontalSpacing
* (parent_bulleted
? 2 : 1);
1089 column_set
->AddPaddingColumn(0, padding
);
1090 column_set
->AddColumn(views::GridLayout::LEADING
,
1091 views::GridLayout::LEADING
,
1093 views::GridLayout::FIXED
,
1094 horizontal_space
- padding
,
1098 void ExpandableContainerView::DetailsView::AddDetail(
1099 const base::string16
& detail
) {
1100 layout_
->StartRowWithPadding(0, 0,
1101 0, views::kRelatedControlSmallVerticalSpacing
);
1102 views::Label
* detail_label
=
1103 new views::Label(PrepareForDisplay(detail
, false));
1104 detail_label
->SetMultiLine(true);
1105 detail_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
1106 if (lighter_color_
) {
1107 detail_label
->SetEnabledColor(kLighterLabelColor
);
1108 detail_label
->SetAutoColorReadabilityEnabled(false);
1110 layout_
->AddView(detail_label
);
1113 gfx::Size
ExpandableContainerView::DetailsView::GetPreferredSize() const {
1114 gfx::Size size
= views::View::GetPreferredSize();
1115 return gfx::Size(size
.width(), size
.height() * state_
);
1118 void ExpandableContainerView::DetailsView::AnimateToState(double state
) {
1120 PreferredSizeChanged();
1124 // ExpandableContainerView -----------------------------------------------------
1126 ExpandableContainerView::ExpandableContainerView(
1127 ExtensionInstallDialogView
* owner
,
1128 const base::string16
& description
,
1129 const PermissionDetails
& details
,
1130 int horizontal_space
,
1131 bool parent_bulleted
,
1132 bool show_expand_link
,
1133 bool lighter_color_details
)
1135 details_view_(NULL
),
1136 more_details_(NULL
),
1137 slide_animation_(this),
1138 arrow_toggle_(NULL
),
1140 views::GridLayout
* layout
= new views::GridLayout(this);
1141 SetLayoutManager(layout
);
1142 int column_set_id
= 0;
1143 views::ColumnSet
* column_set
= layout
->AddColumnSet(column_set_id
);
1144 column_set
->AddColumn(views::GridLayout::LEADING
,
1145 views::GridLayout::LEADING
,
1147 views::GridLayout::USE_PREF
,
1150 if (!description
.empty()) {
1151 layout
->StartRow(0, column_set_id
);
1153 views::Label
* description_label
= new views::Label(description
);
1154 description_label
->SetMultiLine(true);
1155 description_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
1156 description_label
->SizeToFit(horizontal_space
);
1157 layout
->AddView(new BulletedView(description_label
));
1160 if (details
.empty())
1163 details_view_
= new DetailsView(horizontal_space
, parent_bulleted
,
1164 lighter_color_details
);
1166 layout
->StartRow(0, column_set_id
);
1167 layout
->AddView(details_view_
);
1169 for (size_t i
= 0; i
< details
.size(); ++i
)
1170 details_view_
->AddDetail(details
[i
]);
1172 // TODO(meacer): Remove show_expand_link when the experiment is completed.
1173 if (show_expand_link
) {
1174 views::Link
* link
= new views::Link(
1175 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS
));
1177 // Make sure the link width column is as wide as needed for both Show and
1178 // Hide details, so that the arrow doesn't shift horizontally when we
1180 int link_col_width
=
1181 views::kRelatedControlHorizontalSpacing
+
1182 std::max(gfx::GetStringWidth(
1183 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS
),
1185 gfx::GetStringWidth(
1186 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS
),
1187 link
->font_list()));
1189 column_set
= layout
->AddColumnSet(++column_set_id
);
1190 // Padding to the left of the More Details column. If the parent is using
1191 // bullets for its items, then a padding of one unit will make the child
1192 // item (which has no bullet) look like a sibling of its parent. Therefore
1193 // increase the indentation by one more unit to show that it is in fact a
1194 // child item (with no missing bullet) and not a sibling.
1195 column_set
->AddPaddingColumn(
1196 0, views::kRelatedControlHorizontalSpacing
* (parent_bulleted
? 2 : 1));
1197 // The More Details column.
1198 column_set
->AddColumn(views::GridLayout::LEADING
,
1199 views::GridLayout::LEADING
,
1201 views::GridLayout::FIXED
,
1204 // The Up/Down arrow column.
1205 column_set
->AddColumn(views::GridLayout::LEADING
,
1206 views::GridLayout::LEADING
,
1208 views::GridLayout::USE_PREF
,
1212 // Add the More Details link.
1213 layout
->StartRow(0, column_set_id
);
1214 more_details_
= link
;
1215 more_details_
->set_listener(this);
1216 more_details_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
1217 layout
->AddView(more_details_
);
1219 // Add the arrow after the More Details link.
1220 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
1221 arrow_toggle_
= new views::ImageButton(this);
1222 arrow_toggle_
->SetImage(views::Button::STATE_NORMAL
,
1223 rb
.GetImageSkiaNamed(IDR_DOWN_ARROW
));
1224 layout
->AddView(arrow_toggle_
);
1228 ExpandableContainerView::~ExpandableContainerView() {
1231 void ExpandableContainerView::ButtonPressed(
1232 views::Button
* sender
, const ui::Event
& event
) {
1233 ToggleDetailLevel();
1236 void ExpandableContainerView::LinkClicked(
1237 views::Link
* source
, int event_flags
) {
1238 ToggleDetailLevel();
1241 void ExpandableContainerView::AnimationProgressed(
1242 const gfx::Animation
* animation
) {
1243 DCHECK_EQ(&slide_animation_
, animation
);
1245 details_view_
->AnimateToState(animation
->GetCurrentValue());
1248 void ExpandableContainerView::AnimationEnded(const gfx::Animation
* animation
) {
1249 if (arrow_toggle_
) {
1250 if (animation
->GetCurrentValue() != 0.0) {
1251 arrow_toggle_
->SetImage(
1252 views::Button::STATE_NORMAL
,
1253 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
1256 arrow_toggle_
->SetImage(
1257 views::Button::STATE_NORMAL
,
1258 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
1262 if (more_details_
) {
1263 more_details_
->SetText(expanded_
?
1264 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS
) :
1265 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS
));
1269 void ExpandableContainerView::ChildPreferredSizeChanged(views::View
* child
) {
1270 owner_
->ContentsChanged();
1273 void ExpandableContainerView::ToggleDetailLevel() {
1274 expanded_
= !expanded_
;
1276 if (slide_animation_
.IsShowing())
1277 slide_animation_
.Hide();
1279 slide_animation_
.Show();
1282 void ExpandableContainerView::ExpandWithoutAnimation() {
1284 details_view_
->AnimateToState(1.0);