1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include <vector> 6 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" 53 54 using content::OpenURLParams; 55 using content::Referrer; 56 using extensions::BundleInstaller; 57 58 namespace { 59 60 // Size of extension icon in top left of dialog. 61 const int kIconSize = 64; 62 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; 66 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; 70 71 // Width of the left column of the dialog when the extension requests 72 // permissions. 73 const int kPermissionsLeftColumnWidth = 250; 74 75 // Width of the left column of the dialog when the extension requests no 76 // permissions. 77 const int kNoPermissionsLeftColumnWidth = 200; 78 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; 82 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; 86 87 // Lighter color for labels. 88 const SkColor kLighterLabelColor = SkColorSetRGB(0x99, 0x99, 0x99); 89 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 { 95 LINK_SHOWN = 0, 96 LINK_NOT_SHOWN, 97 LINK_CLICKED, 98 NUM_LINK_ACTIONS 99 }; 100 101 typedef std::vector<base::string16> PermissionDetails; 102 class ExpandableContainerView; 103 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); 109 } 110 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, 114 bool bullet_point) { 115 return bullet_point ? l10n_util::GetStringFUTF16( 116 IDS_EXTENSION_PERMISSION_LINE, 117 message) : message; 118 } 119 120 // A custom scrollable view implementation for the dialog. 121 class CustomScrollableView : public views::View { 122 public: 123 CustomScrollableView(); 124 virtual ~CustomScrollableView(); 125 126 private: 127 virtual void Layout() OVERRIDE; 128 129 DISALLOW_COPY_AND_ASSIGN(CustomScrollableView); 130 }; 131 132 // Implements the extension installation dialog for TOOLKIT_VIEWS. 133 class ExtensionInstallDialogView : public views::DialogDelegateView, 134 public views::LinkListener, 135 public views::ButtonListener { 136 public: 137 ExtensionInstallDialogView( 138 content::PageNavigator* navigator, 139 ExtensionInstallPrompt::Delegate* delegate, 140 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt); 141 virtual ~ExtensionInstallDialogView(); 142 143 // Called when one of the child elements has expanded/collapsed. 144 void ContentsChanged(); 145 146 private: 147 // views::DialogDelegateView: 148 virtual int GetDialogButtons() const OVERRIDE; 149 virtual base::string16 GetDialogButtonLabel( 150 ui::DialogButton button) const OVERRIDE; 151 virtual int GetDefaultDialogButton() const OVERRIDE; 152 virtual bool Cancel() OVERRIDE; 153 virtual bool Accept() OVERRIDE; 154 virtual ui::ModalType GetModalType() const OVERRIDE; 155 virtual base::string16 GetWindowTitle() const OVERRIDE; 156 virtual void Layout() OVERRIDE; 157 virtual gfx::Size GetPreferredSize() const OVERRIDE; 158 virtual void ViewHierarchyChanged( 159 const ViewHierarchyChangedDetails& details) OVERRIDE; 160 161 // views::LinkListener: 162 virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE; 163 164 // views::ButtonListener: 165 virtual void ButtonPressed(views::Button* sender, 166 const ui::Event& event) OVERRIDE; 167 168 // Experimental: Toggles inline permission explanations with an animation. 169 void ToggleInlineExplanations(); 170 171 // Creates a layout consisting of dialog header, extension name and icon. 172 views::GridLayout* CreateLayout( 173 views::View* parent, 174 int left_column_width, 175 int column_set_id, 176 bool single_detail_row) const; 177 178 bool is_inline_install() const { 179 return prompt_->type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT; 180 } 181 182 bool is_bundle_install() const { 183 return prompt_->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT; 184 } 185 186 bool is_external_install() const { 187 return prompt_->type() == ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT; 188 } 189 190 // Updates the histogram that holds installation accepted/aborted data. 191 void UpdateInstallResultHistogram(bool accepted) const; 192 193 // Updates the histogram that holds data about whether "Show details" or 194 // "Show permissions" links were shown and/or clicked. 195 void UpdateLinkActionHistogram(int action_type) const; 196 197 content::PageNavigator* navigator_; 198 ExtensionInstallPrompt::Delegate* delegate_; 199 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_; 200 201 // The scroll view containing all the details for the dialog (including all 202 // collapsible/expandable sections). 203 views::ScrollView* scroll_view_; 204 205 // The container view for the scroll view. 206 CustomScrollableView* scrollable_; 207 208 // The container for the simpler view with only the dialog header and the 209 // extension icon. Used for the experiment where the permissions are 210 // initially hidden when the dialog shows. 211 CustomScrollableView* scrollable_header_only_; 212 213 // The preferred size of the dialog. 214 gfx::Size dialog_size_; 215 216 // Experimental: "Show details" link to expand inline explanations and reveal 217 // permision dialog. 218 views::Link* show_details_link_; 219 220 // Experimental: Label for showing information about the checkboxes. 221 views::Label* checkbox_info_label_; 222 223 // Experimental: Contains pointers to inline explanation views. 224 typedef std::vector<ExpandableContainerView*> InlineExplanations; 225 InlineExplanations inline_explanations_; 226 227 // Experimental: Number of unchecked checkboxes in the permission list. 228 // If this becomes zero, the accept button is enabled, otherwise disabled. 229 int unchecked_boxes_; 230 231 DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogView); 232 }; 233 234 // A simple view that prepends a view with a bullet with the help of a grid 235 // layout. 236 class BulletedView : public views::View { 237 public: 238 explicit BulletedView(views::View* view); 239 private: 240 DISALLOW_COPY_AND_ASSIGN(BulletedView); 241 }; 242 243 BulletedView::BulletedView(views::View* view) { 244 views::GridLayout* layout = new views::GridLayout(this); 245 SetLayoutManager(layout); 246 views::ColumnSet* column_set = layout->AddColumnSet(0); 247 column_set->AddColumn(views::GridLayout::LEADING, 248 views::GridLayout::LEADING, 249 0, 250 views::GridLayout::USE_PREF, 251 0, // no fixed width 252 0); 253 column_set->AddColumn(views::GridLayout::LEADING, 254 views::GridLayout::LEADING, 255 0, 256 views::GridLayout::USE_PREF, 257 0, // no fixed width 258 0); 259 layout->StartRow(0, 0); 260 layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true))); 261 layout->AddView(view); 262 } 263 264 // A simple view that prepends a view with a checkbox with the help of a grid 265 // layout. Used for the permission experiment. 266 // TODO(meacer): Remove once the experiment is completed. 267 class CheckboxedView : public views::View { 268 public: 269 CheckboxedView(views::View* view, views::ButtonListener* listener); 270 private: 271 DISALLOW_COPY_AND_ASSIGN(CheckboxedView); 272 }; 273 274 CheckboxedView::CheckboxedView(views::View* view, 275 views::ButtonListener* listener) { 276 views::GridLayout* layout = new views::GridLayout(this); 277 SetLayoutManager(layout); 278 views::ColumnSet* column_set = layout->AddColumnSet(0); 279 column_set->AddColumn(views::GridLayout::LEADING, 280 views::GridLayout::LEADING, 281 0, 282 views::GridLayout::USE_PREF, 283 0, // no fixed width 284 0); 285 column_set->AddColumn(views::GridLayout::LEADING, 286 views::GridLayout::LEADING, 287 0, 288 views::GridLayout::USE_PREF, 289 0, // no fixed width 290 0); 291 layout->StartRow(0, 0); 292 views::Checkbox* checkbox = new views::Checkbox(base::string16()); 293 checkbox->set_listener(listener); 294 // Alignment needs to be explicitly set again here, otherwise the views are 295 // not vertically centered. 296 layout->AddView(checkbox, 1, 1, 297 views::GridLayout::LEADING, views::GridLayout::CENTER); 298 layout->AddView(view, 1, 1, 299 views::GridLayout::LEADING, views::GridLayout::CENTER); 300 } 301 302 // A view to display text with an expandable details section. 303 class ExpandableContainerView : public views::View, 304 public views::ButtonListener, 305 public views::LinkListener, 306 public gfx::AnimationDelegate { 307 public: 308 ExpandableContainerView(ExtensionInstallDialogView* owner, 309 const base::string16& description, 310 const PermissionDetails& details, 311 int horizontal_space, 312 bool parent_bulleted, 313 bool show_expand_link, 314 bool lighter_color_details); 315 virtual ~ExpandableContainerView(); 316 317 // views::View: 318 virtual void ChildPreferredSizeChanged(views::View* child) OVERRIDE; 319 320 // views::ButtonListener: 321 virtual void ButtonPressed(views::Button* sender, 322 const ui::Event& event) OVERRIDE; 323 324 // views::LinkListener: 325 virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE; 326 327 // gfx::AnimationDelegate: 328 virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE; 329 virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE; 330 331 // Expand/Collapse the detail section for this ExpandableContainerView. 332 void ToggleDetailLevel(); 333 334 // Expand the detail section without any animation. 335 // TODO(meacer): Remove once the experiment is completed. 336 void ExpandWithoutAnimation(); 337 338 private: 339 // A view which displays all the details of an IssueAdviceInfoEntry. 340 class DetailsView : public views::View { 341 public: 342 explicit DetailsView(int horizontal_space, bool parent_bulleted, 343 bool lighter_color); 344 virtual ~DetailsView() {} 345 346 // views::View: 347 virtual gfx::Size GetPreferredSize() const OVERRIDE; 348 349 void AddDetail(const base::string16& detail); 350 351 // Animates this to be a height proportional to |state|. 352 void AnimateToState(double state); 353 354 private: 355 views::GridLayout* layout_; 356 double state_; 357 358 // Whether the detail text should be shown with a lighter color. 359 bool lighter_color_; 360 361 DISALLOW_COPY_AND_ASSIGN(DetailsView); 362 }; 363 364 // The dialog that owns |this|. It's also an ancestor in the View hierarchy. 365 ExtensionInstallDialogView* owner_; 366 367 // A view for showing |issue_advice.details|. 368 DetailsView* details_view_; 369 370 // The 'more details' link shown under the heading (changes to 'hide details' 371 // when the details section is expanded). 372 views::Link* more_details_; 373 374 gfx::SlideAnimation slide_animation_; 375 376 // The up/down arrow next to the 'more detail' link (points up/down depending 377 // on whether the details section is expanded). 378 views::ImageButton* arrow_toggle_; 379 380 // Whether the details section is expanded. 381 bool expanded_; 382 383 DISALLOW_COPY_AND_ASSIGN(ExpandableContainerView); 384 }; 385 386 void ShowExtensionInstallDialogImpl( 387 const ExtensionInstallPrompt::ShowParams& show_params, 388 ExtensionInstallPrompt::Delegate* delegate, 389 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) { 390 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 391 CreateBrowserModalDialogViews( 392 new ExtensionInstallDialogView(show_params.navigator, delegate, prompt), 393 show_params.parent_window)->Show(); 394 } 395 396 } // namespace 397 398 CustomScrollableView::CustomScrollableView() {} 399 CustomScrollableView::~CustomScrollableView() {} 400 401 void CustomScrollableView::Layout() { 402 SetBounds(x(), y(), width(), GetHeightForWidth(width())); 403 views::View::Layout(); 404 } 405 406 ExtensionInstallDialogView::ExtensionInstallDialogView( 407 content::PageNavigator* navigator, 408 ExtensionInstallPrompt::Delegate* delegate, 409 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) 410 : navigator_(navigator), 411 delegate_(delegate), 412 prompt_(prompt), 413 scroll_view_(NULL), 414 scrollable_(NULL), 415 scrollable_header_only_(NULL), 416 show_details_link_(NULL), 417 checkbox_info_label_(NULL), 418 unchecked_boxes_(0) { 419 // Possible grid layouts without ExtensionPermissionDialog experiment: 420 // Inline install 421 // w/ permissions no permissions 422 // +--------------------+------+ +--------------+------+ 423 // | heading | icon | | heading | icon | 424 // +--------------------| | +--------------| | 425 // | rating | | | rating | | 426 // +--------------------| | +--------------+ | 427 // | user_count | | | user_count | | 428 // +--------------------| | +--------------| | 429 // | store_link | | | store_link | | 430 // +--------------------+------+ +--------------+------+ 431 // | separator | 432 // +--------------------+------+ 433 // | permissions_header | | 434 // +--------------------+------+ 435 // | permission1 | | 436 // +--------------------+------+ 437 // | permission2 | | 438 // +--------------------+------+ 439 // 440 // Regular install 441 // w/ permissions no permissions 442 // +--------------------+------+ +--------------+------+ 443 // | heading | icon | | heading | icon | 444 // +--------------------| | +--------------+------+ 445 // | permissions_header | | 446 // +--------------------| | 447 // | permission1 | | 448 // +--------------------| | 449 // | permission2 | | 450 // +--------------------+------+ 451 // 452 // If the ExtensionPermissionDialog is on, the layout is modified depending 453 // on the experiment group. For text only experiment, a footer is added at the 454 // bottom of the layouts. For others, inline details are added below some of 455 // the permissions. 456 // 457 // Regular install w/ permissions and footer (experiment): 458 // +--------------------+------+ 459 // | heading | icon | 460 // +--------------------| | 461 // | permissions_header | | 462 // +--------------------| | 463 // | permission1 | | 464 // +--------------------| | 465 // | permission2 | | 466 // +--------------------+------+ 467 // | footer text | | 468 // +--------------------+------+ 469 // 470 // Regular install w/ permissions and inline explanations (experiment): 471 // +--------------------+------+ 472 // | heading | icon | 473 // +--------------------| | 474 // | permissions_header | | 475 // +--------------------| | 476 // | permission1 | | 477 // +--------------------| | 478 // | explanation1 | | 479 // +--------------------| | 480 // | permission2 | | 481 // +--------------------| | 482 // | explanation2 | | 483 // +--------------------+------+ 484 // 485 // Regular install w/ permissions and inline explanations (experiment): 486 // +--------------------+------+ 487 // | heading | icon | 488 // +--------------------| | 489 // | permissions_header | | 490 // +--------------------| | 491 // |checkbox|permission1| | 492 // +--------------------| | 493 // |checkbox|permission2| | 494 // +--------------------+------+ 495 // 496 // Additionally, links or informational text is added to non-client areas of 497 // the dialog depending on the experiment group. 498 499 int left_column_width = 500 (prompt->ShouldShowPermissions() + prompt->GetRetainedFileCount()) > 0 501 ? kPermissionsLeftColumnWidth 502 : kNoPermissionsLeftColumnWidth; 503 if (is_bundle_install()) 504 left_column_width = kBundleLeftColumnWidth; 505 if (is_external_install()) 506 left_column_width = kExternalInstallLeftColumnWidth; 507 508 scroll_view_ = new views::ScrollView(); 509 scroll_view_->set_hide_horizontal_scrollbar(true); 510 AddChildView(scroll_view_); 511 512 int column_set_id = 0; 513 // Create the full scrollable view which will contain all the information 514 // including the permissions. 515 scrollable_ = new CustomScrollableView(); 516 views::GridLayout* layout = CreateLayout( 517 scrollable_, left_column_width, column_set_id, false); 518 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 519 520 if (prompt->ShouldShowPermissions() && 521 prompt->experiment()->should_show_expandable_permission_list()) { 522 // If the experiment should hide the permission list initially, create a 523 // simple layout that contains only the header, extension name and icon. 524 scrollable_header_only_ = new CustomScrollableView(); 525 CreateLayout(scrollable_header_only_, left_column_width, 526 column_set_id, true); 527 scroll_view_->SetContents(scrollable_header_only_); 528 } else { 529 scroll_view_->SetContents(scrollable_); 530 } 531 532 int dialog_width = left_column_width + 2 * views::kPanelHorizMargin; 533 if (!is_bundle_install()) 534 dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset; 535 536 // Widen the dialog for experiment with checkboxes so that the information 537 // label fits the area to the left of the buttons. 538 if (prompt->experiment()->show_checkboxes()) 539 dialog_width += 4 * views::kPanelHorizMargin; 540 541 if (prompt->has_webstore_data()) { 542 layout->StartRow(0, column_set_id); 543 views::View* rating = new views::View(); 544 rating->SetLayoutManager(new views::BoxLayout( 545 views::BoxLayout::kHorizontal, 0, 0, 0)); 546 layout->AddView(rating); 547 prompt->AppendRatingStars(AddResourceIcon, rating); 548 549 const gfx::FontList& small_font_list = 550 rb.GetFontList(ui::ResourceBundle::SmallFont); 551 views::Label* rating_count = 552 new views::Label(prompt->GetRatingCount(), small_font_list); 553 // Add some space between the stars and the rating count. 554 rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0)); 555 rating->AddChildView(rating_count); 556 557 layout->StartRow(0, column_set_id); 558 views::Label* user_count = 559 new views::Label(prompt->GetUserCount(), small_font_list); 560 user_count->SetAutoColorReadabilityEnabled(false); 561 user_count->SetEnabledColor(SK_ColorGRAY); 562 layout->AddView(user_count); 563 564 layout->StartRow(0, column_set_id); 565 views::Link* store_link = new views::Link( 566 l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK)); 567 store_link->SetFontList(small_font_list); 568 store_link->set_listener(this); 569 layout->AddView(store_link); 570 } 571 572 if (is_bundle_install()) { 573 BundleInstaller::ItemList items = prompt->bundle()->GetItemsWithState( 574 BundleInstaller::Item::STATE_PENDING); 575 for (size_t i = 0; i < items.size(); ++i) { 576 base::string16 extension_name = 577 base::UTF8ToUTF16(items[i].localized_name); 578 base::i18n::AdjustStringForLocaleDirection(&extension_name); 579 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 580 layout->StartRow(0, column_set_id); 581 views::Label* extension_label = new views::Label( 582 PrepareForDisplay(extension_name, true)); 583 extension_label->SetMultiLine(true); 584 extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 585 extension_label->SizeToFit(left_column_width); 586 layout->AddView(extension_label); 587 } 588 } 589 590 if (prompt->ShouldShowPermissions()) { 591 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 592 593 if (prompt->GetPermissionCount() > 0) { 594 if (is_inline_install()) { 595 layout->StartRow(0, column_set_id); 596 layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 597 3, 1, views::GridLayout::FILL, views::GridLayout::FILL); 598 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 599 } 600 601 layout->StartRow(0, column_set_id); 602 views::Label* permissions_header = NULL; 603 if (is_bundle_install()) { 604 // We need to pass the FontList in the constructor, rather than calling 605 // SetFontList later, because otherwise SizeToFit mis-judges the width 606 // of the line. 607 permissions_header = 608 new views::Label(prompt->GetPermissionsHeading(), 609 rb.GetFontList(ui::ResourceBundle::MediumFont)); 610 } else { 611 permissions_header = new views::Label(prompt->GetPermissionsHeading()); 612 } 613 permissions_header->SetMultiLine(true); 614 permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); 615 permissions_header->SizeToFit(left_column_width); 616 layout->AddView(permissions_header); 617 618 for (size_t i = 0; i < prompt->GetPermissionCount(); ++i) { 619 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 620 layout->StartRow(0, column_set_id); 621 views::Label* permission_label = 622 new views::Label(prompt->GetPermission(i)); 623 624 const SkColor kTextHighlight = SK_ColorRED; 625 const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3); 626 if (prompt->experiment()->ShouldHighlightText( 627 prompt->GetPermission(i))) { 628 permission_label->SetAutoColorReadabilityEnabled(false); 629 permission_label->SetEnabledColor(kTextHighlight); 630 } else if (prompt->experiment()->ShouldHighlightBackground( 631 prompt->GetPermission(i))) { 632 permission_label->SetLineHeight(18); 633 permission_label->set_background( 634 views::Background::CreateSolidBackground(kBackgroundHighlight)); 635 } 636 637 permission_label->SetMultiLine(true); 638 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 639 permission_label->SizeToFit(left_column_width); 640 641 if (prompt->experiment()->show_checkboxes()) { 642 layout->AddView(new CheckboxedView(permission_label, this)); 643 ++unchecked_boxes_; 644 } else { 645 layout->AddView(new BulletedView(permission_label)); 646 } 647 // If we have more details to provide, show them in collapsed form. 648 if (!prompt->GetPermissionsDetails(i).empty()) { 649 layout->StartRow(0, column_set_id); 650 PermissionDetails details; 651 details.push_back( 652 PrepareForDisplay(prompt->GetPermissionsDetails(i), false)); 653 ExpandableContainerView* details_container = 654 new ExpandableContainerView( 655 this, base::string16(), details, left_column_width, 656 true, true, false); 657 layout->AddView(details_container); 658 } 659 660 if (prompt->experiment()->should_show_inline_explanations()) { 661 base::string16 explanation = 662 prompt->experiment()->GetInlineExplanation( 663 prompt->GetPermission(i)); 664 if (!explanation.empty()) { 665 PermissionDetails details; 666 details.push_back(explanation); 667 ExpandableContainerView* container = 668 new ExpandableContainerView(this, base::string16(), details, 669 left_column_width, 670 false, false, true); 671 // Inline explanations are expanded by default if there is 672 // no "Show details" link. 673 if (!prompt->experiment()->show_details_link()) 674 container->ExpandWithoutAnimation(); 675 layout->StartRow(0, column_set_id); 676 layout->AddView(container); 677 inline_explanations_.push_back(container); 678 } 679 } 680 } 681 } else { 682 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 683 layout->StartRow(0, column_set_id); 684 views::Label* permission_label = new views::Label( 685 l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)); 686 permission_label->SetMultiLine(true); 687 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 688 permission_label->SizeToFit(left_column_width); 689 layout->AddView(permission_label); 690 } 691 } 692 693 if (prompt->GetRetainedFileCount()) { 694 // Slide in under the permissions, if there are any. If there are 695 // either, the retained files prompt stretches all the way to the 696 // right of the dialog. If there are no permissions, the retained 697 // files prompt just takes up the left column. 698 int space_for_files = left_column_width; 699 if (prompt->GetPermissionCount()) { 700 space_for_files += kIconSize; 701 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id); 702 column_set->AddColumn(views::GridLayout::FILL, 703 views::GridLayout::FILL, 704 1, 705 views::GridLayout::USE_PREF, 706 0, // no fixed width 707 space_for_files); 708 } 709 710 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 711 712 layout->StartRow(0, column_set_id); 713 views::Label* retained_files_header = NULL; 714 retained_files_header = 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); 719 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, 727 false, true, false); 728 layout->AddView(issue_advice_view); 729 } 730 731 DCHECK(prompt->type() >= 0); 732 UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type", 733 prompt->type(), 734 ExtensionInstallPrompt::NUM_PROMPT_TYPES); 735 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, 741 1, 742 views::GridLayout::USE_PREF, 743 0, 744 0); 745 // Add two rows of space so that the text stands out. 746 layout->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing); 747 748 layout->StartRow(0, column_set_id); 749 views::Label* explanation = 750 new views::Label(prompt->experiment()->GetExplanationText()); 751 explanation->SetMultiLine(true); 752 explanation->SetHorizontalAlignment(gfx::ALIGN_LEFT); 753 explanation->SizeToFit(left_column_width + kIconSize); 754 layout->AddView(explanation); 755 } 756 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) { 765 int text_id = 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); 774 } else { 775 UpdateLinkActionHistogram(LINK_NOT_SHOWN); 776 } 777 } 778 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); 787 } 788 } 789 790 gfx::Size scrollable_size = scrollable_->GetPreferredSize(); 791 scrollable_->SetBoundsRect(gfx::Rect(scrollable_size)); 792 dialog_size_ = gfx::Size( 793 dialog_width, 794 std::min(scrollable_size.height(), kDialogMaxHeight)); 795 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)); 801 } 802 } 803 804 ExtensionInstallDialogView::~ExtensionInstallDialogView() {} 805 806 views::GridLayout* ExtensionInstallDialogView::CreateLayout( 807 views::View* parent, 808 int left_column_width, 809 int column_set_id, 810 bool single_detail_row) const { 811 views::GridLayout* layout = views::GridLayout::CreatePanel(parent); 812 parent->SetLayoutManager(layout); 813 814 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id); 815 column_set->AddColumn(views::GridLayout::LEADING, 816 views::GridLayout::FILL, 817 0, // no resizing 818 views::GridLayout::USE_PREF, 819 0, // no fixed width 820 left_column_width); 821 if (!is_bundle_install()) { 822 column_set->AddPaddingColumn(0, views::kPanelHorizMargin); 823 column_set->AddColumn(views::GridLayout::TRAILING, 824 views::GridLayout::LEADING, 825 0, // no resizing 826 views::GridLayout::USE_PREF, 827 0, // no fixed width 828 kIconSize); 829 } 830 831 layout->StartRow(0, column_set_id); 832 833 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 834 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); 841 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); 855 } else { 856 int icon_row_span = 1; 857 if (is_inline_install()) { 858 // Also span the rating, user_count and store_link rows. 859 icon_row_span = 4; 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. 868 icon_row_span = 4; 869 } 870 layout->AddView(icon, 1, icon_row_span); 871 } 872 } 873 return layout; 874 } 875 876 void ExtensionInstallDialogView::ContentsChanged() { 877 Layout(); 878 } 879 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_); 893 } else { 894 if (show_details_link_) 895 details.parent->RemoveChildView(show_details_link_); 896 if (checkbox_info_label_) 897 details.parent->RemoveChildView(checkbox_info_label_); 898 } 899 } 900 } 901 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); 907 return buttons; 908 } 909 910 base::string16 ExtensionInstallDialogView::GetDialogButtonLabel( 911 ui::DialogButton button) const { 912 switch (button) { 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); 919 default: 920 NOTREACHED(); 921 return base::string16(); 922 } 923 } 924 925 int ExtensionInstallDialogView::GetDefaultDialogButton() const { 926 return ui::DIALOG_BUTTON_CANCEL; 927 } 928 929 bool ExtensionInstallDialogView::Cancel() { 930 UpdateInstallResultHistogram(false); 931 delegate_->InstallUIAbort(true); 932 return true; 933 } 934 935 bool ExtensionInstallDialogView::Accept() { 936 UpdateInstallResultHistogram(true); 937 delegate_->InstallUIProceed(); 938 return true; 939 } 940 941 ui::ModalType ExtensionInstallDialogView::GetModalType() const { 942 return ui::MODAL_TYPE_WINDOW; 943 } 944 945 base::string16 ExtensionInstallDialogView::GetWindowTitle() const { 946 return prompt_->GetDialogTitle(); 947 } 948 949 void ExtensionInstallDialogView::LinkClicked(views::Link* source, 950 int event_flags) { 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(), 960 kDialogMaxHeight); 961 bounds.set_height(spacing + content_height); 962 scroll_view_->SetContents(scrollable_); 963 GetWidget()->SetBoundsConstrained(bounds); 964 ContentsChanged(); 965 } else { 966 ToggleInlineExplanations(); 967 } 968 show_details_link_->SetVisible(false); 969 } else { 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, 975 false); 976 navigator_->OpenURL(params); 977 GetWidget()->Close(); 978 } 979 } 980 981 void ExtensionInstallDialogView::ToggleInlineExplanations() { 982 for (InlineExplanations::iterator it = inline_explanations_.begin(); 983 it != inline_explanations_.end(); ++it) 984 (*it)->ToggleDetailLevel(); 985 } 986 987 void ExtensionInstallDialogView::Layout() { 988 scroll_view_->SetBounds(0, 0, width(), height()); 989 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( 1005 horizontal_margin, 1006 y_buttons + (cancel_button->height() - link_size.height()) / 2, 1007 link_size.width(), link_size.height()); 1008 } 1009 if (checkbox_info_label_) { 1010 gfx::Size label_size = checkbox_info_label_->GetPreferredSize(); 1011 checkbox_info_label_->SetBounds( 1012 horizontal_margin, 1013 y_buttons + (cancel_button->height() - label_size.height()) / 2, 1014 label_size.width(), label_size.height()); 1015 checkbox_info_label_->SizeToFit(max_width); 1016 } 1017 } 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); 1022 1023 DialogDelegateView::Layout(); 1024 } 1025 1026 gfx::Size ExtensionInstallDialogView::GetPreferredSize() const { 1027 return dialog_size_; 1028 } 1029 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()) 1035 --unchecked_boxes_; 1036 else 1037 ++unchecked_boxes_; 1038 1039 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0); 1040 checkbox_info_label_->SetVisible(unchecked_boxes_ > 0); 1041 } 1042 } 1043 1044 void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted) 1045 const { 1046 if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT) 1047 UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted); 1048 } 1049 1050 void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type) 1051 const { 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", 1056 action_type, 1057 NUM_LINK_ACTIONS); 1058 } else { 1059 // The clickable link in the UI is "Show Details". 1060 UMA_HISTOGRAM_ENUMERATION( 1061 "Extensions.InstallPromptExperiment.ShowDetails", 1062 action_type, 1063 NUM_LINK_ACTIONS); 1064 } 1065 } 1066 1067 // static 1068 ExtensionInstallPrompt::ShowDialogCallback 1069 ExtensionInstallPrompt::GetDefaultShowDialogCallback() { 1070 return base::Bind(&ShowExtensionInstallDialogImpl); 1071 } 1072 1073 // ExpandableContainerView::DetailsView ---------------------------------------- 1074 1075 ExpandableContainerView::DetailsView::DetailsView(int horizontal_space, 1076 bool parent_bulleted, 1077 bool lighter_color) 1078 : layout_(new views::GridLayout(this)), 1079 state_(0), 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. 1087 int padding = 1088 views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1); 1089 column_set->AddPaddingColumn(0, padding); 1090 column_set->AddColumn(views::GridLayout::LEADING, 1091 views::GridLayout::LEADING, 1092 0, 1093 views::GridLayout::FIXED, 1094 horizontal_space - padding, 1095 0); 1096 } 1097 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); 1109 } 1110 layout_->AddView(detail_label); 1111 } 1112 1113 gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const { 1114 gfx::Size size = views::View::GetPreferredSize(); 1115 return gfx::Size(size.width(), size.height() * state_); 1116 } 1117 1118 void ExpandableContainerView::DetailsView::AnimateToState(double state) { 1119 state_ = state; 1120 PreferredSizeChanged(); 1121 SchedulePaint(); 1122 } 1123 1124 // ExpandableContainerView ----------------------------------------------------- 1125 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) 1134 : owner_(owner), 1135 details_view_(NULL), 1136 more_details_(NULL), 1137 slide_animation_(this), 1138 arrow_toggle_(NULL), 1139 expanded_(false) { 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, 1146 0, 1147 views::GridLayout::USE_PREF, 1148 0, 1149 0); 1150 if (!description.empty()) { 1151 layout->StartRow(0, column_set_id); 1152 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)); 1158 } 1159 1160 if (details.empty()) 1161 return; 1162 1163 details_view_ = new DetailsView(horizontal_space, parent_bulleted, 1164 lighter_color_details); 1165 1166 layout->StartRow(0, column_set_id); 1167 layout->AddView(details_view_); 1168 1169 for (size_t i = 0; i < details.size(); ++i) 1170 details_view_->AddDetail(details[i]); 1171 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)); 1176 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 1179 // toggle. 1180 int link_col_width = 1181 views::kRelatedControlHorizontalSpacing + 1182 std::max(gfx::GetStringWidth( 1183 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS), 1184 link->font_list()), 1185 gfx::GetStringWidth( 1186 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS), 1187 link->font_list())); 1188 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, 1200 0, 1201 views::GridLayout::FIXED, 1202 link_col_width, 1203 link_col_width); 1204 // The Up/Down arrow column. 1205 column_set->AddColumn(views::GridLayout::LEADING, 1206 views::GridLayout::LEADING, 1207 0, 1208 views::GridLayout::USE_PREF, 1209 0, 1210 0); 1211 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_); 1218 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_); 1225 } 1226 } 1227 1228 ExpandableContainerView::~ExpandableContainerView() { 1229 } 1230 1231 void ExpandableContainerView::ButtonPressed( 1232 views::Button* sender, const ui::Event& event) { 1233 ToggleDetailLevel(); 1234 } 1235 1236 void ExpandableContainerView::LinkClicked( 1237 views::Link* source, int event_flags) { 1238 ToggleDetailLevel(); 1239 } 1240 1241 void ExpandableContainerView::AnimationProgressed( 1242 const gfx::Animation* animation) { 1243 DCHECK_EQ(&slide_animation_, animation); 1244 if (details_view_) 1245 details_view_->AnimateToState(animation->GetCurrentValue()); 1246 } 1247 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( 1254 IDR_UP_ARROW)); 1255 } else { 1256 arrow_toggle_->SetImage( 1257 views::Button::STATE_NORMAL, 1258 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 1259 IDR_DOWN_ARROW)); 1260 } 1261 } 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)); 1266 } 1267 } 1268 1269 void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) { 1270 owner_->ContentsChanged(); 1271 } 1272 1273 void ExpandableContainerView::ToggleDetailLevel() { 1274 expanded_ = !expanded_; 1275 1276 if (slide_animation_.IsShowing()) 1277 slide_animation_.Hide(); 1278 else 1279 slide_animation_.Show(); 1280 } 1281 1282 void ExpandableContainerView::ExpandWithoutAnimation() { 1283 expanded_ = true; 1284 details_view_->AnimateToState(1.0); 1285 } 1286