extension_install_dialog_view.cc revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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 54using content::OpenURLParams; 55using content::Referrer; 56using extensions::BundleInstaller; 57 58namespace { 59 60// Size of extension icon in top left of dialog. 61const int kIconSize = 69; 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. 65const int kIconOffset = 16; 66 67// The dialog will resize based on its content, but this sets a maximum height 68// before overflowing a scrollbar. 69const int kDialogMaxHeight = 300; 70 71// Width of the left column of the dialog when the extension requests 72// permissions. 73const int kPermissionsLeftColumnWidth = 250; 74 75// Width of the left column of the dialog when the extension requests no 76// permissions. 77const 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. 81const 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. 85const int kExternalInstallLeftColumnWidth = 350; 86 87// Lighter color for labels. 88const 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. 94enum ExperimentLinkAction { 95 LINK_SHOWN = 0, 96 LINK_NOT_SHOWN, 97 LINK_CLICKED, 98 NUM_LINK_ACTIONS 99}; 100 101typedef std::vector<base::string16> PermissionDetails; 102class ExpandableContainerView; 103 104void 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. 113base::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. 121class 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. 133class ExtensionInstallDialogView : public views::DialogDelegateView, 134 public views::LinkListener, 135 public views::ButtonListener { 136 public: 137 ExtensionInstallDialogView(content::PageNavigator* navigator, 138 ExtensionInstallPrompt::Delegate* delegate, 139 const ExtensionInstallPrompt::Prompt& prompt); 140 virtual ~ExtensionInstallDialogView(); 141 142 // Called when one of the child elements has expanded/collapsed. 143 void ContentsChanged(); 144 145 private: 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() OVERRIDE; 157 virtual void ViewHierarchyChanged( 158 const ViewHierarchyChangedDetails& details) OVERRIDE; 159 160 // views::LinkListener: 161 virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE; 162 163 // views::ButtonListener: 164 virtual void ButtonPressed(views::Button* sender, 165 const ui::Event& event) OVERRIDE; 166 167 // Experimental: Toggles inline permission explanations with an animation. 168 void ToggleInlineExplanations(); 169 170 // Creates a layout consisting of dialog header, extension name and icon. 171 views::GridLayout* CreateLayout( 172 views::View* parent, 173 int left_column_width, 174 int column_set_id, 175 bool single_detail_row) const; 176 177 bool is_inline_install() const { 178 return prompt_.type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT; 179 } 180 181 bool is_bundle_install() const { 182 return prompt_.type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT; 183 } 184 185 bool is_external_install() const { 186 return prompt_.type() == ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT; 187 } 188 189 // Updates the histogram that holds installation accepted/aborted data. 190 void UpdateInstallResultHistogram(bool accepted) const; 191 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; 195 196 content::PageNavigator* navigator_; 197 ExtensionInstallPrompt::Delegate* delegate_; 198 const ExtensionInstallPrompt::Prompt& prompt_; 199 200 // The scroll view containing all the details for the dialog (including all 201 // collapsible/expandable sections). 202 views::ScrollView* scroll_view_; 203 204 // The container view for the scroll view. 205 CustomScrollableView* scrollable_; 206 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_; 211 212 // The preferred size of the dialog. 213 gfx::Size dialog_size_; 214 215 // Experimental: "Show details" link to expand inline explanations and reveal 216 // permision dialog. 217 views::Link* show_details_link_; 218 219 // Experimental: Label for showing information about the checkboxes. 220 views::Label* checkbox_info_label_; 221 222 // Experimental: Contains pointers to inline explanation views. 223 typedef std::vector<ExpandableContainerView*> InlineExplanations; 224 InlineExplanations inline_explanations_; 225 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_; 229 230 DISALLOW_COPY_AND_ASSIGN(ExtensionInstallDialogView); 231}; 232 233// A simple view that prepends a view with a bullet with the help of a grid 234// layout. 235class BulletedView : public views::View { 236 public: 237 explicit BulletedView(views::View* view); 238 private: 239 DISALLOW_COPY_AND_ASSIGN(BulletedView); 240}; 241 242BulletedView::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, 248 0, 249 views::GridLayout::USE_PREF, 250 0, // no fixed width 251 0); 252 column_set->AddColumn(views::GridLayout::LEADING, 253 views::GridLayout::LEADING, 254 0, 255 views::GridLayout::USE_PREF, 256 0, // no fixed width 257 0); 258 layout->StartRow(0, 0); 259 layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true))); 260 layout->AddView(view); 261} 262 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. 266class CheckboxedView : public views::View { 267 public: 268 CheckboxedView(views::View* view, views::ButtonListener* listener); 269 private: 270 DISALLOW_COPY_AND_ASSIGN(CheckboxedView); 271}; 272 273CheckboxedView::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, 280 0, 281 views::GridLayout::USE_PREF, 282 0, // no fixed width 283 0); 284 column_set->AddColumn(views::GridLayout::LEADING, 285 views::GridLayout::LEADING, 286 0, 287 views::GridLayout::USE_PREF, 288 0, // no fixed width 289 0); 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); 299} 300 301// A view to display text with an expandable details section. 302class ExpandableContainerView : public views::View, 303 public views::ButtonListener, 304 public views::LinkListener, 305 public gfx::AnimationDelegate { 306 public: 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(); 315 316 // views::View: 317 virtual void ChildPreferredSizeChanged(views::View* child) OVERRIDE; 318 319 // views::ButtonListener: 320 virtual void ButtonPressed(views::Button* sender, 321 const ui::Event& event) OVERRIDE; 322 323 // views::LinkListener: 324 virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE; 325 326 // gfx::AnimationDelegate: 327 virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE; 328 virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE; 329 330 // Expand/Collapse the detail section for this ExpandableContainerView. 331 void ToggleDetailLevel(); 332 333 // Expand the detail section without any animation. 334 // TODO(meacer): Remove once the experiment is completed. 335 void ExpandWithoutAnimation(); 336 337 private: 338 // A view which displays all the details of an IssueAdviceInfoEntry. 339 class DetailsView : public views::View { 340 public: 341 explicit DetailsView(int horizontal_space, bool parent_bulleted, 342 bool lighter_color); 343 virtual ~DetailsView() {} 344 345 // views::View: 346 virtual gfx::Size GetPreferredSize() OVERRIDE; 347 348 void AddDetail(const base::string16& detail); 349 350 // Animates this to be a height proportional to |state|. 351 void AnimateToState(double state); 352 353 private: 354 views::GridLayout* layout_; 355 double state_; 356 357 // Whether the detail text should be shown with a lighter color. 358 bool lighter_color_; 359 360 DISALLOW_COPY_AND_ASSIGN(DetailsView); 361 }; 362 363 // The dialog that owns |this|. It's also an ancestor in the View hierarchy. 364 ExtensionInstallDialogView* owner_; 365 366 // A view for showing |issue_advice.details|. 367 DetailsView* details_view_; 368 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_; 372 373 gfx::SlideAnimation slide_animation_; 374 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_; 378 379 // Whether the details section is expanded. 380 bool expanded_; 381 382 DISALLOW_COPY_AND_ASSIGN(ExpandableContainerView); 383}; 384 385void ShowExtensionInstallDialogImpl( 386 const ExtensionInstallPrompt::ShowParams& show_params, 387 ExtensionInstallPrompt::Delegate* delegate, 388 const ExtensionInstallPrompt::Prompt& prompt) { 389 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 390 CreateBrowserModalDialogViews( 391 new ExtensionInstallDialogView(show_params.navigator, delegate, prompt), 392 show_params.parent_window)->Show(); 393} 394 395} // namespace 396 397CustomScrollableView::CustomScrollableView() {} 398CustomScrollableView::~CustomScrollableView() {} 399 400void CustomScrollableView::Layout() { 401 SetBounds(x(), y(), width(), GetHeightForWidth(width())); 402 views::View::Layout(); 403} 404 405ExtensionInstallDialogView::ExtensionInstallDialogView( 406 content::PageNavigator* navigator, 407 ExtensionInstallPrompt::Delegate* delegate, 408 const ExtensionInstallPrompt::Prompt& prompt) 409 : navigator_(navigator), 410 delegate_(delegate), 411 prompt_(prompt), 412 scroll_view_(NULL), 413 scrollable_(NULL), 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: 419 // Inline install 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 // +--------------------+------+ +--------------+------+ 430 // | separator | 431 // +--------------------+------+ 432 // | permissions_header | | 433 // +--------------------+------+ 434 // | permission1 | | 435 // +--------------------+------+ 436 // | permission2 | | 437 // +--------------------+------+ 438 // 439 // Regular install 440 // w/ permissions XOR oauth issues no permissions 441 // +--------------------+------+ +--------------+------+ 442 // | heading | icon | | heading | icon | 443 // +--------------------| | +--------------+------+ 444 // | permissions_header | | 445 // +--------------------| | 446 // | permission1 | | 447 // +--------------------| | 448 // | permission2 | | 449 // +--------------------+------+ 450 // 451 // w/ permissions AND oauth issues 452 // +--------------------+------+ 453 // | heading | icon | 454 // +--------------------| | 455 // | permissions_header | | 456 // +--------------------| | 457 // | permission1 | | 458 // +--------------------| | 459 // | permission2 | | 460 // +--------------------+------+ 461 // | oauth header | 462 // +---------------------------+ 463 // | oauth issue 1 | 464 // +---------------------------+ 465 // | oauth issue 2 | 466 // +---------------------------+ 467 // 468 // If the ExtensionPermissionDialog is on, the layout is modified depending 469 // on the experiment group. For text only experiment, a footer is added at the 470 // bottom of the layouts. For others, inline details are added below some of 471 // the permissions. 472 // 473 // Regular install w/ permissions and footer (experiment): 474 // +--------------------+------+ 475 // | heading | icon | 476 // +--------------------| | 477 // | permissions_header | | 478 // +--------------------| | 479 // | permission1 | | 480 // +--------------------| | 481 // | permission2 | | 482 // +--------------------+------+ 483 // | footer text | | 484 // +--------------------+------+ 485 // 486 // Regular install w/ permissions and inline explanations (experiment): 487 // +--------------------+------+ 488 // | heading | icon | 489 // +--------------------| | 490 // | permissions_header | | 491 // +--------------------| | 492 // | permission1 | | 493 // +--------------------| | 494 // | explanation1 | | 495 // +--------------------| | 496 // | permission2 | | 497 // +--------------------| | 498 // | explanation2 | | 499 // +--------------------+------+ 500 // 501 // Regular install w/ permissions and inline explanations (experiment): 502 // +--------------------+------+ 503 // | heading | icon | 504 // +--------------------| | 505 // | permissions_header | | 506 // +--------------------| | 507 // |checkbox|permission1| | 508 // +--------------------| | 509 // |checkbox|permission2| | 510 // +--------------------+------+ 511 // 512 // Additionally, links or informational text is added to non-client areas of 513 // the dialog depending on the experiment group. 514 515 int left_column_width = 516 (prompt.ShouldShowPermissions() + prompt.GetOAuthIssueCount() + 517 prompt.GetRetainedFileCount()) > 0 ? 518 kPermissionsLeftColumnWidth : kNoPermissionsLeftColumnWidth; 519 if (is_bundle_install()) 520 left_column_width = kBundleLeftColumnWidth; 521 if (is_external_install()) 522 left_column_width = kExternalInstallLeftColumnWidth; 523 524 scroll_view_ = new views::ScrollView(); 525 scroll_view_->set_hide_horizontal_scrollbar(true); 526 AddChildView(scroll_view_); 527 528 int column_set_id = 0; 529 // Create the full scrollable view which will contain all the information 530 // including the permissions. 531 scrollable_ = new CustomScrollableView(); 532 views::GridLayout* layout = CreateLayout( 533 scrollable_, left_column_width, column_set_id, false); 534 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 535 536 if (prompt.ShouldShowPermissions() && 537 prompt.experiment()->should_show_expandable_permission_list()) { 538 // If the experiment should hide the permission list initially, create a 539 // simple layout that contains only the header, extension name and icon. 540 scrollable_header_only_ = new CustomScrollableView(); 541 CreateLayout(scrollable_header_only_, left_column_width, 542 column_set_id, true); 543 scroll_view_->SetContents(scrollable_header_only_); 544 } else { 545 scroll_view_->SetContents(scrollable_); 546 } 547 548 int dialog_width = left_column_width + 2 * views::kPanelHorizMargin; 549 if (!is_bundle_install()) 550 dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset; 551 552 // Widen the dialog for experiment with checkboxes so that the information 553 // label fits the area to the left of the buttons. 554 if (prompt.experiment()->show_checkboxes()) 555 dialog_width += 4 * views::kPanelHorizMargin; 556 557 if (prompt.has_webstore_data()) { 558 layout->StartRow(0, column_set_id); 559 views::View* rating = new views::View(); 560 rating->SetLayoutManager(new views::BoxLayout( 561 views::BoxLayout::kHorizontal, 0, 0, 0)); 562 layout->AddView(rating); 563 prompt.AppendRatingStars(AddResourceIcon, rating); 564 565 const gfx::FontList& small_font_list = 566 rb.GetFontList(ui::ResourceBundle::SmallFont); 567 views::Label* rating_count = 568 new views::Label(prompt.GetRatingCount(), small_font_list); 569 // Add some space between the stars and the rating count. 570 rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0)); 571 rating->AddChildView(rating_count); 572 573 layout->StartRow(0, column_set_id); 574 views::Label* user_count = 575 new views::Label(prompt.GetUserCount(), small_font_list); 576 user_count->SetAutoColorReadabilityEnabled(false); 577 user_count->SetEnabledColor(SK_ColorGRAY); 578 layout->AddView(user_count); 579 580 layout->StartRow(0, column_set_id); 581 views::Link* store_link = new views::Link( 582 l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK)); 583 store_link->SetFontList(small_font_list); 584 store_link->set_listener(this); 585 layout->AddView(store_link); 586 } 587 588 if (is_bundle_install()) { 589 BundleInstaller::ItemList items = prompt.bundle()->GetItemsWithState( 590 BundleInstaller::Item::STATE_PENDING); 591 for (size_t i = 0; i < items.size(); ++i) { 592 base::string16 extension_name = 593 base::UTF8ToUTF16(items[i].localized_name); 594 base::i18n::AdjustStringForLocaleDirection(&extension_name); 595 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 596 layout->StartRow(0, column_set_id); 597 views::Label* extension_label = new views::Label( 598 PrepareForDisplay(extension_name, true)); 599 extension_label->SetMultiLine(true); 600 extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 601 extension_label->SizeToFit(left_column_width); 602 layout->AddView(extension_label); 603 } 604 } 605 606 if (prompt.ShouldShowPermissions()) { 607 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 608 609 if (prompt.GetPermissionCount() > 0) { 610 if (is_inline_install()) { 611 layout->StartRow(0, column_set_id); 612 layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 613 3, 1, views::GridLayout::FILL, views::GridLayout::FILL); 614 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 615 } 616 617 layout->StartRow(0, column_set_id); 618 views::Label* permissions_header = NULL; 619 if (is_bundle_install()) { 620 // We need to pass the FontList in the constructor, rather than calling 621 // SetFontList later, because otherwise SizeToFit mis-judges the width 622 // of the line. 623 permissions_header = new views::Label( 624 prompt.GetPermissionsHeading(), 625 rb.GetFontList(ui::ResourceBundle::MediumFont)); 626 } else { 627 permissions_header = new views::Label(prompt.GetPermissionsHeading()); 628 } 629 permissions_header->SetMultiLine(true); 630 permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); 631 permissions_header->SizeToFit(left_column_width); 632 layout->AddView(permissions_header); 633 634 for (size_t i = 0; i < prompt.GetPermissionCount(); ++i) { 635 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 636 layout->StartRow(0, column_set_id); 637 views::Label* permission_label = 638 new views::Label(prompt.GetPermission(i)); 639 640 const SkColor kTextHighlight = SK_ColorRED; 641 const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3); 642 if (prompt.experiment()->ShouldHighlightText( 643 prompt.GetPermission(i))) { 644 permission_label->SetAutoColorReadabilityEnabled(false); 645 permission_label->SetEnabledColor(kTextHighlight); 646 } else if (prompt.experiment()->ShouldHighlightBackground( 647 prompt.GetPermission(i))) { 648 permission_label->SetLineHeight(18); 649 permission_label->set_background( 650 views::Background::CreateSolidBackground(kBackgroundHighlight)); 651 } 652 653 permission_label->SetMultiLine(true); 654 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 655 permission_label->SizeToFit(left_column_width); 656 657 if (prompt.experiment()->show_checkboxes()) { 658 layout->AddView(new CheckboxedView(permission_label, this)); 659 ++unchecked_boxes_; 660 } else { 661 layout->AddView(new BulletedView(permission_label)); 662 } 663 // If we have more details to provide, show them in collapsed form. 664 if (!prompt.GetPermissionsDetails(i).empty()) { 665 layout->StartRow(0, column_set_id); 666 PermissionDetails details; 667 details.push_back( 668 PrepareForDisplay(prompt.GetPermissionsDetails(i), false)); 669 ExpandableContainerView* details_container = 670 new ExpandableContainerView( 671 this, base::string16(), details, left_column_width, 672 true, true, false); 673 layout->AddView(details_container); 674 } 675 676 if (prompt.experiment()->should_show_inline_explanations()) { 677 base::string16 explanation = 678 prompt.experiment()->GetInlineExplanation( 679 prompt.GetPermission(i)); 680 if (!explanation.empty()) { 681 PermissionDetails details; 682 details.push_back(explanation); 683 ExpandableContainerView* container = 684 new ExpandableContainerView(this, base::string16(), details, 685 left_column_width, 686 false, false, true); 687 // Inline explanations are expanded by default if there is 688 // no "Show details" link. 689 if (!prompt.experiment()->show_details_link()) 690 container->ExpandWithoutAnimation(); 691 layout->StartRow(0, column_set_id); 692 layout->AddView(container); 693 inline_explanations_.push_back(container); 694 } 695 } 696 } 697 } else { 698 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 699 layout->StartRow(0, column_set_id); 700 views::Label* permission_label = new views::Label( 701 l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)); 702 permission_label->SetMultiLine(true); 703 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 704 permission_label->SizeToFit(left_column_width); 705 layout->AddView(permission_label); 706 } 707 } 708 709 if (prompt.GetOAuthIssueCount()) { 710 // Slide in under the permissions, if there are any. If there are 711 // permissions, the OAuth prompt stretches all the way to the right of the 712 // dialog. If there are no permissions, the OAuth prompt just takes up the 713 // left column. 714 int space_for_oauth = left_column_width; 715 if (prompt.GetPermissionCount()) { 716 space_for_oauth += kIconSize; 717 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id); 718 column_set->AddColumn(views::GridLayout::FILL, 719 views::GridLayout::FILL, 720 1, 721 views::GridLayout::USE_PREF, 722 0, // no fixed width 723 space_for_oauth); 724 } 725 726 layout->StartRowWithPadding(0, column_set_id, 727 0, views::kRelatedControlVerticalSpacing); 728 views::Label* oauth_header = new views::Label(prompt.GetOAuthHeading()); 729 oauth_header->SetMultiLine(true); 730 oauth_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); 731 oauth_header->SizeToFit(left_column_width); 732 layout->AddView(oauth_header); 733 734 for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) { 735 layout->StartRowWithPadding( 736 0, column_set_id, 737 0, views::kRelatedControlVerticalSpacing); 738 739 PermissionDetails details; 740 const IssueAdviceInfoEntry& entry = prompt.GetOAuthIssue(i); 741 for (size_t x = 0; x < entry.details.size(); ++x) 742 details.push_back(entry.details[x]); 743 ExpandableContainerView* issue_advice_view = 744 new ExpandableContainerView( 745 this, entry.description, details, space_for_oauth, 746 true, true, false); 747 layout->AddView(issue_advice_view); 748 } 749 } 750 if (prompt.GetRetainedFileCount()) { 751 // Slide in under the permissions or OAuth, if there are any. If there are 752 // either, the retained files prompt stretches all the way to the right of 753 // the dialog. If there are no permissions or OAuth, the retained files 754 // prompt just takes up the left column. 755 int space_for_files = left_column_width; 756 if (prompt.GetPermissionCount() || prompt.GetOAuthIssueCount()) { 757 space_for_files += kIconSize; 758 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id); 759 column_set->AddColumn(views::GridLayout::FILL, 760 views::GridLayout::FILL, 761 1, 762 views::GridLayout::USE_PREF, 763 0, // no fixed width 764 space_for_files); 765 } 766 767 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 768 769 layout->StartRow(0, column_set_id); 770 views::Label* retained_files_header = NULL; 771 retained_files_header = 772 new views::Label(prompt.GetRetainedFilesHeading()); 773 retained_files_header->SetMultiLine(true); 774 retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); 775 retained_files_header->SizeToFit(space_for_files); 776 layout->AddView(retained_files_header); 777 778 layout->StartRow(0, column_set_id); 779 PermissionDetails details; 780 for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i) 781 details.push_back(prompt.GetRetainedFile(i)); 782 ExpandableContainerView* issue_advice_view = 783 new ExpandableContainerView( 784 this, base::string16(), details, space_for_files, 785 false, true, false); 786 layout->AddView(issue_advice_view); 787 } 788 789 DCHECK(prompt.type() >= 0); 790 UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type", 791 prompt.type(), 792 ExtensionInstallPrompt::NUM_PROMPT_TYPES); 793 794 if (prompt.ShouldShowPermissions()) { 795 if (prompt.ShouldShowExplanationText()) { 796 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id); 797 column_set->AddColumn(views::GridLayout::LEADING, 798 views::GridLayout::FILL, 799 1, 800 views::GridLayout::USE_PREF, 801 0, 802 0); 803 // Add two rows of space so that the text stands out. 804 layout->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing); 805 806 layout->StartRow(0, column_set_id); 807 views::Label* explanation = new views::Label( 808 prompt.experiment()->GetExplanationText()); 809 explanation->SetMultiLine(true); 810 explanation->SetHorizontalAlignment(gfx::ALIGN_LEFT); 811 explanation->SizeToFit(left_column_width + kIconSize); 812 layout->AddView(explanation); 813 } 814 815 if (prompt.experiment()->should_show_expandable_permission_list() || 816 (prompt.experiment()->show_details_link() && 817 prompt.experiment()->should_show_inline_explanations() && 818 !inline_explanations_.empty())) { 819 // Don't show the "Show details" link if there are OAuth issues or 820 // retained files. These have their own "Show details" links and having 821 // multiple levels of links is confusing. 822 if (prompt.GetOAuthIssueCount() + prompt.GetRetainedFileCount() == 0) { 823 int text_id = 824 prompt.experiment()->should_show_expandable_permission_list() ? 825 IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS : 826 IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_DETAILS; 827 show_details_link_ = new views::Link( 828 l10n_util::GetStringUTF16(text_id)); 829 show_details_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 830 show_details_link_->set_listener(this); 831 UpdateLinkActionHistogram(LINK_SHOWN); 832 } else { 833 UpdateLinkActionHistogram(LINK_NOT_SHOWN); 834 } 835 } 836 837 if (prompt.experiment()->show_checkboxes()) { 838 checkbox_info_label_ = new views::Label( 839 l10n_util::GetStringUTF16( 840 IDS_EXTENSION_PROMPT_EXPERIMENT_CHECKBOX_INFO)); 841 checkbox_info_label_->SetMultiLine(true); 842 checkbox_info_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 843 checkbox_info_label_->SetAutoColorReadabilityEnabled(false); 844 checkbox_info_label_->SetEnabledColor(kLighterLabelColor); 845 } 846 } 847 848 gfx::Size scrollable_size = scrollable_->GetPreferredSize(); 849 scrollable_->SetBoundsRect(gfx::Rect(scrollable_size)); 850 dialog_size_ = gfx::Size( 851 dialog_width, 852 std::min(scrollable_size.height(), kDialogMaxHeight)); 853 854 if (scrollable_header_only_) { 855 gfx::Size header_only_size = scrollable_header_only_->GetPreferredSize(); 856 scrollable_header_only_->SetBoundsRect(gfx::Rect(header_only_size)); 857 dialog_size_ = gfx::Size( 858 dialog_width, std::min(header_only_size.height(), kDialogMaxHeight)); 859 } 860} 861 862ExtensionInstallDialogView::~ExtensionInstallDialogView() {} 863 864views::GridLayout* ExtensionInstallDialogView::CreateLayout( 865 views::View* parent, 866 int left_column_width, 867 int column_set_id, 868 bool single_detail_row) const { 869 views::GridLayout* layout = views::GridLayout::CreatePanel(parent); 870 parent->SetLayoutManager(layout); 871 872 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id); 873 column_set->AddColumn(views::GridLayout::LEADING, 874 views::GridLayout::FILL, 875 0, // no resizing 876 views::GridLayout::USE_PREF, 877 0, // no fixed width 878 left_column_width); 879 if (!is_bundle_install()) { 880 column_set->AddPaddingColumn(0, views::kPanelHorizMargin); 881 column_set->AddColumn(views::GridLayout::TRAILING, 882 views::GridLayout::LEADING, 883 0, // no resizing 884 views::GridLayout::USE_PREF, 885 0, // no fixed width 886 kIconSize); 887 } 888 889 layout->StartRow(0, column_set_id); 890 891 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 892 893 views::Label* heading = new views::Label( 894 prompt_.GetHeading(), rb.GetFontList(ui::ResourceBundle::MediumFont)); 895 heading->SetMultiLine(true); 896 heading->SetHorizontalAlignment(gfx::ALIGN_LEFT); 897 heading->SizeToFit(left_column_width); 898 layout->AddView(heading); 899 900 if (!is_bundle_install()) { 901 // Scale down to icon size, but allow smaller icons (don't scale up). 902 const gfx::ImageSkia* image = prompt_.icon().ToImageSkia(); 903 gfx::Size size(image->width(), image->height()); 904 if (size.width() > kIconSize || size.height() > kIconSize) 905 size = gfx::Size(kIconSize, kIconSize); 906 views::ImageView* icon = new views::ImageView(); 907 icon->SetImageSize(size); 908 icon->SetImage(*image); 909 icon->SetHorizontalAlignment(views::ImageView::CENTER); 910 icon->SetVerticalAlignment(views::ImageView::CENTER); 911 if (single_detail_row) { 912 layout->AddView(icon); 913 } else { 914 int icon_row_span = 1; 915 if (is_inline_install()) { 916 // Also span the rating, user_count and store_link rows. 917 icon_row_span = 4; 918 } else if (prompt_.ShouldShowPermissions()) { 919 size_t permission_count = prompt_.GetPermissionCount(); 920 // Also span the permission header and each of the permission rows (all 921 // have a padding row above it). This also works for the 'no special 922 // permissions' case. 923 icon_row_span = 3 + permission_count * 2; 924 } else if (prompt_.GetOAuthIssueCount()) { 925 // Also span the permission header and each of the permission rows (all 926 // have a padding row above it). 927 icon_row_span = 3 + prompt_.GetOAuthIssueCount() * 2; 928 } else if (prompt_.GetRetainedFileCount()) { 929 // Also span the permission header and the retained files container. 930 icon_row_span = 4; 931 } 932 layout->AddView(icon, 1, icon_row_span); 933 } 934 } 935 return layout; 936} 937 938void ExtensionInstallDialogView::ContentsChanged() { 939 Layout(); 940} 941 942void ExtensionInstallDialogView::ViewHierarchyChanged( 943 const ViewHierarchyChangedDetails& details) { 944 // Since we want the links to show up in the same visual row as the accept 945 // and cancel buttons, which is provided by the framework, we must add the 946 // buttons to the non-client view, which is the parent of this view. 947 // Similarly, when we're removed from the view hierarchy, we must take care 948 // to clean up those items as well. 949 if (details.child == this) { 950 if (details.is_add) { 951 if (show_details_link_) 952 details.parent->AddChildView(show_details_link_); 953 if (checkbox_info_label_) 954 details.parent->AddChildView(checkbox_info_label_); 955 } else { 956 if (show_details_link_) 957 details.parent->RemoveChildView(show_details_link_); 958 if (checkbox_info_label_) 959 details.parent->RemoveChildView(checkbox_info_label_); 960 } 961 } 962} 963 964int ExtensionInstallDialogView::GetDialogButtons() const { 965 int buttons = prompt_.GetDialogButtons(); 966 // Simply having just an OK button is *not* supported. See comment on function 967 // GetDialogButtons in dialog_delegate.h for reasons. 968 DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0); 969 return buttons; 970} 971 972base::string16 ExtensionInstallDialogView::GetDialogButtonLabel( 973 ui::DialogButton button) const { 974 switch (button) { 975 case ui::DIALOG_BUTTON_OK: 976 return prompt_.GetAcceptButtonLabel(); 977 case ui::DIALOG_BUTTON_CANCEL: 978 return prompt_.HasAbortButtonLabel() ? 979 prompt_.GetAbortButtonLabel() : 980 l10n_util::GetStringUTF16(IDS_CANCEL); 981 default: 982 NOTREACHED(); 983 return base::string16(); 984 } 985} 986 987int ExtensionInstallDialogView::GetDefaultDialogButton() const { 988 return ui::DIALOG_BUTTON_CANCEL; 989} 990 991bool ExtensionInstallDialogView::Cancel() { 992 UpdateInstallResultHistogram(false); 993 delegate_->InstallUIAbort(true); 994 return true; 995} 996 997bool ExtensionInstallDialogView::Accept() { 998 UpdateInstallResultHistogram(true); 999 delegate_->InstallUIProceed(); 1000 return true; 1001} 1002 1003ui::ModalType ExtensionInstallDialogView::GetModalType() const { 1004 return ui::MODAL_TYPE_WINDOW; 1005} 1006 1007base::string16 ExtensionInstallDialogView::GetWindowTitle() const { 1008 return prompt_.GetDialogTitle(); 1009} 1010 1011void ExtensionInstallDialogView::LinkClicked(views::Link* source, 1012 int event_flags) { 1013 if (source == show_details_link_) { 1014 UpdateLinkActionHistogram(LINK_CLICKED); 1015 // Show details link is used to either reveal whole permission list or to 1016 // reveal inline explanations. 1017 if (prompt_.experiment()->should_show_expandable_permission_list()) { 1018 gfx::Rect bounds = GetWidget()->GetWindowBoundsInScreen(); 1019 int spacing = bounds.height() - 1020 scrollable_header_only_->GetPreferredSize().height(); 1021 int content_height = std::min(scrollable_->GetPreferredSize().height(), 1022 kDialogMaxHeight); 1023 bounds.set_height(spacing + content_height); 1024 scroll_view_->SetContents(scrollable_); 1025 GetWidget()->SetBoundsConstrained(bounds); 1026 ContentsChanged(); 1027 } else { 1028 ToggleInlineExplanations(); 1029 } 1030 show_details_link_->SetVisible(false); 1031 } else { 1032 GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() + 1033 prompt_.extension()->id()); 1034 OpenURLParams params( 1035 store_url, Referrer(), NEW_FOREGROUND_TAB, 1036 content::PAGE_TRANSITION_LINK, 1037 false); 1038 navigator_->OpenURL(params); 1039 GetWidget()->Close(); 1040 } 1041} 1042 1043void ExtensionInstallDialogView::ToggleInlineExplanations() { 1044 for (InlineExplanations::iterator it = inline_explanations_.begin(); 1045 it != inline_explanations_.end(); ++it) 1046 (*it)->ToggleDetailLevel(); 1047} 1048 1049void ExtensionInstallDialogView::Layout() { 1050 scroll_view_->SetBounds(0, 0, width(), height()); 1051 1052 if (show_details_link_ || checkbox_info_label_) { 1053 views::LabelButton* cancel_button = GetDialogClientView()->cancel_button(); 1054 gfx::Rect parent_bounds = parent()->GetContentsBounds(); 1055 // By default, layouts have an inset of kButtonHEdgeMarginNew. In order to 1056 // align the link horizontally with the left side of the contents of the 1057 // layout, put a horizontal margin with this amount. 1058 const int horizontal_margin = views::kButtonHEdgeMarginNew; 1059 const int vertical_margin = views::kButtonVEdgeMarginNew; 1060 int y_buttons = parent_bounds.bottom() - 1061 cancel_button->GetPreferredSize().height() - vertical_margin; 1062 int max_width = dialog_size_.width() - cancel_button->width() * 2 - 1063 horizontal_margin * 2 - views::kRelatedButtonHSpacing; 1064 if (show_details_link_) { 1065 gfx::Size link_size = show_details_link_->GetPreferredSize(); 1066 show_details_link_->SetBounds( 1067 horizontal_margin, 1068 y_buttons + (cancel_button->height() - link_size.height()) / 2, 1069 link_size.width(), link_size.height()); 1070 } 1071 if (checkbox_info_label_) { 1072 gfx::Size label_size = checkbox_info_label_->GetPreferredSize(); 1073 checkbox_info_label_->SetBounds( 1074 horizontal_margin, 1075 y_buttons + (cancel_button->height() - label_size.height()) / 2, 1076 label_size.width(), label_size.height()); 1077 checkbox_info_label_->SizeToFit(max_width); 1078 } 1079 } 1080 // Disable accept button if there are unchecked boxes and 1081 // the experiment is on. 1082 if (prompt_.experiment()->show_checkboxes()) 1083 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0); 1084 1085 DialogDelegateView::Layout(); 1086} 1087 1088gfx::Size ExtensionInstallDialogView::GetPreferredSize() { 1089 return dialog_size_; 1090} 1091 1092void ExtensionInstallDialogView::ButtonPressed(views::Button* sender, 1093 const ui::Event& event) { 1094 if (std::string(views::Checkbox::kViewClassName) == sender->GetClassName()) { 1095 views::Checkbox* checkbox = static_cast<views::Checkbox*>(sender); 1096 if (checkbox->checked()) 1097 --unchecked_boxes_; 1098 else 1099 ++unchecked_boxes_; 1100 1101 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0); 1102 checkbox_info_label_->SetVisible(unchecked_boxes_ > 0); 1103 } 1104} 1105 1106void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted) 1107 const { 1108 if (prompt_.type() == ExtensionInstallPrompt::INSTALL_PROMPT) 1109 UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted); 1110} 1111 1112void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type) 1113 const { 1114 if (prompt_.experiment()->should_show_expandable_permission_list()) { 1115 // The clickable link in the UI is "Show Permissions". 1116 UMA_HISTOGRAM_ENUMERATION( 1117 "Extensions.InstallPromptExperiment.ShowPermissions", 1118 action_type, 1119 NUM_LINK_ACTIONS); 1120 } else { 1121 // The clickable link in the UI is "Show Details". 1122 UMA_HISTOGRAM_ENUMERATION( 1123 "Extensions.InstallPromptExperiment.ShowDetails", 1124 action_type, 1125 NUM_LINK_ACTIONS); 1126 } 1127} 1128 1129// static 1130ExtensionInstallPrompt::ShowDialogCallback 1131ExtensionInstallPrompt::GetDefaultShowDialogCallback() { 1132 return base::Bind(&ShowExtensionInstallDialogImpl); 1133} 1134 1135// ExpandableContainerView::DetailsView ---------------------------------------- 1136 1137ExpandableContainerView::DetailsView::DetailsView(int horizontal_space, 1138 bool parent_bulleted, 1139 bool lighter_color) 1140 : layout_(new views::GridLayout(this)), 1141 state_(0), 1142 lighter_color_(lighter_color) { 1143 SetLayoutManager(layout_); 1144 views::ColumnSet* column_set = layout_->AddColumnSet(0); 1145 // If the parent is using bullets for its items, then a padding of one unit 1146 // will make the child item (which has no bullet) look like a sibling of its 1147 // parent. Therefore increase the indentation by one more unit to show that it 1148 // is in fact a child item (with no missing bullet) and not a sibling. 1149 int padding = 1150 views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1); 1151 column_set->AddPaddingColumn(0, padding); 1152 column_set->AddColumn(views::GridLayout::LEADING, 1153 views::GridLayout::LEADING, 1154 0, 1155 views::GridLayout::FIXED, 1156 horizontal_space - padding, 1157 0); 1158} 1159 1160void ExpandableContainerView::DetailsView::AddDetail( 1161 const base::string16& detail) { 1162 layout_->StartRowWithPadding(0, 0, 1163 0, views::kRelatedControlSmallVerticalSpacing); 1164 views::Label* detail_label = 1165 new views::Label(PrepareForDisplay(detail, false)); 1166 detail_label->SetMultiLine(true); 1167 detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 1168 if (lighter_color_) { 1169 detail_label->SetEnabledColor(kLighterLabelColor); 1170 detail_label->SetAutoColorReadabilityEnabled(false); 1171 } 1172 layout_->AddView(detail_label); 1173} 1174 1175gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() { 1176 gfx::Size size = views::View::GetPreferredSize(); 1177 return gfx::Size(size.width(), size.height() * state_); 1178} 1179 1180void ExpandableContainerView::DetailsView::AnimateToState(double state) { 1181 state_ = state; 1182 PreferredSizeChanged(); 1183 SchedulePaint(); 1184} 1185 1186// ExpandableContainerView ----------------------------------------------------- 1187 1188ExpandableContainerView::ExpandableContainerView( 1189 ExtensionInstallDialogView* owner, 1190 const base::string16& description, 1191 const PermissionDetails& details, 1192 int horizontal_space, 1193 bool parent_bulleted, 1194 bool show_expand_link, 1195 bool lighter_color_details) 1196 : owner_(owner), 1197 details_view_(NULL), 1198 more_details_(NULL), 1199 slide_animation_(this), 1200 arrow_toggle_(NULL), 1201 expanded_(false) { 1202 views::GridLayout* layout = new views::GridLayout(this); 1203 SetLayoutManager(layout); 1204 int column_set_id = 0; 1205 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id); 1206 column_set->AddColumn(views::GridLayout::LEADING, 1207 views::GridLayout::LEADING, 1208 0, 1209 views::GridLayout::USE_PREF, 1210 0, 1211 0); 1212 if (!description.empty()) { 1213 layout->StartRow(0, column_set_id); 1214 1215 views::Label* description_label = new views::Label(description); 1216 description_label->SetMultiLine(true); 1217 description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 1218 description_label->SizeToFit(horizontal_space); 1219 layout->AddView(new BulletedView(description_label)); 1220 } 1221 1222 if (details.empty()) 1223 return; 1224 1225 details_view_ = new DetailsView(horizontal_space, parent_bulleted, 1226 lighter_color_details); 1227 1228 layout->StartRow(0, column_set_id); 1229 layout->AddView(details_view_); 1230 1231 for (size_t i = 0; i < details.size(); ++i) 1232 details_view_->AddDetail(details[i]); 1233 1234 // TODO(meacer): Remove show_expand_link when the experiment is completed. 1235 if (show_expand_link) { 1236 views::Link* link = new views::Link( 1237 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS)); 1238 1239 // Make sure the link width column is as wide as needed for both Show and 1240 // Hide details, so that the arrow doesn't shift horizontally when we 1241 // toggle. 1242 int link_col_width = 1243 views::kRelatedControlHorizontalSpacing + 1244 std::max(gfx::GetStringWidth( 1245 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS), 1246 link->font_list()), 1247 gfx::GetStringWidth( 1248 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS), 1249 link->font_list())); 1250 1251 column_set = layout->AddColumnSet(++column_set_id); 1252 // Padding to the left of the More Details column. If the parent is using 1253 // bullets for its items, then a padding of one unit will make the child 1254 // item (which has no bullet) look like a sibling of its parent. Therefore 1255 // increase the indentation by one more unit to show that it is in fact a 1256 // child item (with no missing bullet) and not a sibling. 1257 column_set->AddPaddingColumn( 1258 0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1)); 1259 // The More Details column. 1260 column_set->AddColumn(views::GridLayout::LEADING, 1261 views::GridLayout::LEADING, 1262 0, 1263 views::GridLayout::FIXED, 1264 link_col_width, 1265 link_col_width); 1266 // The Up/Down arrow column. 1267 column_set->AddColumn(views::GridLayout::LEADING, 1268 views::GridLayout::LEADING, 1269 0, 1270 views::GridLayout::USE_PREF, 1271 0, 1272 0); 1273 1274 // Add the More Details link. 1275 layout->StartRow(0, column_set_id); 1276 more_details_ = link; 1277 more_details_->set_listener(this); 1278 more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 1279 layout->AddView(more_details_); 1280 1281 // Add the arrow after the More Details link. 1282 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1283 arrow_toggle_ = new views::ImageButton(this); 1284 arrow_toggle_->SetImage(views::Button::STATE_NORMAL, 1285 rb.GetImageSkiaNamed(IDR_DOWN_ARROW)); 1286 layout->AddView(arrow_toggle_); 1287 } 1288} 1289 1290ExpandableContainerView::~ExpandableContainerView() { 1291} 1292 1293void ExpandableContainerView::ButtonPressed( 1294 views::Button* sender, const ui::Event& event) { 1295 ToggleDetailLevel(); 1296} 1297 1298void ExpandableContainerView::LinkClicked( 1299 views::Link* source, int event_flags) { 1300 ToggleDetailLevel(); 1301} 1302 1303void ExpandableContainerView::AnimationProgressed( 1304 const gfx::Animation* animation) { 1305 DCHECK_EQ(&slide_animation_, animation); 1306 if (details_view_) 1307 details_view_->AnimateToState(animation->GetCurrentValue()); 1308} 1309 1310void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) { 1311 if (arrow_toggle_) { 1312 if (animation->GetCurrentValue() != 0.0) { 1313 arrow_toggle_->SetImage( 1314 views::Button::STATE_NORMAL, 1315 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 1316 IDR_UP_ARROW)); 1317 } else { 1318 arrow_toggle_->SetImage( 1319 views::Button::STATE_NORMAL, 1320 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 1321 IDR_DOWN_ARROW)); 1322 } 1323 } 1324 if (more_details_) { 1325 more_details_->SetText(expanded_ ? 1326 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) : 1327 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS)); 1328 } 1329} 1330 1331void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) { 1332 owner_->ContentsChanged(); 1333} 1334 1335void ExpandableContainerView::ToggleDetailLevel() { 1336 expanded_ = !expanded_; 1337 1338 if (slide_animation_.IsShowing()) 1339 slide_animation_.Hide(); 1340 else 1341 slide_animation_.Show(); 1342} 1343 1344void ExpandableContainerView::ExpandWithoutAnimation() { 1345 expanded_ = true; 1346 details_view_->AnimateToState(1.0); 1347} 1348