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 "chrome/browser/ui/views/extensions/extension_install_dialog_view.h" 6 7 #include <vector> 8 9 #include "base/basictypes.h" 10 #include "base/command_line.h" 11 #include "base/compiler_specific.h" 12 #include "base/i18n/rtl.h" 13 #include "base/metrics/histogram.h" 14 #include "base/strings/string_util.h" 15 #include "base/strings/utf_string_conversions.h" 16 #include "chrome/browser/browser_process.h" 17 #include "chrome/browser/extensions/api/experience_sampling_private/experience_sampling.h" 18 #include "chrome/browser/extensions/bundle_installer.h" 19 #include "chrome/browser/extensions/extension_install_prompt_experiment.h" 20 #include "chrome/browser/profiles/profile.h" 21 #include "chrome/browser/profiles/profile_manager.h" 22 #include "chrome/browser/ui/views/constrained_window_views.h" 23 #include "chrome/common/extensions/extension_constants.h" 24 #include "chrome/grit/generated_resources.h" 25 #include "chrome/installer/util/browser_distribution.h" 26 #include "content/public/browser/page_navigator.h" 27 #include "content/public/browser/web_contents.h" 28 #include "extensions/common/extension.h" 29 #include "extensions/common/extension_urls.h" 30 #include "grit/theme_resources.h" 31 #include "ui/base/l10n/l10n_util.h" 32 #include "ui/base/resource/resource_bundle.h" 33 #include "ui/gfx/text_utils.h" 34 #include "ui/views/background.h" 35 #include "ui/views/border.h" 36 #include "ui/views/controls/button/checkbox.h" 37 #include "ui/views/controls/button/image_button.h" 38 #include "ui/views/controls/button/label_button.h" 39 #include "ui/views/controls/image_view.h" 40 #include "ui/views/controls/label.h" 41 #include "ui/views/controls/link.h" 42 #include "ui/views/controls/scroll_view.h" 43 #include "ui/views/controls/separator.h" 44 #include "ui/views/layout/box_layout.h" 45 #include "ui/views/layout/grid_layout.h" 46 #include "ui/views/layout/layout_constants.h" 47 #include "ui/views/widget/widget.h" 48 #include "ui/views/window/dialog_client_view.h" 49 50 using content::OpenURLParams; 51 using content::Referrer; 52 using extensions::BundleInstaller; 53 using extensions::ExperienceSamplingEvent; 54 55 namespace { 56 57 // Width of the bullet column in BulletedView. 58 const int kBulletWidth = 20; 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 void AddResourceIcon(const gfx::ImageSkia* skia_image, void* data) { 102 views::View* parent = static_cast<views::View*>(data); 103 views::ImageView* image_view = new views::ImageView(); 104 image_view->SetImage(*skia_image); 105 parent->AddChildView(image_view); 106 } 107 108 // Creates a string for displaying |message| to the user. If it has to look 109 // like a entry in a bullet point list, one is added. 110 base::string16 PrepareForDisplay(const base::string16& message, 111 bool bullet_point) { 112 return bullet_point ? l10n_util::GetStringFUTF16( 113 IDS_EXTENSION_PERMISSION_LINE, 114 message) : message; 115 } 116 117 } // namespace 118 119 BulletedView::BulletedView(views::View* view) { 120 views::GridLayout* layout = new views::GridLayout(this); 121 SetLayoutManager(layout); 122 views::ColumnSet* column_set = layout->AddColumnSet(0); 123 column_set->AddColumn(views::GridLayout::CENTER, 124 views::GridLayout::LEADING, 125 0, 126 views::GridLayout::FIXED, 127 kBulletWidth, 128 0); 129 column_set->AddColumn(views::GridLayout::LEADING, 130 views::GridLayout::LEADING, 131 0, 132 views::GridLayout::USE_PREF, 133 0, // No fixed width. 134 0); 135 layout->StartRow(0, 0); 136 layout->AddView(new views::Label(PrepareForDisplay(base::string16(), true))); 137 layout->AddView(view); 138 } 139 140 CheckboxedView::CheckboxedView(views::View* view, 141 views::ButtonListener* listener) { 142 views::GridLayout* layout = new views::GridLayout(this); 143 SetLayoutManager(layout); 144 views::ColumnSet* column_set = layout->AddColumnSet(0); 145 column_set->AddColumn(views::GridLayout::LEADING, 146 views::GridLayout::LEADING, 147 0, 148 views::GridLayout::USE_PREF, 149 0, // No fixed width. 150 0); 151 column_set->AddColumn(views::GridLayout::LEADING, 152 views::GridLayout::LEADING, 153 0, 154 views::GridLayout::USE_PREF, 155 0, // No fixed width. 156 0); 157 layout->StartRow(0, 0); 158 views::Checkbox* checkbox = new views::Checkbox(base::string16()); 159 checkbox->set_listener(listener); 160 // Alignment needs to be explicitly set again here, otherwise the views are 161 // not vertically centered. 162 layout->AddView(checkbox, 1, 1, 163 views::GridLayout::LEADING, views::GridLayout::CENTER); 164 layout->AddView(view, 1, 1, 165 views::GridLayout::LEADING, views::GridLayout::CENTER); 166 } 167 168 void ShowExtensionInstallDialogImpl( 169 const ExtensionInstallPrompt::ShowParams& show_params, 170 ExtensionInstallPrompt::Delegate* delegate, 171 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) { 172 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 173 CreateBrowserModalDialogViews( 174 new ExtensionInstallDialogView(show_params.navigator, delegate, prompt), 175 show_params.parent_window)->Show(); 176 } 177 178 CustomScrollableView::CustomScrollableView() {} 179 CustomScrollableView::~CustomScrollableView() {} 180 181 void CustomScrollableView::Layout() { 182 SetBounds(x(), y(), width(), GetHeightForWidth(width())); 183 views::View::Layout(); 184 } 185 186 ExtensionInstallDialogView::ExtensionInstallDialogView( 187 content::PageNavigator* navigator, 188 ExtensionInstallPrompt::Delegate* delegate, 189 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) 190 : navigator_(navigator), 191 delegate_(delegate), 192 prompt_(prompt), 193 scroll_view_(NULL), 194 scrollable_(NULL), 195 scrollable_header_only_(NULL), 196 show_details_link_(NULL), 197 checkbox_info_label_(NULL), 198 unchecked_boxes_(0), 199 handled_result_(false) { 200 InitView(); 201 } 202 203 ExtensionInstallDialogView::~ExtensionInstallDialogView() { 204 if (!handled_result_) 205 delegate_->InstallUIAbort(true); 206 } 207 208 void ExtensionInstallDialogView::InitView() { 209 // Possible grid layouts without ExtensionPermissionDialog experiment: 210 // Inline install 211 // w/ permissions no permissions 212 // +--------------------+------+ +--------------+------+ 213 // | heading | icon | | heading | icon | 214 // +--------------------| | +--------------| | 215 // | rating | | | rating | | 216 // +--------------------| | +--------------+ | 217 // | user_count | | | user_count | | 218 // +--------------------| | +--------------| | 219 // | store_link | | | store_link | | 220 // +--------------------+------+ +--------------+------+ 221 // | separator | 222 // +--------------------+------+ 223 // | permissions_header | | 224 // +--------------------+------+ 225 // | permission1 | | 226 // +--------------------+------+ 227 // | permission2 | | 228 // +--------------------+------+ 229 // 230 // Regular install 231 // w/ permissions no permissions 232 // +--------------------+------+ +--------------+------+ 233 // | heading | icon | | heading | icon | 234 // +--------------------| | +--------------+------+ 235 // | permissions_header | | 236 // +--------------------| | 237 // | permission1 | | 238 // +--------------------| | 239 // | permission2 | | 240 // +--------------------+------+ 241 // 242 // If the ExtensionPermissionDialog is on, the layout is modified depending 243 // on the experiment group. For text only experiment, a footer is added at the 244 // bottom of the layouts. For others, inline details are added below some of 245 // the permissions. 246 // 247 // Regular install w/ permissions and footer (experiment): 248 // +--------------------+------+ 249 // | heading | icon | 250 // +--------------------| | 251 // | permissions_header | | 252 // +--------------------| | 253 // | permission1 | | 254 // +--------------------| | 255 // | permission2 | | 256 // +--------------------+------+ 257 // | footer text | | 258 // +--------------------+------+ 259 // 260 // Regular install w/ permissions and inline explanations (experiment): 261 // +--------------------+------+ 262 // | heading | icon | 263 // +--------------------| | 264 // | permissions_header | | 265 // +--------------------| | 266 // | permission1 | | 267 // +--------------------| | 268 // | explanation1 | | 269 // +--------------------| | 270 // | permission2 | | 271 // +--------------------| | 272 // | explanation2 | | 273 // +--------------------+------+ 274 // 275 // Regular install w/ permissions and inline explanations (experiment): 276 // +--------------------+------+ 277 // | heading | icon | 278 // +--------------------| | 279 // | permissions_header | | 280 // +--------------------| | 281 // |checkbox|permission1| | 282 // +--------------------| | 283 // |checkbox|permission2| | 284 // +--------------------+------+ 285 // 286 // Additionally, links or informational text is added to non-client areas of 287 // the dialog depending on the experiment group. 288 289 int left_column_width = 290 (prompt_->ShouldShowPermissions() + prompt_->GetRetainedFileCount()) > 0 291 ? kPermissionsLeftColumnWidth 292 : kNoPermissionsLeftColumnWidth; 293 if (is_bundle_install()) 294 left_column_width = kBundleLeftColumnWidth; 295 if (is_external_install()) 296 left_column_width = kExternalInstallLeftColumnWidth; 297 298 scroll_view_ = new views::ScrollView(); 299 scroll_view_->set_hide_horizontal_scrollbar(true); 300 AddChildView(scroll_view_); 301 302 int column_set_id = 0; 303 // Create the full scrollable view which will contain all the information 304 // including the permissions. 305 scrollable_ = new CustomScrollableView(); 306 views::GridLayout* layout = CreateLayout( 307 scrollable_, left_column_width, column_set_id, false); 308 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 309 310 if (prompt_->ShouldShowPermissions() && 311 prompt_->experiment()->should_show_expandable_permission_list()) { 312 // If the experiment should hide the permission list initially, create a 313 // simple layout that contains only the header, extension name and icon. 314 scrollable_header_only_ = new CustomScrollableView(); 315 CreateLayout(scrollable_header_only_, left_column_width, 316 column_set_id, true); 317 scroll_view_->SetContents(scrollable_header_only_); 318 } else { 319 scroll_view_->SetContents(scrollable_); 320 } 321 322 int dialog_width = left_column_width + 2 * views::kPanelHorizMargin; 323 if (!is_bundle_install()) 324 dialog_width += views::kPanelHorizMargin + kIconSize + kIconOffset; 325 326 // Widen the dialog for experiment with checkboxes so that the information 327 // label fits the area to the left of the buttons. 328 if (prompt_->experiment()->show_checkboxes()) 329 dialog_width += 4 * views::kPanelHorizMargin; 330 331 if (prompt_->has_webstore_data()) { 332 layout->StartRow(0, column_set_id); 333 views::View* rating = new views::View(); 334 rating->SetLayoutManager(new views::BoxLayout( 335 views::BoxLayout::kHorizontal, 0, 0, 0)); 336 layout->AddView(rating); 337 prompt_->AppendRatingStars(AddResourceIcon, rating); 338 339 const gfx::FontList& small_font_list = 340 rb.GetFontList(ui::ResourceBundle::SmallFont); 341 views::Label* rating_count = 342 new views::Label(prompt_->GetRatingCount(), small_font_list); 343 // Add some space between the stars and the rating count. 344 rating_count->SetBorder(views::Border::CreateEmptyBorder(0, 2, 0, 0)); 345 rating->AddChildView(rating_count); 346 347 layout->StartRow(0, column_set_id); 348 views::Label* user_count = 349 new views::Label(prompt_->GetUserCount(), small_font_list); 350 user_count->SetAutoColorReadabilityEnabled(false); 351 user_count->SetEnabledColor(SK_ColorGRAY); 352 layout->AddView(user_count); 353 354 layout->StartRow(0, column_set_id); 355 views::Link* store_link = new views::Link( 356 l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_STORE_LINK)); 357 store_link->SetFontList(small_font_list); 358 store_link->set_listener(this); 359 layout->AddView(store_link); 360 } 361 362 if (is_bundle_install()) { 363 BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState( 364 BundleInstaller::Item::STATE_PENDING); 365 for (size_t i = 0; i < items.size(); ++i) { 366 base::string16 extension_name = 367 base::UTF8ToUTF16(items[i].localized_name); 368 base::i18n::AdjustStringForLocaleDirection(&extension_name); 369 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 370 layout->StartRow(0, column_set_id); 371 views::Label* extension_label = new views::Label( 372 PrepareForDisplay(extension_name, true)); 373 extension_label->SetMultiLine(true); 374 extension_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 375 extension_label->SizeToFit(left_column_width); 376 layout->AddView(extension_label); 377 } 378 } 379 380 bool has_permissions = 381 prompt_->GetPermissionCount( 382 ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS) > 0; 383 if (prompt_->ShouldShowPermissions()) { 384 AddPermissions( 385 layout, 386 rb, 387 column_set_id, 388 left_column_width, 389 ExtensionInstallPrompt::PermissionsType::REGULAR_PERMISSIONS); 390 AddPermissions( 391 layout, 392 rb, 393 column_set_id, 394 left_column_width, 395 ExtensionInstallPrompt::PermissionsType::WITHHELD_PERMISSIONS); 396 if (!has_permissions) { 397 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 398 layout->StartRow(0, column_set_id); 399 views::Label* permission_label = new views::Label( 400 l10n_util::GetStringUTF16(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)); 401 permission_label->SetMultiLine(true); 402 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 403 permission_label->SizeToFit(left_column_width); 404 layout->AddView(permission_label); 405 } 406 } 407 408 if (prompt_->GetRetainedFileCount()) { 409 // Slide in under the permissions, if there are any. If there are 410 // either, the retained files prompt stretches all the way to the 411 // right of the dialog. If there are no permissions, the retained 412 // files prompt just takes up the left column. 413 int space_for_files = left_column_width; 414 if (has_permissions) { 415 space_for_files += kIconSize; 416 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id); 417 column_set->AddColumn(views::GridLayout::FILL, 418 views::GridLayout::FILL, 419 1, 420 views::GridLayout::USE_PREF, 421 0, // no fixed width 422 space_for_files); 423 } 424 425 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 426 427 layout->StartRow(0, column_set_id); 428 views::Label* retained_files_header = NULL; 429 retained_files_header = 430 new views::Label(prompt_->GetRetainedFilesHeading()); 431 retained_files_header->SetMultiLine(true); 432 retained_files_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); 433 retained_files_header->SizeToFit(space_for_files); 434 layout->AddView(retained_files_header); 435 436 layout->StartRow(0, column_set_id); 437 PermissionDetails details; 438 for (size_t i = 0; i < prompt_->GetRetainedFileCount(); ++i) 439 details.push_back(prompt_->GetRetainedFile(i)); 440 ExpandableContainerView* issue_advice_view = 441 new ExpandableContainerView( 442 this, base::string16(), details, space_for_files, 443 false, true, false); 444 layout->AddView(issue_advice_view); 445 } 446 447 DCHECK(prompt_->type() >= 0); 448 UMA_HISTOGRAM_ENUMERATION("Extensions.InstallPrompt.Type", 449 prompt_->type(), 450 ExtensionInstallPrompt::NUM_PROMPT_TYPES); 451 452 if (prompt_->ShouldShowPermissions()) { 453 if (prompt_->ShouldShowExplanationText()) { 454 views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id); 455 column_set->AddColumn(views::GridLayout::LEADING, 456 views::GridLayout::FILL, 457 1, 458 views::GridLayout::USE_PREF, 459 0, 460 0); 461 // Add two rows of space so that the text stands out. 462 layout->AddPaddingRow(0, 2 * views::kRelatedControlVerticalSpacing); 463 464 layout->StartRow(0, column_set_id); 465 views::Label* explanation = 466 new views::Label(prompt_->experiment()->GetExplanationText()); 467 explanation->SetMultiLine(true); 468 explanation->SetHorizontalAlignment(gfx::ALIGN_LEFT); 469 explanation->SizeToFit(left_column_width + kIconSize); 470 layout->AddView(explanation); 471 } 472 473 if (prompt_->experiment()->should_show_expandable_permission_list() || 474 (prompt_->experiment()->show_details_link() && 475 prompt_->experiment()->should_show_inline_explanations() && 476 !inline_explanations_.empty())) { 477 // Don't show the "Show details" link if there are retained 478 // files. These have their own "Show details" links and having 479 // multiple levels of links is confusing. 480 if (prompt_->GetRetainedFileCount() == 0) { 481 int text_id = 482 prompt_->experiment()->should_show_expandable_permission_list() 483 ? IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS 484 : IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_DETAILS; 485 show_details_link_ = new views::Link( 486 l10n_util::GetStringUTF16(text_id)); 487 show_details_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 488 show_details_link_->set_listener(this); 489 UpdateLinkActionHistogram(LINK_SHOWN); 490 } else { 491 UpdateLinkActionHistogram(LINK_NOT_SHOWN); 492 } 493 } 494 495 if (prompt_->experiment()->show_checkboxes()) { 496 checkbox_info_label_ = new views::Label( 497 l10n_util::GetStringUTF16( 498 IDS_EXTENSION_PROMPT_EXPERIMENT_CHECKBOX_INFO)); 499 checkbox_info_label_->SetMultiLine(true); 500 checkbox_info_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 501 checkbox_info_label_->SetAutoColorReadabilityEnabled(false); 502 checkbox_info_label_->SetEnabledColor(kLighterLabelColor); 503 } 504 } 505 506 gfx::Size scrollable_size = scrollable_->GetPreferredSize(); 507 scrollable_->SetBoundsRect(gfx::Rect(scrollable_size)); 508 dialog_size_ = gfx::Size( 509 dialog_width, 510 std::min(scrollable_size.height(), kDialogMaxHeight)); 511 512 if (scrollable_header_only_) { 513 gfx::Size header_only_size = scrollable_header_only_->GetPreferredSize(); 514 scrollable_header_only_->SetBoundsRect(gfx::Rect(header_only_size)); 515 dialog_size_ = gfx::Size( 516 dialog_width, std::min(header_only_size.height(), kDialogMaxHeight)); 517 } 518 519 std::string event_name = ExperienceSamplingEvent::kExtensionInstallDialog; 520 event_name.append( 521 ExtensionInstallPrompt::PromptTypeToString(prompt_->type())); 522 sampling_event_ = ExperienceSamplingEvent::Create(event_name); 523 } 524 525 bool ExtensionInstallDialogView::AddPermissions( 526 views::GridLayout* layout, 527 ui::ResourceBundle& rb, 528 int column_set_id, 529 int left_column_width, 530 ExtensionInstallPrompt::PermissionsType perm_type) { 531 if (prompt_->GetPermissionCount(perm_type) == 0) 532 return false; 533 534 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 535 if (is_inline_install()) { 536 layout->StartRow(0, column_set_id); 537 layout->AddView(new views::Separator(views::Separator::HORIZONTAL), 538 3, 539 1, 540 views::GridLayout::FILL, 541 views::GridLayout::FILL); 542 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 543 } 544 545 layout->StartRow(0, column_set_id); 546 views::Label* permissions_header = NULL; 547 if (is_bundle_install()) { 548 // We need to pass the FontList in the constructor, rather than calling 549 // SetFontList later, because otherwise SizeToFit mis-judges the width 550 // of the line. 551 permissions_header = 552 new views::Label(prompt_->GetPermissionsHeading(perm_type), 553 rb.GetFontList(ui::ResourceBundle::MediumFont)); 554 } else { 555 permissions_header = 556 new views::Label(prompt_->GetPermissionsHeading(perm_type)); 557 } 558 permissions_header->SetMultiLine(true); 559 permissions_header->SetHorizontalAlignment(gfx::ALIGN_LEFT); 560 permissions_header->SizeToFit(left_column_width); 561 layout->AddView(permissions_header); 562 563 for (size_t i = 0; i < prompt_->GetPermissionCount(perm_type); ++i) { 564 layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); 565 layout->StartRow(0, column_set_id); 566 views::Label* permission_label = 567 new views::Label(prompt_->GetPermission(i, perm_type)); 568 569 const SkColor kTextHighlight = SK_ColorRED; 570 const SkColor kBackgroundHighlight = SkColorSetRGB(0xFB, 0xF7, 0xA3); 571 if (prompt_->experiment()->ShouldHighlightText( 572 prompt_->GetPermission(i, perm_type))) { 573 permission_label->SetAutoColorReadabilityEnabled(false); 574 permission_label->SetEnabledColor(kTextHighlight); 575 } else if (prompt_->experiment()->ShouldHighlightBackground( 576 prompt_->GetPermission(i, perm_type))) { 577 permission_label->SetLineHeight(18); 578 permission_label->set_background( 579 views::Background::CreateSolidBackground(kBackgroundHighlight)); 580 } 581 582 permission_label->SetMultiLine(true); 583 permission_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 584 585 if (prompt_->experiment()->show_checkboxes()) { 586 permission_label->SizeToFit(left_column_width); 587 layout->AddView(new CheckboxedView(permission_label, this)); 588 ++unchecked_boxes_; 589 } else { 590 permission_label->SizeToFit(left_column_width - kBulletWidth); 591 layout->AddView(new BulletedView(permission_label)); 592 } 593 594 // If we have more details to provide, show them in collapsed form. 595 if (!prompt_->GetPermissionsDetails(i, perm_type).empty()) { 596 layout->StartRow(0, column_set_id); 597 PermissionDetails details; 598 details.push_back(PrepareForDisplay( 599 prompt_->GetPermissionsDetails(i, perm_type), false)); 600 ExpandableContainerView* details_container = 601 new ExpandableContainerView(this, 602 base::string16(), 603 details, 604 left_column_width, 605 true, 606 true, 607 false); 608 layout->AddView(details_container); 609 } 610 611 if (prompt_->experiment()->should_show_inline_explanations()) { 612 base::string16 explanation = prompt_->experiment()->GetInlineExplanation( 613 prompt_->GetPermission(i, perm_type)); 614 if (!explanation.empty()) { 615 PermissionDetails details; 616 details.push_back(explanation); 617 ExpandableContainerView* container = 618 new ExpandableContainerView(this, 619 base::string16(), 620 details, 621 left_column_width, 622 false, 623 false, 624 true); 625 // Inline explanations are expanded by default if there is 626 // no "Show details" link. 627 if (!prompt_->experiment()->show_details_link()) 628 container->ExpandWithoutAnimation(); 629 layout->StartRow(0, column_set_id); 630 layout->AddView(container); 631 inline_explanations_.push_back(container); 632 } 633 } 634 } 635 return true; 636 } 637 638 views::GridLayout* ExtensionInstallDialogView::CreateLayout( 639 views::View* parent, 640 int left_column_width, 641 int column_set_id, 642 bool single_detail_row) const { 643 views::GridLayout* layout = views::GridLayout::CreatePanel(parent); 644 parent->SetLayoutManager(layout); 645 646 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id); 647 column_set->AddColumn(views::GridLayout::LEADING, 648 views::GridLayout::FILL, 649 0, // no resizing 650 views::GridLayout::USE_PREF, 651 0, // no fixed width 652 left_column_width); 653 if (!is_bundle_install()) { 654 column_set->AddPaddingColumn(0, views::kPanelHorizMargin); 655 column_set->AddColumn(views::GridLayout::TRAILING, 656 views::GridLayout::LEADING, 657 0, // no resizing 658 views::GridLayout::USE_PREF, 659 0, // no fixed width 660 kIconSize); 661 } 662 663 layout->StartRow(0, column_set_id); 664 665 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 666 667 views::Label* heading = new views::Label( 668 prompt_->GetHeading(), rb.GetFontList(ui::ResourceBundle::MediumFont)); 669 heading->SetMultiLine(true); 670 heading->SetHorizontalAlignment(gfx::ALIGN_LEFT); 671 heading->SizeToFit(left_column_width); 672 layout->AddView(heading); 673 674 if (!is_bundle_install()) { 675 // Scale down to icon size, but allow smaller icons (don't scale up). 676 const gfx::ImageSkia* image = prompt_->icon().ToImageSkia(); 677 gfx::Size size(image->width(), image->height()); 678 if (size.width() > kIconSize || size.height() > kIconSize) 679 size = gfx::Size(kIconSize, kIconSize); 680 views::ImageView* icon = new views::ImageView(); 681 icon->SetImageSize(size); 682 icon->SetImage(*image); 683 icon->SetHorizontalAlignment(views::ImageView::CENTER); 684 icon->SetVerticalAlignment(views::ImageView::CENTER); 685 if (single_detail_row) { 686 layout->AddView(icon); 687 } else { 688 int icon_row_span = 1; 689 if (is_inline_install()) { 690 // Also span the rating, user_count and store_link rows. 691 icon_row_span = 4; 692 } else if (prompt_->ShouldShowPermissions()) { 693 size_t permission_count = prompt_->GetPermissionCount( 694 ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS); 695 // Also span the permission header and each of the permission rows (all 696 // have a padding row above it). This also works for the 'no special 697 // permissions' case. 698 icon_row_span = 3 + permission_count * 2; 699 } else if (prompt_->GetRetainedFileCount()) { 700 // Also span the permission header and the retained files container. 701 icon_row_span = 4; 702 } 703 layout->AddView(icon, 1, icon_row_span); 704 } 705 } 706 return layout; 707 } 708 709 void ExtensionInstallDialogView::ContentsChanged() { 710 Layout(); 711 } 712 713 void ExtensionInstallDialogView::ViewHierarchyChanged( 714 const ViewHierarchyChangedDetails& details) { 715 // Since we want the links to show up in the same visual row as the accept 716 // and cancel buttons, which is provided by the framework, we must add the 717 // buttons to the non-client view, which is the parent of this view. 718 // Similarly, when we're removed from the view hierarchy, we must take care 719 // to clean up those items as well. 720 if (details.child == this) { 721 if (details.is_add) { 722 if (show_details_link_) 723 details.parent->AddChildView(show_details_link_); 724 if (checkbox_info_label_) 725 details.parent->AddChildView(checkbox_info_label_); 726 } else { 727 if (show_details_link_) 728 details.parent->RemoveChildView(show_details_link_); 729 if (checkbox_info_label_) 730 details.parent->RemoveChildView(checkbox_info_label_); 731 } 732 } 733 } 734 735 int ExtensionInstallDialogView::GetDialogButtons() const { 736 int buttons = prompt_->GetDialogButtons(); 737 // Simply having just an OK button is *not* supported. See comment on function 738 // GetDialogButtons in dialog_delegate.h for reasons. 739 DCHECK_GT(buttons & ui::DIALOG_BUTTON_CANCEL, 0); 740 return buttons; 741 } 742 743 base::string16 ExtensionInstallDialogView::GetDialogButtonLabel( 744 ui::DialogButton button) const { 745 switch (button) { 746 case ui::DIALOG_BUTTON_OK: 747 return prompt_->GetAcceptButtonLabel(); 748 case ui::DIALOG_BUTTON_CANCEL: 749 return prompt_->HasAbortButtonLabel() 750 ? prompt_->GetAbortButtonLabel() 751 : l10n_util::GetStringUTF16(IDS_CANCEL); 752 default: 753 NOTREACHED(); 754 return base::string16(); 755 } 756 } 757 758 int ExtensionInstallDialogView::GetDefaultDialogButton() const { 759 return ui::DIALOG_BUTTON_CANCEL; 760 } 761 762 bool ExtensionInstallDialogView::Cancel() { 763 if (handled_result_) 764 return true; 765 766 handled_result_ = true; 767 UpdateInstallResultHistogram(false); 768 if (sampling_event_) 769 sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kDeny); 770 delegate_->InstallUIAbort(true); 771 return true; 772 } 773 774 bool ExtensionInstallDialogView::Accept() { 775 DCHECK(!handled_result_); 776 777 handled_result_ = true; 778 UpdateInstallResultHistogram(true); 779 if (sampling_event_) 780 sampling_event_->CreateUserDecisionEvent(ExperienceSamplingEvent::kProceed); 781 delegate_->InstallUIProceed(); 782 return true; 783 } 784 785 ui::ModalType ExtensionInstallDialogView::GetModalType() const { 786 return ui::MODAL_TYPE_WINDOW; 787 } 788 789 base::string16 ExtensionInstallDialogView::GetWindowTitle() const { 790 return prompt_->GetDialogTitle(); 791 } 792 793 void ExtensionInstallDialogView::LinkClicked(views::Link* source, 794 int event_flags) { 795 if (source == show_details_link_) { 796 UpdateLinkActionHistogram(LINK_CLICKED); 797 // Show details link is used to either reveal whole permission list or to 798 // reveal inline explanations. 799 if (prompt_->experiment()->should_show_expandable_permission_list()) { 800 gfx::Rect bounds = GetWidget()->GetWindowBoundsInScreen(); 801 int spacing = bounds.height() - 802 scrollable_header_only_->GetPreferredSize().height(); 803 int content_height = std::min(scrollable_->GetPreferredSize().height(), 804 kDialogMaxHeight); 805 bounds.set_height(spacing + content_height); 806 scroll_view_->SetContents(scrollable_); 807 GetWidget()->SetBoundsConstrained(bounds); 808 ContentsChanged(); 809 } else { 810 ToggleInlineExplanations(); 811 } 812 show_details_link_->SetVisible(false); 813 } else { 814 GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() + 815 prompt_->extension()->id()); 816 OpenURLParams params( 817 store_url, Referrer(), NEW_FOREGROUND_TAB, 818 ui::PAGE_TRANSITION_LINK, 819 false); 820 navigator_->OpenURL(params); 821 GetWidget()->Close(); 822 } 823 } 824 825 void ExtensionInstallDialogView::ToggleInlineExplanations() { 826 for (InlineExplanations::iterator it = inline_explanations_.begin(); 827 it != inline_explanations_.end(); ++it) 828 (*it)->ToggleDetailLevel(); 829 } 830 831 void ExtensionInstallDialogView::Layout() { 832 scroll_view_->SetBounds(0, 0, width(), height()); 833 834 if (show_details_link_ || checkbox_info_label_) { 835 views::LabelButton* cancel_button = GetDialogClientView()->cancel_button(); 836 gfx::Rect parent_bounds = parent()->GetContentsBounds(); 837 // By default, layouts have an inset of kButtonHEdgeMarginNew. In order to 838 // align the link horizontally with the left side of the contents of the 839 // layout, put a horizontal margin with this amount. 840 const int horizontal_margin = views::kButtonHEdgeMarginNew; 841 const int vertical_margin = views::kButtonVEdgeMarginNew; 842 int y_buttons = parent_bounds.bottom() - 843 cancel_button->GetPreferredSize().height() - vertical_margin; 844 int max_width = dialog_size_.width() - cancel_button->width() * 2 - 845 horizontal_margin * 2 - views::kRelatedButtonHSpacing; 846 if (show_details_link_) { 847 gfx::Size link_size = show_details_link_->GetPreferredSize(); 848 show_details_link_->SetBounds( 849 horizontal_margin, 850 y_buttons + (cancel_button->height() - link_size.height()) / 2, 851 link_size.width(), link_size.height()); 852 } 853 if (checkbox_info_label_) { 854 gfx::Size label_size = checkbox_info_label_->GetPreferredSize(); 855 checkbox_info_label_->SetBounds( 856 horizontal_margin, 857 y_buttons + (cancel_button->height() - label_size.height()) / 2, 858 label_size.width(), label_size.height()); 859 checkbox_info_label_->SizeToFit(max_width); 860 } 861 } 862 // Disable accept button if there are unchecked boxes and 863 // the experiment is on. 864 if (prompt_->experiment()->show_checkboxes()) 865 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0); 866 867 DialogDelegateView::Layout(); 868 } 869 870 gfx::Size ExtensionInstallDialogView::GetPreferredSize() const { 871 return dialog_size_; 872 } 873 874 void ExtensionInstallDialogView::ButtonPressed(views::Button* sender, 875 const ui::Event& event) { 876 if (std::string(views::Checkbox::kViewClassName) == sender->GetClassName()) { 877 views::Checkbox* checkbox = static_cast<views::Checkbox*>(sender); 878 if (checkbox->checked()) 879 --unchecked_boxes_; 880 else 881 ++unchecked_boxes_; 882 883 GetDialogClientView()->ok_button()->SetEnabled(unchecked_boxes_ == 0); 884 checkbox_info_label_->SetVisible(unchecked_boxes_ > 0); 885 } 886 } 887 888 void ExtensionInstallDialogView::UpdateInstallResultHistogram(bool accepted) 889 const { 890 if (prompt_->type() == ExtensionInstallPrompt::INSTALL_PROMPT) 891 UMA_HISTOGRAM_BOOLEAN("Extensions.InstallPrompt.Accepted", accepted); 892 } 893 894 void ExtensionInstallDialogView::UpdateLinkActionHistogram(int action_type) 895 const { 896 if (prompt_->experiment()->should_show_expandable_permission_list()) { 897 // The clickable link in the UI is "Show Permissions". 898 UMA_HISTOGRAM_ENUMERATION( 899 "Extensions.InstallPromptExperiment.ShowPermissions", 900 action_type, 901 NUM_LINK_ACTIONS); 902 } else { 903 // The clickable link in the UI is "Show Details". 904 UMA_HISTOGRAM_ENUMERATION( 905 "Extensions.InstallPromptExperiment.ShowDetails", 906 action_type, 907 NUM_LINK_ACTIONS); 908 } 909 } 910 911 // ExpandableContainerView::DetailsView ---------------------------------------- 912 913 ExpandableContainerView::DetailsView::DetailsView(int horizontal_space, 914 bool parent_bulleted, 915 bool lighter_color) 916 : layout_(new views::GridLayout(this)), 917 state_(0), 918 lighter_color_(lighter_color) { 919 SetLayoutManager(layout_); 920 views::ColumnSet* column_set = layout_->AddColumnSet(0); 921 // If the parent is using bullets for its items, then a padding of one unit 922 // will make the child item (which has no bullet) look like a sibling of its 923 // parent. Therefore increase the indentation by one more unit to show that it 924 // is in fact a child item (with no missing bullet) and not a sibling. 925 int padding = 926 views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1); 927 column_set->AddPaddingColumn(0, padding); 928 column_set->AddColumn(views::GridLayout::LEADING, 929 views::GridLayout::LEADING, 930 0, 931 views::GridLayout::FIXED, 932 horizontal_space - padding, 933 0); 934 } 935 936 void ExpandableContainerView::DetailsView::AddDetail( 937 const base::string16& detail) { 938 layout_->StartRowWithPadding(0, 0, 939 0, views::kRelatedControlSmallVerticalSpacing); 940 views::Label* detail_label = 941 new views::Label(PrepareForDisplay(detail, false)); 942 detail_label->SetMultiLine(true); 943 detail_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 944 if (lighter_color_) { 945 detail_label->SetEnabledColor(kLighterLabelColor); 946 detail_label->SetAutoColorReadabilityEnabled(false); 947 } 948 layout_->AddView(detail_label); 949 } 950 951 gfx::Size ExpandableContainerView::DetailsView::GetPreferredSize() const { 952 gfx::Size size = views::View::GetPreferredSize(); 953 return gfx::Size(size.width(), size.height() * state_); 954 } 955 956 void ExpandableContainerView::DetailsView::AnimateToState(double state) { 957 state_ = state; 958 PreferredSizeChanged(); 959 SchedulePaint(); 960 } 961 962 // ExpandableContainerView ----------------------------------------------------- 963 964 ExpandableContainerView::ExpandableContainerView( 965 ExtensionInstallDialogView* owner, 966 const base::string16& description, 967 const PermissionDetails& details, 968 int horizontal_space, 969 bool parent_bulleted, 970 bool show_expand_link, 971 bool lighter_color_details) 972 : owner_(owner), 973 details_view_(NULL), 974 more_details_(NULL), 975 slide_animation_(this), 976 arrow_toggle_(NULL), 977 expanded_(false) { 978 views::GridLayout* layout = new views::GridLayout(this); 979 SetLayoutManager(layout); 980 int column_set_id = 0; 981 views::ColumnSet* column_set = layout->AddColumnSet(column_set_id); 982 column_set->AddColumn(views::GridLayout::LEADING, 983 views::GridLayout::LEADING, 984 0, 985 views::GridLayout::USE_PREF, 986 0, 987 0); 988 if (!description.empty()) { 989 layout->StartRow(0, column_set_id); 990 991 views::Label* description_label = new views::Label(description); 992 description_label->SetMultiLine(true); 993 description_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 994 description_label->SizeToFit(horizontal_space); 995 layout->AddView(new BulletedView(description_label)); 996 } 997 998 if (details.empty()) 999 return; 1000 1001 details_view_ = new DetailsView(horizontal_space, parent_bulleted, 1002 lighter_color_details); 1003 1004 layout->StartRow(0, column_set_id); 1005 layout->AddView(details_view_); 1006 1007 for (size_t i = 0; i < details.size(); ++i) 1008 details_view_->AddDetail(details[i]); 1009 1010 // TODO(meacer): Remove show_expand_link when the experiment is completed. 1011 if (show_expand_link) { 1012 views::Link* link = new views::Link( 1013 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS)); 1014 1015 // Make sure the link width column is as wide as needed for both Show and 1016 // Hide details, so that the arrow doesn't shift horizontally when we 1017 // toggle. 1018 int link_col_width = 1019 views::kRelatedControlHorizontalSpacing + 1020 std::max(gfx::GetStringWidth( 1021 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS), 1022 link->font_list()), 1023 gfx::GetStringWidth( 1024 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS), 1025 link->font_list())); 1026 1027 column_set = layout->AddColumnSet(++column_set_id); 1028 // Padding to the left of the More Details column. If the parent is using 1029 // bullets for its items, then a padding of one unit will make the child 1030 // item (which has no bullet) look like a sibling of its parent. Therefore 1031 // increase the indentation by one more unit to show that it is in fact a 1032 // child item (with no missing bullet) and not a sibling. 1033 column_set->AddPaddingColumn( 1034 0, views::kRelatedControlHorizontalSpacing * (parent_bulleted ? 2 : 1)); 1035 // The More Details column. 1036 column_set->AddColumn(views::GridLayout::LEADING, 1037 views::GridLayout::LEADING, 1038 0, 1039 views::GridLayout::FIXED, 1040 link_col_width, 1041 link_col_width); 1042 // The Up/Down arrow column. 1043 column_set->AddColumn(views::GridLayout::LEADING, 1044 views::GridLayout::LEADING, 1045 0, 1046 views::GridLayout::USE_PREF, 1047 0, 1048 0); 1049 1050 // Add the More Details link. 1051 layout->StartRow(0, column_set_id); 1052 more_details_ = link; 1053 more_details_->set_listener(this); 1054 more_details_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 1055 layout->AddView(more_details_); 1056 1057 // Add the arrow after the More Details link. 1058 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 1059 arrow_toggle_ = new views::ImageButton(this); 1060 arrow_toggle_->SetImage(views::Button::STATE_NORMAL, 1061 rb.GetImageSkiaNamed(IDR_DOWN_ARROW)); 1062 layout->AddView(arrow_toggle_); 1063 } 1064 } 1065 1066 ExpandableContainerView::~ExpandableContainerView() { 1067 } 1068 1069 void ExpandableContainerView::ButtonPressed( 1070 views::Button* sender, const ui::Event& event) { 1071 ToggleDetailLevel(); 1072 } 1073 1074 void ExpandableContainerView::LinkClicked( 1075 views::Link* source, int event_flags) { 1076 ToggleDetailLevel(); 1077 } 1078 1079 void ExpandableContainerView::AnimationProgressed( 1080 const gfx::Animation* animation) { 1081 DCHECK_EQ(&slide_animation_, animation); 1082 if (details_view_) 1083 details_view_->AnimateToState(animation->GetCurrentValue()); 1084 } 1085 1086 void ExpandableContainerView::AnimationEnded(const gfx::Animation* animation) { 1087 if (arrow_toggle_) { 1088 if (animation->GetCurrentValue() != 0.0) { 1089 arrow_toggle_->SetImage( 1090 views::Button::STATE_NORMAL, 1091 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 1092 IDR_UP_ARROW)); 1093 } else { 1094 arrow_toggle_->SetImage( 1095 views::Button::STATE_NORMAL, 1096 ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 1097 IDR_DOWN_ARROW)); 1098 } 1099 } 1100 if (more_details_) { 1101 more_details_->SetText(expanded_ ? 1102 l10n_util::GetStringUTF16(IDS_EXTENSIONS_HIDE_DETAILS) : 1103 l10n_util::GetStringUTF16(IDS_EXTENSIONS_SHOW_DETAILS)); 1104 } 1105 } 1106 1107 void ExpandableContainerView::ChildPreferredSizeChanged(views::View* child) { 1108 owner_->ContentsChanged(); 1109 } 1110 1111 void ExpandableContainerView::ToggleDetailLevel() { 1112 expanded_ = !expanded_; 1113 1114 if (slide_animation_.IsShowing()) 1115 slide_animation_.Hide(); 1116 else 1117 slide_animation_.Show(); 1118 } 1119 1120 void ExpandableContainerView::ExpandWithoutAnimation() { 1121 expanded_ = true; 1122 details_view_->AnimateToState(1.0); 1123 } 1124 1125 // static 1126 ExtensionInstallPrompt::ShowDialogCallback 1127 ExtensionInstallPrompt::GetDefaultShowDialogCallback() { 1128 return base::Bind(&ShowExtensionInstallDialogImpl); 1129 } 1130