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