extension_install_view_controller.mm revision 424c4d7b64af9d0d8fd9624f381f469654d5e3d2
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#import "chrome/browser/ui/cocoa/extensions/extension_install_view_controller.h" 6 7#include "base/auto_reset.h" 8#include "base/i18n/rtl.h" 9#include "base/mac/bundle_locations.h" 10#include "base/mac/mac_util.h" 11#include "base/strings/string_util.h" 12#include "base/strings/sys_string_conversions.h" 13#include "base/strings/utf_string_conversions.h" 14#include "chrome/browser/extensions/bundle_installer.h" 15#import "chrome/browser/ui/chrome_style.h" 16#include "chrome/common/extensions/extension.h" 17#include "content/public/browser/page_navigator.h" 18#include "grit/generated_resources.h" 19#include "skia/ext/skia_utils_mac.h" 20#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 21#import "ui/base/cocoa/controls/hyperlink_button_cell.h" 22#include "ui/base/l10n/l10n_util.h" 23#include "ui/base/l10n/l10n_util_mac.h" 24#include "ui/gfx/image/image_skia_util_mac.h" 25 26using content::OpenURLParams; 27using content::Referrer; 28using extensions::BundleInstaller; 29 30namespace { 31 32// A collection of attributes (bitmask) for how to draw a cell, the expand 33// marker and the text in the cell. 34enum CellAttributesMask { 35 kBoldText = 1 << 0, 36 kNoExpandMarker = 1 << 1, 37 kUseBullet = 1 << 2, 38 kAutoExpandCell = 1 << 3, 39 kUseCustomLinkCell = 1 << 4, 40 kCanExpand = 1 << 5, 41}; 42 43typedef NSUInteger CellAttributes; 44 45} // namespace. 46 47@interface ExtensionInstallViewController () 48- (BOOL)isBundleInstall; 49- (BOOL)isInlineInstall; 50- (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage; 51- (void)onOutlineViewRowCountDidChange; 52- (NSDictionary*)buildItemWithTitle:(NSString*)title 53 cellAttributes:(CellAttributes)cellAttributes 54 children:(NSArray*)children; 55- (NSDictionary*)buildDetailToggleItem:(size_t)type 56 permissionsDetailIndex:(size_t)index; 57- (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt; 58- (void)updateViewFrame:(NSRect)frame; 59@end 60 61@interface DetailToggleHyperlinkButtonCell : HyperlinkButtonCell { 62 NSUInteger permissionsDetailIndex_; 63 ExtensionInstallPrompt::DetailsType permissionsDetailType_; 64 SEL linkClickedAction_; 65} 66 67@property(assign, nonatomic) NSUInteger permissionsDetailIndex; 68@property(assign, nonatomic) 69 ExtensionInstallPrompt::DetailsType permissionsDetailType; 70@property(assign, nonatomic) SEL linkClickedAction; 71 72@end 73 74namespace { 75 76// Padding above the warnings separator, we must also subtract this when hiding 77// it. 78const CGFloat kWarningsSeparatorPadding = 14; 79 80// The left padding for the link cell. 81const CGFloat kLinkCellPaddingLeft = 3; 82 83// Maximum height we will adjust controls to when trying to accomodate their 84// contents. 85const CGFloat kMaxControlHeight = 250; 86 87NSString* const kTitleKey = @"title"; 88NSString* const kChildrenKey = @"children"; 89NSString* const kCellAttributesKey = @"cellAttributes"; 90NSString* const kPermissionsDetailIndex = @"permissionsDetailIndex"; 91NSString* const kPermissionsDetailType = @"permissionsDetailType"; 92 93// Adjust the |control|'s height so that its content is not clipped. 94// This also adds the change in height to the |totalOffset| and shifts the 95// control down by that amount. 96void OffsetControlVerticallyToFitContent(NSControl* control, 97 CGFloat* totalOffset) { 98 // Adjust the control's height so that its content is not clipped. 99 NSRect currentRect = [control frame]; 100 NSRect fitRect = currentRect; 101 fitRect.size.height = kMaxControlHeight; 102 CGFloat desiredHeight = [[control cell] cellSizeForBounds:fitRect].height; 103 CGFloat offset = desiredHeight - NSHeight(currentRect); 104 105 [control setFrameSize:NSMakeSize(NSWidth(currentRect), 106 NSHeight(currentRect) + offset)]; 107 108 *totalOffset += offset; 109 110 // Move the control vertically by the new total offset. 111 NSPoint origin = [control frame].origin; 112 origin.y -= *totalOffset; 113 [control setFrameOrigin:origin]; 114} 115 116// Gets the desired height of |outlineView|. Simply using the view's frame 117// doesn't work if an animation is pending. 118CGFloat GetDesiredOutlineViewHeight(NSOutlineView* outlineView) { 119 CGFloat height = 0; 120 for (NSInteger i = 0; i < [outlineView numberOfRows]; ++i) 121 height += NSHeight([outlineView rectOfRow:i]); 122 return height; 123} 124 125void OffsetOutlineViewVerticallyToFitContent(NSOutlineView* outlineView, 126 CGFloat* totalOffset) { 127 NSScrollView* scrollView = [outlineView enclosingScrollView]; 128 NSRect frame = [scrollView frame]; 129 CGFloat desiredHeight = GetDesiredOutlineViewHeight(outlineView); 130 if (desiredHeight > kMaxControlHeight) 131 desiredHeight = kMaxControlHeight; 132 CGFloat offset = desiredHeight - NSHeight(frame); 133 frame.size.height += offset; 134 135 *totalOffset += offset; 136 137 // Move the control vertically by the new total offset. 138 frame.origin.y -= *totalOffset; 139 [scrollView setFrame:frame]; 140} 141 142void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { 143 ExtensionInstallViewController* controller = 144 static_cast<ExtensionInstallViewController*>(data); 145 [controller appendRatingStar:skiaImage]; 146} 147 148void DrawBulletInFrame(NSRect frame) { 149 NSRect rect; 150 rect.size.width = std::min(NSWidth(frame), NSHeight(frame)) * 0.25; 151 rect.size.height = NSWidth(rect); 152 rect.origin.x = frame.origin.x + (NSWidth(frame) - NSWidth(rect)) / 2.0; 153 rect.origin.y = frame.origin.y + (NSHeight(frame) - NSHeight(rect)) / 2.0; 154 rect = NSIntegralRect(rect); 155 156 [[NSColor colorWithCalibratedWhite:0.0 alpha:0.42] set]; 157 [[NSBezierPath bezierPathWithOvalInRect:rect] fill]; 158} 159 160bool HasAttribute(id item, CellAttributesMask attributeMask) { 161 return [[item objectForKey:kCellAttributesKey] intValue] & attributeMask; 162} 163 164} // namespace 165 166@implementation ExtensionInstallViewController 167 168@synthesize iconView = iconView_; 169@synthesize titleField = titleField_; 170@synthesize itemsField = itemsField_; 171@synthesize cancelButton = cancelButton_; 172@synthesize okButton = okButton_; 173@synthesize outlineView = outlineView_; 174@synthesize warningsSeparator = warningsSeparator_; 175@synthesize ratingStars = ratingStars_; 176@synthesize ratingCountField = ratingCountField_; 177@synthesize userCountField = userCountField_; 178@synthesize storeLinkButton = storeLinkButton_; 179 180- (id)initWithNavigator:(content::PageNavigator*)navigator 181 delegate:(ExtensionInstallPrompt::Delegate*)delegate 182 prompt:(const ExtensionInstallPrompt::Prompt&)prompt { 183 // We use a different XIB in the case of bundle installs, inline installs or 184 // no permission warnings. These are laid out nicely for the data they 185 // display. 186 NSString* nibName = nil; 187 if (prompt.type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT) { 188 nibName = @"ExtensionInstallPromptBundle"; 189 } else if (prompt.type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT) { 190 nibName = @"ExtensionInstallPromptInline"; 191 } else if (!prompt.ShouldShowPermissions() && 192 prompt.GetOAuthIssueCount() == 0 && 193 prompt.GetRetainedFileCount() == 0) { 194 nibName = @"ExtensionInstallPromptNoWarnings"; 195 } else { 196 nibName = @"ExtensionInstallPrompt"; 197 } 198 199 if ((self = [super initWithNibName:nibName 200 bundle:base::mac::FrameworkBundle()])) { 201 navigator_ = navigator; 202 delegate_ = delegate; 203 prompt_.reset(new ExtensionInstallPrompt::Prompt(prompt)); 204 warnings_.reset([[self buildWarnings:prompt] retain]); 205 } 206 return self; 207} 208 209- (IBAction)storeLinkClicked:(id)sender { 210 GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() + 211 prompt_->extension()->id()); 212 navigator_->OpenURL(OpenURLParams( 213 store_url, Referrer(), NEW_FOREGROUND_TAB, content::PAGE_TRANSITION_LINK, 214 false)); 215 216 delegate_->InstallUIAbort(/*user_initiated=*/true); 217} 218 219- (IBAction)cancel:(id)sender { 220 delegate_->InstallUIAbort(/*user_initiated=*/true); 221} 222 223- (IBAction)ok:(id)sender { 224 delegate_->InstallUIProceed(); 225} 226 227- (void)awakeFromNib { 228 // Set control labels. 229 [titleField_ setStringValue:base::SysUTF16ToNSString(prompt_->GetHeading())]; 230 NSRect okButtonRect; 231 if (prompt_->HasAcceptButtonLabel()) { 232 [okButton_ setTitle:base::SysUTF16ToNSString( 233 prompt_->GetAcceptButtonLabel())]; 234 } else { 235 [okButton_ removeFromSuperview]; 236 okButtonRect = [okButton_ frame]; 237 okButton_ = nil; 238 } 239 [cancelButton_ setTitle:prompt_->HasAbortButtonLabel() ? 240 base::SysUTF16ToNSString(prompt_->GetAbortButtonLabel()) : 241 l10n_util::GetNSString(IDS_CANCEL)]; 242 if ([self isInlineInstall]) { 243 prompt_->AppendRatingStars(AppendRatingStarsShim, self); 244 [ratingCountField_ setStringValue:base::SysUTF16ToNSString( 245 prompt_->GetRatingCount())]; 246 [userCountField_ setStringValue:base::SysUTF16ToNSString( 247 prompt_->GetUserCount())]; 248 [[storeLinkButton_ cell] setUnderlineOnHover:YES]; 249 [[storeLinkButton_ cell] setTextColor: 250 gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())]; 251 } 252 253 // The bundle install dialog has no icon. 254 if (![self isBundleInstall]) 255 [iconView_ setImage:prompt_->icon().ToNSImage()]; 256 257 // The dialog is laid out in the NIB exactly how we want it assuming that 258 // each label fits on one line. However, for each label, we want to allow 259 // wrapping onto multiple lines. So we accumulate an offset by measuring how 260 // big each label wants to be, and comparing it to how big it actually is. 261 // Then we shift each label down and resize by the appropriate amount, then 262 // finally resize the window. 263 CGFloat totalOffset = 0.0; 264 265 OffsetControlVerticallyToFitContent(titleField_, &totalOffset); 266 267 // Resize |okButton_| and |cancelButton_| to fit the button labels, but keep 268 // them right-aligned. 269 NSSize buttonDelta; 270 if (okButton_) { 271 buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_]; 272 if (buttonDelta.width) { 273 [okButton_ setFrame:NSOffsetRect([okButton_ frame], 274 -buttonDelta.width, 0)]; 275 [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame], 276 -buttonDelta.width, 0)]; 277 } 278 } else { 279 // Make |cancelButton_| right-aligned in the absence of |okButton_|. 280 NSRect cancelButtonRect = [cancelButton_ frame]; 281 cancelButtonRect.origin.x = 282 NSMaxX(okButtonRect) - NSWidth(cancelButtonRect); 283 [cancelButton_ setFrame:cancelButtonRect]; 284 } 285 buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_]; 286 if (buttonDelta.width) { 287 [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame], 288 -buttonDelta.width, 0)]; 289 } 290 291 if ([self isBundleInstall]) { 292 // We display the list of extension names as a simple text string, seperated 293 // by newlines. 294 BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState( 295 BundleInstaller::Item::STATE_PENDING); 296 297 NSMutableString* joinedItems = [NSMutableString string]; 298 for (size_t i = 0; i < items.size(); ++i) { 299 if (i > 0) 300 [joinedItems appendString:@"\n"]; 301 [joinedItems appendString:base::SysUTF16ToNSString( 302 items[i].GetNameForDisplay())]; 303 } 304 [itemsField_ setStringValue:joinedItems]; 305 306 // Adjust the controls to fit the list of extensions. 307 OffsetControlVerticallyToFitContent(itemsField_, &totalOffset); 308 } 309 310 // If there are any warnings or OAuth issues, then we have to do some special 311 // layout. 312 if (prompt_->ShouldShowPermissions() || prompt_->GetOAuthIssueCount() > 0 || 313 prompt_->GetRetainedFileCount() > 0) { 314 NSSize spacing = [outlineView_ intercellSpacing]; 315 spacing.width += 2; 316 spacing.height += 2; 317 [outlineView_ setIntercellSpacing:spacing]; 318 [[[[outlineView_ tableColumns] objectAtIndex:0] dataCell] setWraps:YES]; 319 for (id item in warnings_.get()) 320 [self expandItemAndChildren:item]; 321 322 // Adjust the outline view to fit the warnings. 323 OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset); 324 } else if ([self isInlineInstall] || [self isBundleInstall]) { 325 // Inline and bundle installs that don't have a permissions section need to 326 // hide controls related to that and shrink the window by the space they 327 // take up. 328 NSRect hiddenRect = NSUnionRect([warningsSeparator_ frame], 329 [[outlineView_ enclosingScrollView] frame]); 330 [warningsSeparator_ setHidden:YES]; 331 [[outlineView_ enclosingScrollView] setHidden:YES]; 332 totalOffset -= NSHeight(hiddenRect) + kWarningsSeparatorPadding; 333 } 334 335 // If necessary, adjust the window size. 336 if (totalOffset) { 337 NSRect currentRect = [[self view] bounds]; 338 currentRect.size.height += totalOffset; 339 [self updateViewFrame:currentRect]; 340 } 341} 342 343- (BOOL)isBundleInstall { 344 return prompt_->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT; 345} 346 347- (BOOL)isInlineInstall { 348 return prompt_->type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT; 349} 350 351- (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage { 352 NSImage* image = gfx::NSImageFromImageSkiaWithColorSpace( 353 *skiaImage, base::mac::GetSystemColorSpace()); 354 NSRect frame = NSMakeRect(0, 0, skiaImage->width(), skiaImage->height()); 355 base::scoped_nsobject<NSImageView> view( 356 [[NSImageView alloc] initWithFrame:frame]); 357 [view setImage:image]; 358 359 // Add this star after all the other ones 360 CGFloat maxStarRight = 0; 361 if ([[ratingStars_ subviews] count]) { 362 maxStarRight = NSMaxX([[[ratingStars_ subviews] lastObject] frame]); 363 } 364 NSRect starBounds = NSMakeRect(maxStarRight, 0, 365 skiaImage->width(), skiaImage->height()); 366 [view setFrame:starBounds]; 367 [ratingStars_ addSubview:view]; 368} 369 370- (void)onOutlineViewRowCountDidChange { 371 // Force the outline view to update. 372 [outlineView_ reloadData]; 373 374 CGFloat totalOffset = 0.0; 375 OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset); 376 if (totalOffset) { 377 NSRect currentRect = [[self view] bounds]; 378 currentRect.size.height += totalOffset; 379 [self updateViewFrame:currentRect]; 380 } 381} 382 383- (id)outlineView:(NSOutlineView*)outlineView 384 child:(NSInteger)index 385 ofItem:(id)item { 386 if (!item) 387 return [warnings_ objectAtIndex:index]; 388 if ([item isKindOfClass:[NSDictionary class]]) 389 return [[item objectForKey:kChildrenKey] objectAtIndex:index]; 390 NOTREACHED(); 391 return nil; 392} 393 394- (BOOL)outlineView:(NSOutlineView*)outlineView 395 isItemExpandable:(id)item { 396 return [self outlineView:outlineView numberOfChildrenOfItem:item] > 0; 397} 398 399- (NSInteger)outlineView:(NSOutlineView*)outlineView 400 numberOfChildrenOfItem:(id)item { 401 if (!item) 402 return [warnings_ count]; 403 404 if ([item isKindOfClass:[NSDictionary class]]) 405 return [[item objectForKey:kChildrenKey] count]; 406 407 NOTREACHED(); 408 return 0; 409} 410 411- (id)outlineView:(NSOutlineView*)outlineView 412 objectValueForTableColumn:(NSTableColumn *)tableColumn 413 byItem:(id)item { 414 return [item objectForKey:kTitleKey]; 415} 416 417- (BOOL)outlineView:(NSOutlineView *)outlineView 418 shouldExpandItem:(id)item { 419 return HasAttribute(item, kCanExpand); 420} 421 422- (void)outlineViewItemDidExpand:sender { 423 // Call via run loop to avoid animation glitches. 424 [self performSelector:@selector(onOutlineViewRowCountDidChange) 425 withObject:nil 426 afterDelay:0]; 427} 428 429- (void)outlineViewItemDidCollapse:sender { 430 // Call via run loop to avoid animation glitches. 431 [self performSelector:@selector(onOutlineViewRowCountDidChange) 432 withObject:nil 433 afterDelay:0]; 434} 435 436- (CGFloat)outlineView:(NSOutlineView *)outlineView 437 heightOfRowByItem:(id)item { 438 // Prevent reentrancy due to the frameOfCellAtColumn:row: call below. 439 if (isComputingRowHeight_) 440 return 1; 441 base::AutoReset<BOOL> reset(&isComputingRowHeight_, YES); 442 443 NSCell* cell = [[[outlineView_ tableColumns] objectAtIndex:0] dataCell]; 444 [cell setStringValue:[item objectForKey:kTitleKey]]; 445 NSRect bounds = NSZeroRect; 446 NSInteger row = [outlineView_ rowForItem:item]; 447 bounds.size.width = NSWidth([outlineView_ frameOfCellAtColumn:0 row:row]); 448 bounds.size.height = kMaxControlHeight; 449 450 return [cell cellSizeForBounds:bounds].height; 451} 452 453- (BOOL)outlineView:(NSOutlineView*)outlineView 454 shouldShowOutlineCellForItem:(id)item { 455 return !HasAttribute(item, kNoExpandMarker); 456} 457 458- (BOOL)outlineView:(NSOutlineView*)outlineView 459 shouldTrackCell:(NSCell*)cell 460 forTableColumn:(NSTableColumn*)tableColumn 461 item:(id)item { 462 return HasAttribute(item, kUseCustomLinkCell); 463} 464 465- (void)outlineView:(NSOutlineView*)outlineView 466 willDisplayCell:(id)cell 467 forTableColumn:(NSTableColumn *)tableColumn 468 item:(id)item { 469 if (HasAttribute(item, kBoldText)) 470 [cell setFont:[NSFont boldSystemFontOfSize:12.0]]; 471 else 472 [cell setFont:[NSFont systemFontOfSize:12.0]]; 473} 474 475- (void)outlineView:(NSOutlineView *)outlineView 476 willDisplayOutlineCell:(id)cell 477 forTableColumn:(NSTableColumn *)tableColumn 478 item:(id)item { 479 if (HasAttribute(item, kNoExpandMarker)) { 480 [cell setImagePosition:NSNoImage]; 481 return; 482 } 483 484 if (HasAttribute(item, kUseBullet)) { 485 // Replace disclosure triangles with bullet lists for leaf nodes. 486 [cell setImagePosition:NSNoImage]; 487 DrawBulletInFrame([outlineView_ frameOfOutlineCellAtRow: 488 [outlineView_ rowForItem:item]]); 489 return; 490 } 491 492 // Reset image to default value. 493 [cell setImagePosition:NSImageOverlaps]; 494} 495 496- (BOOL)outlineView:(NSOutlineView *)outlineView 497 shouldSelectItem:(id)item { 498 return false; 499} 500 501- (NSCell*)outlineView:(NSOutlineView*)outlineView 502 dataCellForTableColumn:(NSTableColumn*)tableColumn 503 item:(id)item { 504 if (HasAttribute(item, kUseCustomLinkCell)) { 505 base::scoped_nsobject<DetailToggleHyperlinkButtonCell> cell( 506 [[DetailToggleHyperlinkButtonCell alloc] initTextCell:@""]); 507 [cell setTarget:self]; 508 [cell setLinkClickedAction:@selector(onToggleDetailsLinkClicked:)]; 509 [cell setAlignment:NSLeftTextAlignment]; 510 [cell setUnderlineOnHover:YES]; 511 [cell setTextColor: 512 gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())]; 513 514 size_t detailsIndex = 515 [[item objectForKey:kPermissionsDetailIndex] unsignedIntegerValue]; 516 [cell setPermissionsDetailIndex:detailsIndex]; 517 518 ExtensionInstallPrompt::DetailsType detailsType = 519 static_cast<ExtensionInstallPrompt::DetailsType>( 520 [[item objectForKey:kPermissionsDetailType] unsignedIntegerValue]); 521 [cell setPermissionsDetailType:detailsType]; 522 523 if (prompt_->GetIsShowingDetails(detailsType, detailsIndex)) { 524 [cell setTitle: 525 l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_HIDE_DETAILS)]; 526 } else { 527 [cell setTitle: 528 l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_SHOW_DETAILS)]; 529 } 530 531 return cell.autorelease(); 532 } else { 533 return [tableColumn dataCell]; 534 } 535} 536 537- (void)expandItemAndChildren:(id)item { 538 if (HasAttribute(item, kAutoExpandCell)) 539 [outlineView_ expandItem:item expandChildren:NO]; 540 541 for (id child in [item objectForKey:kChildrenKey]) 542 [self expandItemAndChildren:child]; 543} 544 545- (void)onToggleDetailsLinkClicked:(id)sender { 546 size_t index = [sender permissionsDetailIndex]; 547 ExtensionInstallPrompt::DetailsType type = [sender permissionsDetailType]; 548 prompt_->SetIsShowingDetails( 549 type, index, !prompt_->GetIsShowingDetails(type, index)); 550 551 warnings_.reset([[self buildWarnings:*prompt_] retain]); 552 [outlineView_ reloadData]; 553 554 for (id item in warnings_.get()) 555 [self expandItemAndChildren:item]; 556} 557 558- (NSDictionary*)buildItemWithTitle:(NSString*)title 559 cellAttributes:(CellAttributes)cellAttributes 560 children:(NSArray*)children { 561 if (!children || ([children count] == 0 && cellAttributes & kUseBullet)) { 562 // Add a dummy child even though this is a leaf node. This will cause 563 // the outline view to show a disclosure triangle for this item. 564 // This is later overriden in willDisplayOutlineCell: to draw a bullet 565 // instead. (The bullet could be placed in the title instead but then 566 // the bullet wouldn't line up with disclosure triangles of sibling nodes.) 567 children = [NSArray arrayWithObject:[NSDictionary dictionary]]; 568 } else { 569 cellAttributes = cellAttributes | kCanExpand; 570 } 571 572 return @{ 573 kTitleKey : title, 574 kChildrenKey : children, 575 kCellAttributesKey : [NSNumber numberWithInt:cellAttributes], 576 kPermissionsDetailIndex : @0ul, 577 kPermissionsDetailType : @0ul, 578 }; 579} 580 581- (NSDictionary*)buildDetailToggleItem:(size_t)type 582 permissionsDetailIndex:(size_t)index { 583 return @{ 584 kTitleKey : @"", 585 kChildrenKey : @[ @{} ], 586 kCellAttributesKey : [NSNumber numberWithInt:kUseCustomLinkCell | 587 kNoExpandMarker], 588 kPermissionsDetailIndex : [NSNumber numberWithUnsignedInteger:index], 589 kPermissionsDetailType : [NSNumber numberWithUnsignedInteger:type], 590 }; 591} 592 593- (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt { 594 NSMutableArray* warnings = [NSMutableArray array]; 595 NSString* heading = nil; 596 597 ExtensionInstallPrompt::DetailsType type = 598 ExtensionInstallPrompt::PERMISSIONS_DETAILS; 599 if (prompt.ShouldShowPermissions()) { 600 NSMutableArray* children = [NSMutableArray array]; 601 if (prompt.GetPermissionCount() > 0) { 602 for (size_t i = 0; i < prompt.GetPermissionCount(); ++i) { 603 [children addObject: 604 [self buildItemWithTitle:SysUTF16ToNSString(prompt.GetPermission(i)) 605 cellAttributes:kUseBullet 606 children:nil]]; 607 608 // If there are additional details, add them below this item. 609 if (!prompt.GetPermissionsDetails(i).empty()) { 610 if (prompt.GetIsShowingDetails( 611 ExtensionInstallPrompt::PERMISSIONS_DETAILS, i)) { 612 [children addObject: 613 [self buildItemWithTitle:SysUTF16ToNSString( 614 prompt.GetPermissionsDetails(i)) 615 cellAttributes:kNoExpandMarker 616 children:nil]]; 617 } 618 619 // Add a row for the link. 620 [children addObject: 621 [self buildDetailToggleItem:type permissionsDetailIndex:i]]; 622 } 623 } 624 625 heading = SysUTF16ToNSString(prompt.GetPermissionsHeading()); 626 } else { 627 [children addObject: 628 [self buildItemWithTitle: 629 l10n_util::GetNSString(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS) 630 cellAttributes:kUseBullet 631 children:nil]]; 632 heading = @""; 633 } 634 635 [warnings addObject:[self 636 buildItemWithTitle:heading 637 cellAttributes:kBoldText | kAutoExpandCell | kNoExpandMarker 638 children:children]]; 639 } 640 641 if (prompt.GetOAuthIssueCount() > 0) { 642 type = ExtensionInstallPrompt::OAUTH_DETAILS; 643 644 NSMutableArray* children = [NSMutableArray array]; 645 646 for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) { 647 NSMutableArray* details = [NSMutableArray array]; 648 const IssueAdviceInfoEntry& issue = prompt.GetOAuthIssue(i); 649 if (!issue.details.empty() && prompt.GetIsShowingDetails(type, i)) { 650 for (size_t j = 0; j < issue.details.size(); ++j) { 651 [details addObject: 652 [self buildItemWithTitle:SysUTF16ToNSString(issue.details[j]) 653 cellAttributes:kNoExpandMarker 654 children:nil]]; 655 } 656 } 657 658 [children addObject: 659 [self buildItemWithTitle:SysUTF16ToNSString(issue.description) 660 cellAttributes:kUseBullet | kAutoExpandCell 661 children:details]]; 662 663 if (!issue.details.empty()) { 664 // Add a row for the link. 665 [children addObject: 666 [self buildDetailToggleItem:type permissionsDetailIndex:i]]; 667 } 668 } 669 670 [warnings addObject: 671 [self buildItemWithTitle:SysUTF16ToNSString(prompt.GetOAuthHeading()) 672 cellAttributes:kBoldText | kAutoExpandCell| kNoExpandMarker 673 children:children]]; 674 } 675 676 if (prompt.GetRetainedFileCount() > 0) { 677 type = ExtensionInstallPrompt::RETAINED_FILES_DETAILS; 678 679 NSMutableArray* children = [NSMutableArray array]; 680 681 if (prompt.GetIsShowingDetails(type, 0)) { 682 for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i) { 683 [children addObject: 684 [self buildItemWithTitle:SysUTF16ToNSString( 685 prompt.GetRetainedFile(i)) 686 cellAttributes:kUseBullet 687 children:nil]]; 688 } 689 } 690 691 [warnings addObject: 692 [self buildItemWithTitle:SysUTF16ToNSString( 693 prompt.GetRetainedFilesHeadingWithCount()) 694 cellAttributes:kBoldText | kAutoExpandCell | kNoExpandMarker 695 children:children]]; 696 697 // Add a row for the link. 698 [warnings addObject: 699 [self buildDetailToggleItem:type permissionsDetailIndex:0]]; 700 } 701 702 return warnings; 703} 704 705- (void)updateViewFrame:(NSRect)frame { 706 NSWindow* window = [[self view] window]; 707 [window setFrame:[window frameRectForContentRect:frame] display:YES]; 708 [[self view] setFrame:frame]; 709} 710 711@end 712 713 714@implementation DetailToggleHyperlinkButtonCell 715 716@synthesize permissionsDetailIndex = permissionsDetailIndex_; 717@synthesize permissionsDetailType = permissionsDetailType_; 718@synthesize linkClickedAction = linkClickedAction_; 719 720+ (BOOL)prefersTrackingUntilMouseUp { 721 return YES; 722} 723 724- (NSRect)drawingRectForBounds:(NSRect)rect { 725 NSRect rectInset = NSMakeRect(rect.origin.x + kLinkCellPaddingLeft, 726 rect.origin.y, 727 rect.size.width - kLinkCellPaddingLeft, 728 rect.size.height); 729 return [super drawingRectForBounds:rectInset]; 730} 731 732- (NSUInteger)hitTestForEvent:(NSEvent*)event 733 inRect:(NSRect)cellFrame 734 ofView:(NSView*)controlView { 735 NSUInteger hitTestResult = 736 [super hitTestForEvent:event inRect:cellFrame ofView:controlView]; 737 if ((hitTestResult & NSCellHitContentArea) != 0) 738 hitTestResult |= NSCellHitTrackableArea; 739 return hitTestResult; 740} 741 742- (void)handleLinkClicked { 743 [NSApp sendAction:linkClickedAction_ to:[self target] from:self]; 744} 745 746- (BOOL)trackMouse:(NSEvent*)event 747 inRect:(NSRect)cellFrame 748 ofView:(NSView*)controlView 749 untilMouseUp:(BOOL)flag { 750 BOOL result = YES; 751 NSUInteger hitTestResult = 752 [self hitTestForEvent:event inRect:cellFrame ofView:controlView]; 753 if ((hitTestResult & NSCellHitContentArea) != 0) { 754 result = [super trackMouse:event 755 inRect:cellFrame 756 ofView:controlView 757 untilMouseUp:flag]; 758 event = [NSApp currentEvent]; 759 hitTestResult = 760 [self hitTestForEvent:event inRect:cellFrame ofView:controlView]; 761 if ((hitTestResult & NSCellHitContentArea) != 0) 762 [self handleLinkClicked]; 763 } 764 return result; 765} 766 767- (NSArray*)accessibilityActionNames { 768 return [[super accessibilityActionNames] 769 arrayByAddingObject:NSAccessibilityPressAction]; 770} 771 772- (void)accessibilityPerformAction:(NSString*)action { 773 if ([action isEqualToString:NSAccessibilityPressAction]) 774 [self handleLinkClicked]; 775 else 776 [super accessibilityPerformAction:action]; 777} 778 779@end 780