// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/extensions/extension_install_dialog_view.h" #include #include #include #include #include #include "base/callback_helpers.h" #include "base/command_line.h" #include "base/compiler_specific.h" #include "base/i18n/rtl.h" #include "base/macros.h" #include "base/metrics/histogram.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/extensions/api/experience_sampling_private/experience_sampling.h" #include "chrome/browser/extensions/bundle_installer.h" #include "chrome/browser/extensions/extension_install_prompt_show_params.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h" #include "chrome/common/extensions/extension_constants.h" #include "chrome/grit/generated_resources.h" #include "chrome/installer/util/browser_distribution.h" #include "components/constrained_window/constrained_window_views.h" #include "content/public/browser/page_navigator.h" #include "content/public/browser/web_contents.h" #include "extensions/common/extension.h" #include "extensions/common/extension_urls.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/paint_vector_icon.h" #include "ui/gfx/text_utils.h" #include "ui/gfx/vector_icons_public.h" #include "ui/native_theme/common_theme.h" #include "ui/views/border.h" #include "ui/views/controls/button/image_button.h" #include "ui/views/controls/image_view.h" #include "ui/views/controls/label.h" #include "ui/views/controls/link.h" #include "ui/views/controls/scroll_view.h" #include "ui/views/controls/separator.h" #include "ui/views/layout/box_layout.h" #include "ui/views/layout/grid_layout.h" #include "ui/views/layout/layout_constants.h" #include "ui/views/widget/widget.h" using content::OpenURLParams; using content::Referrer; using extensions::BundleInstaller; using extensions::ExperienceSamplingEvent; namespace { // Width of the bullet column in BulletedView. const int kBulletWidth = 20; // Size of extension icon in top left of dialog. const int kIconSize = 64; // Size of the icons of individual extensions for bundle installs. const int kSmallIconSize = 32; // Padding between extension icon and title for bundle installs. const int kSmallIconPadding = 6; // The maximum height of the scroll view before it will show a scrollbar. const int kScrollViewMaxHeight = 250; // Width of the left column of the dialog when the extension requests // permissions. const int kPermissionsLeftColumnWidth = 250; // Width of the left column of the dialog when the extension requests no // permissions. const int kNoPermissionsLeftColumnWidth = 200; // Width of the left column for external install prompts. The text is long in // this case, so make it wider than normal. const int kExternalInstallLeftColumnWidth = 350; void AddResourceIcon(const gfx::ImageSkia* skia_image, void* data) { views::View* parent = static_cast(data); views::ImageView* image_view = new views::ImageView(); image_view->SetImage(*skia_image); parent->AddChildView(image_view); } // Creates a string for displaying |message| to the user. If it has to look // like a entry in a bullet point list, one is added. base::string16 PrepareForDisplay(const base::string16& message, bool bullet_point) { return bullet_point ? l10n_util::GetStringFUTF16( IDS_EXTENSION_PERMISSION_LINE, message) : message; } void ShowExtensionInstallDialogImpl( ExtensionInstallPromptShowParams* show_params, const ExtensionInstallPrompt::DoneCallback& done_callback, scoped_ptr prompt) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); bool use_tab_modal_dialog = prompt->ShouldUseTabModalDialog(); ExtensionInstallDialogView* dialog = new ExtensionInstallDialogView( show_params->profile(), show_params->GetParentWebContents(), done_callback, std::move(prompt)); if (use_tab_modal_dialog) { content::WebContents* parent_web_contents = show_params->GetParentWebContents(); if (parent_web_contents) constrained_window::ShowWebModalDialogViews(dialog, parent_web_contents); } else { constrained_window::CreateBrowserModalDialogViews( dialog, show_params->GetParentWindow()) ->Show(); } } // A custom scrollable view implementation for the dialog. class CustomScrollableView : public views::View { public: CustomScrollableView() {} ~CustomScrollableView() override {} // Called when one of the child elements has expanded/collapsed. void ChildPreferredSizeChanged(views::View* child) override { PreferredSizeChanged(); } private: void Layout() override { SetBounds(x(), y(), width(), GetHeightForWidth(width())); views::View::Layout(); } DISALLOW_COPY_AND_ASSIGN(CustomScrollableView); }; } // namespace BulletedView::BulletedView(views::View* view) { views::GridLayout* layout = new views::GridLayout(this); SetLayoutManager(layout); views::ColumnSet* column_set = layout->AddColumnSet(0); column_set->AddColumn(views::GridLayout::CENTER, views::GridLayout::LEADING, 0, views::GridLayout::FIXED, kBulletWidth, 0); column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::LEADING, 0, views::GridLayout::USE_PREF, 0, // No fixed width. 0); layout->StartRow(0, 0); layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true))); layout->AddView(view); } IconedView::IconedView(views::View* view, const gfx::ImageSkia& image) { views::GridLayout* layout = new views::GridLayout(this); SetLayoutManager(layout); views::ColumnSet* column_set = layout->AddColumnSet(0); column_set->AddColumn(views::GridLayout::CENTER, views::GridLayout::LEADING, 0, views::GridLayout::FIXED, kSmallIconSize, 0); column_set->AddPaddingColumn(0, kSmallIconPadding); column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, // No fixed width. 0); layout->StartRow(0, 0); gfx::Size size(image.width(), image.height()); if (size.width() > kSmallIconSize || size.height() > kSmallIconSize) size = gfx::Size(kSmallIconSize, kSmallIconSize); views::ImageView* image_view = new views::ImageView; image_view->SetImage(image); image_view->SetImageSize(size); layout->AddView(image_view); layout->AddView(view); } ExtensionInstallDialogView::ExtensionInstallDialogView( Profile* profile, content::PageNavigator* navigator, const ExtensionInstallPrompt::DoneCallback& done_callback, scoped_ptr prompt) : profile_(profile), navigator_(navigator), done_callback_(done_callback), prompt_(std::move(prompt)), container_(NULL), scroll_view_(NULL), handled_result_(false) { InitView(); } ExtensionInstallDialogView::~ExtensionInstallDialogView() { if (!handled_result_ && !done_callback_.is_null()) { base::ResetAndReturn(&done_callback_) .Run(ExtensionInstallPrompt::Result::USER_CANCELED); } } void ExtensionInstallDialogView::InitView() { // Possible grid layouts: // With webstore data (inline install, external install, repair) // w/ permissions no permissions // +--------------+------+ +--------------+------+ // | title | icon | | title | icon | // +--------------| | +--------------| | // | rating | | | rating | | // +--------------| | +--------------| | // | user_count | | | user_count | | // +--------------| | +--------------| | // | store_link | | | store_link | | // +--------------+------+ +--------------+------+ // | separator | | scroll_view (empty) | // +---------------------+ +---------------------+ // | scroll_view | // +---------------------+ // // No webstore data (all other types) // +--------------+------+ // | title | icon | // +--------------| | // | scroll_view | | // +--------------+------+ // The scroll_view contains permissions (if there are any) and retained // files/devices (if there are any; post-install-permissions prompt only). int left_column_width = (prompt_->ShouldShowPermissions() || prompt_->GetRetainedFileCount() > 0) ? kPermissionsLeftColumnWidth : kNoPermissionsLeftColumnWidth; if (is_external_install()) left_column_width = kExternalInstallLeftColumnWidth; int column_set_id = 0; views::GridLayout* layout = CreateLayout(left_column_width, column_set_id); ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); if (prompt_->has_webstore_data()) { layout->StartRow(0, column_set_id); views::View* rating = new views::View(); rating->SetLayoutManager(new views::BoxLayout( views::BoxLayout::kHorizontal, 0, 0, 0)); layout->AddView(rating); prompt_->AppendRatingStars(AddResourceIcon, rating); const gfx::FontList& small_font_list = rb.GetFontList(ui::ResourceBundle::SmallFont); views::Label* rating_count = new views::Label(prompt_->GetRatingCount(), small_font_list); // Add some space between the stars and the rating count. rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0)); rating->AddChildView(rating_count); layout->StartRow(0, column_set_id); views::Label* user_count = new views::Label(prompt_->GetUserCount(), small_font_list); user_count->SetAutoColorReadabilityEnabled(false); user_count->SetEnabledColor(SK_ColorGRAY); layout->AddView(user_count); layout->StartRow(0, column_set_id); views::Link* store_link = new views::Link( l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK)); store_link->SetFontList(small_font_list); store_link->set_listener(this); layout->AddView(store_link); if (prompt_->ShouldShowPermissions()) { layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); layout->StartRow(0, column_set_id); layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 3, 1, views::GridLayout::FILL, views::GridLayout::FILL); } } int content_width = left_column_width + views::kPanelHorizMargin + kIconSize; // Create the scrollable view which will contain the permissions and retained // files/devices. CustomScrollableView* scrollable = new CustomScrollableView(); views::GridLayout* scroll_layout = new views::GridLayout(scrollable); scrollable->SetLayoutManager(scroll_layout); views::ColumnSet* scrollable_column_set = scroll_layout->AddColumnSet(column_set_id); // If we have webstore data, there's a separator below it, so we can span the // whole content width. Otherwise just use the width of the left column so // that we don't overlap the icon. int scrollable_width = prompt_->has_webstore_data() ? content_width : left_column_width; scrollable_column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::LEADING, 0, // no resizing views::GridLayout::USE_PREF, scrollable_width, scrollable_width); // Pad to the very right of the dialog, so the scrollbar will be on the edge. int padding_width = content_width + views::kButtonHEdgeMarginNew - scrollable_width; scrollable_column_set->AddPaddingColumn(0, padding_width); layout->StartRow(0, column_set_id); scroll_view_ = new views::ScrollView(); scroll_view_->set_hide_horizontal_scrollbar(true); scroll_view_->SetContents(scrollable); layout->AddView(scroll_view_, 4, 1); if (is_bundle_install()) { BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState( BundleInstaller::Item::STATE_PENDING); scroll_layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing); for (const BundleInstaller::Item& item : items) { scroll_layout->StartRow(0, column_set_id); views::Label* extension_label = new views::Label(item.GetNameForDisplay()); extension_label->SetMultiLine(true); extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); extension_label->SizeToFit( scrollable_width - kSmallIconSize - kSmallIconPadding); gfx::ImageSkia image = gfx::ImageSkia::CreateFrom1xBitmap(item.icon); scroll_layout->AddView(new IconedView(extension_label, image)); } scroll_layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); } if (prompt_->ShouldShowPermissions()) { bool has_permissions = prompt_->GetPermissionCount( ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS) > 0; if (has_permissions) { AddPermissions( scroll_layout, rb, column_set_id, scrollable_width, ExtensionInstallPrompt::PermissionsType::REGULAR_PERMISSIONS); AddPermissions( scroll_layout, rb, column_set_id, scrollable_width, ExtensionInstallPrompt::PermissionsType::WITHHELD_PERMISSIONS); } else { scroll_layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); scroll_layout->StartRow(0, column_set_id); views::Label* permission_label = new views::Label( l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)); permission_label->SetMultiLine(true); permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); permission_label->SizeToFit(scrollable_width); scroll_layout->AddView(permission_label); } } if (prompt_->GetRetainedFileCount()) { scroll_layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); scroll_layout->StartRow(0, column_set_id); views::Label* retained_files_header = new views::Label(prompt_->GetRetainedFilesHeading()); retained_files_header->SetMultiLine(true); retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); retained_files_header->SizeToFit(scrollable_width); scroll_layout->AddView(retained_files_header); scroll_layout->StartRow(0, column_set_id); PermissionDetails details; for (size_t i = 0; i < prompt_->GetRetainedFileCount(); ++i) { details.push_back(prompt_->GetRetainedFile(i)); } ExpandableContainerView* issue_advice_view = new ExpandableContainerView(this, base::string16(), details, scrollable_width, false); scroll_layout->AddView(issue_advice_view); } if (prompt_->GetRetainedDeviceCount()) { scroll_layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); scroll_layout->StartRow(0, column_set_id); views::Label* retained_devices_header = new views::Label(prompt_->GetRetainedDevicesHeading()); retained_devices_header->SetMultiLine(true); retained_devices_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); retained_devices_header->SizeToFit(scrollable_width); scroll_layout->AddView(retained_devices_header); scroll_layout->StartRow(0, column_set_id); PermissionDetails details; for (size_t i = 0; i < prompt_->GetRetainedDeviceCount(); ++i) { details.push_back(prompt_->GetRetainedDeviceMessageString(i)); } ExpandableContainerView* issue_advice_view = new ExpandableContainerView(this, base::string16(), details, scrollable_width, false); scroll_layout->AddView(issue_advice_view); } DCHECK_GE(prompt_->type(), 0); UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type", prompt_->type(), ExtensionInstallPrompt::NUM_PROMPT_TYPES); scroll_view_->ClipHeightTo( 0, std::min(kScrollViewMaxHeight, scrollable->GetPreferredSize().height())); dialog_size_ = gfx::Size( content_width + 2 * views::kButtonHEdgeMarginNew, container_->GetPreferredSize().height()); std::string event_name = ExperienceSamplingEvent::kExtensionInstallDialog; event_name.append( ExtensionInstallPrompt::PromptTypeToString(prompt_->type())); sampling_event_ = ExperienceSamplingEvent::Create(event_name); } bool ExtensionInstallDialogView::AddPermissions( views::GridLayout* layout, ui::ResourceBundle& rb, int column_set_id, int left_column_width, ExtensionInstallPrompt::PermissionsType perm_type) { if (prompt_->GetPermissionCount(perm_type) == 0) return false; layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); layout->StartRow(0, column_set_id); views::Label* permissions_header = NULL; if (is_bundle_install()) { // We need to pass the FontList in the constructor, rather than calling // SetFontList later, because otherwise SizeToFit mis-judges the width // of the line. permissions_header = new views::Label(prompt_->GetPermissionsHeading(perm_type), rb.GetFontList(ui::ResourceBundle::MediumFont)); } else { permissions_header = new views::Label(prompt_->GetPermissionsHeading(perm_type)); } permissions_header->SetMultiLine(true); permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); permissions_header->SizeToFit(left_column_width); layout->AddView(permissions_header); for (size_t i = 0; i < prompt_->GetPermissionCount(perm_type); ++i) { layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); layout->StartRow(0, column_set_id); views::Label* permission_label = new views::Label(prompt_->GetPermission(i, perm_type)); permission_label->SetMultiLine(true); permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); permission_label->SizeToFit(left_column_width - kBulletWidth); layout->AddView(new BulletedView(permission_label)); // If we have more details to provide, show them in collapsed form. if (!prompt_->GetPermissionsDetails(i, perm_type).empty()) { layout->StartRow(0, column_set_id); PermissionDetails details; details.push_back(PrepareForDisplay( prompt_->GetPermissionsDetails(i, perm_type), false)); ExpandableContainerView* details_container = new ExpandableContainerView(this, base::string16(), details, left_column_width, true); layout->AddView(details_container); } } return true; } views::GridLayout* ExtensionInstallDialogView::CreateLayout( int left_column_width, int column_set_id) { container_ = new views::View(); // This is basically views::GridLayout::CreatePanel, but without a top or // right margin (we effectively get a top margin anyway from the empty dialog // title, and we add an explicit padding column as a right margin below). views::GridLayout* layout = new views::GridLayout(container_); layout->SetInsets(0, views::kButtonHEdgeMarginNew, views::kPanelVertMargin, 0); container_->SetLayoutManager(layout); AddChildView(container_); views::ColumnSet* column_set = layout->AddColumnSet(column_set_id); column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::LEADING, 0, // no resizing views::GridLayout::USE_PREF, 0, // no fixed width left_column_width); column_set->AddPaddingColumn(0, views::kPanelHorizMargin); column_set->AddColumn(views::GridLayout::TRAILING, views::GridLayout::LEADING, 0, // no resizing views::GridLayout::USE_PREF, 0, // no fixed width kIconSize); column_set->AddPaddingColumn(0, views::kButtonHEdgeMarginNew); layout->StartRow(0, column_set_id); views::Label* title = new views::Label(prompt_->GetDialogTitle(), ui::ResourceBundle::GetSharedInstance().GetFontList( ui::ResourceBundle::MediumFont)); title->SetMultiLine(true); title->SetHorizontalAlignment(gfx::ALIGN_LEFT); title->SizeToFit(left_column_width); layout->AddView(title); // Scale down to icon size, but allow smaller icons (don't scale up). const gfx::ImageSkia* image = prompt_->icon().ToImageSkia(); gfx::Size size(image->width(), image->height()); if (size.width() > kIconSize || size.height() > kIconSize) size = gfx::Size(kIconSize, kIconSize); views::ImageView* icon = new views::ImageView(); icon->SetImageSize(size); icon->SetImage(*image); int icon_row_span = 1; // Always span the title. if (prompt_->has_webstore_data()) { // Also span the rating, user_count and store_link rows. icon_row_span += 3; // Note: Do not span the permissions here, there's a separator in between! } else { // Also span the scrollable container with permissions, retained files etc. icon_row_span += 1; } layout->AddView(icon, 1, icon_row_span); return layout; } int ExtensionInstallDialogView::GetDialogButtons() const { int buttons = prompt_->GetDialogButtons(); // Simply having just an OK button is *not* supported. See comment on function // GetDialogButtons in dialog_delegate.h for reasons. DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0); return buttons; } base::string16 ExtensionInstallDialogView::GetDialogButtonLabel( ui::DialogButton button) const { switch (button) { case ui::DIALOG_BUTTON_OK: return prompt_->GetAcceptButtonLabel(); case ui::DIALOG_BUTTON_CANCEL: return prompt_->GetAbortButtonLabel(); default: NOTREACHED(); return base::string16(); } } int ExtensionInstallDialogView::GetDefaultDialogButton() const { return ui::DIALOG_BUTTON_CANCEL; } bool ExtensionInstallDialogView::Cancel() { if (handled_result_) return true; handled_result_ = true; UpdateInstallResultHistogram(false); if (sampling_event_) sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kDeny); base::ResetAndReturn(&done_callback_) .Run(ExtensionInstallPrompt::Result::USER_CANCELED); return true; } bool ExtensionInstallDialogView::Accept() { DCHECK(!handled_result_); handled_result_ = true; UpdateInstallResultHistogram(true); if (sampling_event_) sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kProceed); base::ResetAndReturn(&done_callback_) .Run(ExtensionInstallPrompt::Result::ACCEPTED); return true; } ui::ModalType ExtensionInstallDialogView::GetModalType() const { return prompt_->ShouldUseTabModalDialog() ? ui::MODAL_TYPE_CHILD : ui::MODAL_TYPE_WINDOW; } void ExtensionInstallDialogView::LinkClicked(views::Link* source, int event_flags) { GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() + prompt_->extension()->id()); OpenURLParams params( store_url, Referrer(), NEW_FOREGROUND_TAB, ui::PAGE_TRANSITION_LINK, false); if (navigator_) { navigator_->OpenURL(params); } else { chrome::ScopedTabbedBrowserDisplayer displayer(profile_); displayer.browser()->OpenURL(params); } GetWidget()->Close(); } void ExtensionInstallDialogView::Layout() { container_->SetBounds(0, 0, width(), height()); DialogDelegateView::Layout(); } gfx::Size ExtensionInstallDialogView::GetPreferredSize() const { return dialog_size_; } void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted) const { if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT) UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted); } // ExpandableContainerView::DetailsView ---------------------------------------- ExpandableContainerView::DetailsView::DetailsView(int horizontal_space, bool parent_bulleted) : layout_(new views::GridLayout(this)), state_(0) { SetLayoutManager(layout_); views::ColumnSet* column_set = layout_->AddColumnSet(0); // If the parent is using bullets for its items, then a padding of one unit // will make the child item (which has no bullet) look like a sibling of its // parent. Therefore increase the indentation by one more unit to show that it // is in fact a child item (with no missing bullet) and not a sibling. int padding = views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1); column_set->AddPaddingColumn(0, padding); column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::LEADING, 0, views::GridLayout::FIXED, horizontal_space - padding, 0); } void ExpandableContainerView::DetailsView::AddDetail( const base::string16& detail) { layout_->StartRowWithPadding(0, 0, 0, views::kRelatedControlSmallVerticalSpacing); views::Label* detail_label = new views::Label(PrepareForDisplay(detail, false)); detail_label->SetMultiLine(true); detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); layout_->AddView(detail_label); } gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const { gfx::Size size = views::View::GetPreferredSize(); return gfx::Size(size.width(), size.height() * state_); } void ExpandableContainerView::DetailsView::AnimateToState(double state) { state_ = state; PreferredSizeChanged(); SchedulePaint(); } // ExpandableContainerView ----------------------------------------------------- ExpandableContainerView::ExpandableContainerView( ExtensionInstallDialogView* owner, const base::string16& description, const PermissionDetails& details, int horizontal_space, bool parent_bulleted) : owner_(owner), details_view_(NULL), slide_animation_(this), more_details_(NULL), arrow_toggle_(NULL), expanded_(false) { views::GridLayout* layout = new views::GridLayout(this); SetLayoutManager(layout); int column_set_id = 0; views::ColumnSet* column_set = layout->AddColumnSet(column_set_id); column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::LEADING, 0, views::GridLayout::USE_PREF, 0, 0); if (!description.empty()) { layout->StartRow(0, column_set_id); views::Label* description_label = new views::Label(description); description_label->SetMultiLine(true); description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); description_label->SizeToFit(horizontal_space); layout->AddView(new BulletedView(description_label)); } if (details.empty()) return; details_view_ = new DetailsView(horizontal_space, parent_bulleted); layout->StartRow(0, column_set_id); layout->AddView(details_view_); for (size_t i = 0; i < details.size(); ++i) details_view_->AddDetail(details[i]); // Make sure the link width column is as wide as needed for both Show and // Hide details, so that the arrow doesn't shift horizontally when we // toggle. views::Link* link = new views::Link( l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS)); int link_col_width = link->GetPreferredSize().width(); link->SetText(l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS)); link_col_width = std::max(link_col_width, link->GetPreferredSize().width()); column_set = layout->AddColumnSet(++column_set_id); // Padding to the left of the More Details column. If the parent is using // bullets for its items, then a padding of one unit will make the child // item (which has no bullet) look like a sibling of its parent. Therefore // increase the indentation by one more unit to show that it is in fact a // child item (with no missing bullet) and not a sibling. column_set->AddPaddingColumn( 0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1)); // The More Details column. column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::LEADING, 0, views::GridLayout::FIXED, link_col_width, link_col_width); // The Up/Down arrow column. column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::TRAILING, 0, views::GridLayout::USE_PREF, 0, 0); // Add the More Details link. layout->StartRow(0, column_set_id); more_details_ = link; more_details_->set_listener(this); more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT); layout->AddView(more_details_); // Add the arrow after the More Details link. arrow_toggle_ = new views::ImageButton(this); UpdateArrowToggle(false); layout->AddView(arrow_toggle_); } ExpandableContainerView::~ExpandableContainerView() { } void ExpandableContainerView::ButtonPressed( views::Button* sender, const ui::Event& event) { ToggleDetailLevel(); } void ExpandableContainerView::LinkClicked( views::Link* source, int event_flags) { ToggleDetailLevel(); } void ExpandableContainerView::AnimationProgressed( const gfx::Animation* animation) { DCHECK_EQ(&slide_animation_, animation); if (details_view_) details_view_->AnimateToState(animation->GetCurrentValue()); } void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) { if (arrow_toggle_) UpdateArrowToggle(animation->GetCurrentValue() != 0.0); if (more_details_) { more_details_->SetText(expanded_ ? l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) : l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS)); } } void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) { PreferredSizeChanged(); } void ExpandableContainerView::ToggleDetailLevel() { expanded_ = !expanded_; if (slide_animation_.IsShowing()) slide_animation_.Hide(); else slide_animation_.Show(); } void ExpandableContainerView::UpdateArrowToggle(bool expanded) { gfx::ImageSkia icon = gfx::CreateVectorIcon( expanded ? gfx::VectorIconId::FIND_PREV : gfx::VectorIconId::FIND_NEXT, 16, gfx::kChromeIconGrey); arrow_toggle_->SetImage(views::Button::STATE_NORMAL, &icon); } // static ExtensionInstallPrompt::ShowDialogCallback ExtensionInstallPrompt::GetDefaultShowDialogCallback() { return base::Bind(&ShowExtensionInstallDialogImpl); }