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.
5 #import "chrome/browser/ui/cocoa/extensions/extension_install_view_controller.h"
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 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/ui/browser.h"
17 #import "chrome/browser/ui/chrome_style.h"
18 #include "chrome/browser/ui/cocoa/extensions/bundle_util.h"
19 #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
20 #include "chrome/common/extensions/extension_constants.h"
21 #include "chrome/grit/generated_resources.h"
22 #include "content/public/browser/page_navigator.h"
23 #include "extensions/common/extension.h"
24 #include "extensions/common/extension_urls.h"
25 #include "skia/ext/skia_utils_mac.h"
26 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
27 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
28 #include "ui/base/l10n/l10n_util.h"
29 #include "ui/base/l10n/l10n_util_mac.h"
30 #include "ui/gfx/image/image_skia_util_mac.h"
32 using content::OpenURLParams;
33 using content::Referrer;
34 using extensions::BundleInstaller;
38 // A collection of attributes (bitmask) for how to draw a cell, the expand
39 // marker and the text in the cell.
40 enum CellAttributesMask {
42 kNoExpandMarker = 1 << 1,
44 kAutoExpandCell = 1 << 3,
45 kUseCustomLinkCell = 1 << 4,
49 typedef NSUInteger CellAttributes;
53 @interface ExtensionInstallViewController ()
54 - (BOOL)isBundleInstall;
55 - (BOOL)hasWebstoreData;
56 - (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage;
57 - (void)onOutlineViewRowCountDidChange;
58 - (NSDictionary*)buildItemWithTitle:(NSString*)title
59 cellAttributes:(CellAttributes)cellAttributes
60 children:(NSArray*)children;
61 - (NSDictionary*)buildDetailToggleItem:(size_t)type
62 permissionsDetailIndex:(size_t)index;
63 - (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt;
64 // Adds permissions of |type| from |prompt| to |children| and returns the
65 // the appropriate permissions header. If no permissions are found, NULL is
68 appendPermissionsForPrompt:(const ExtensionInstallPrompt::Prompt&)prompt
69 withType:(ExtensionInstallPrompt::PermissionsType)type
70 children:(NSMutableArray*)children;
71 - (void)updateViewFrame:(NSRect)frame;
74 @interface DetailToggleHyperlinkButtonCell : HyperlinkButtonCell {
75 NSUInteger permissionsDetailIndex_;
76 ExtensionInstallPrompt::DetailsType permissionsDetailType_;
77 SEL linkClickedAction_;
80 @property(assign, nonatomic) NSUInteger permissionsDetailIndex;
81 @property(assign, nonatomic)
82 ExtensionInstallPrompt::DetailsType permissionsDetailType;
83 @property(assign, nonatomic) SEL linkClickedAction;
89 // Padding above the warnings separator, we must also subtract this when hiding
91 const CGFloat kWarningsSeparatorPadding = 14;
93 // The left padding for the link cell.
94 const CGFloat kLinkCellPaddingLeft = 3;
96 // Maximum height we will adjust controls to when trying to accomodate their
98 const CGFloat kMaxControlHeight = 250;
100 NSString* const kTitleKey = @"title";
101 NSString* const kChildrenKey = @"children";
102 NSString* const kCellAttributesKey = @"cellAttributes";
103 NSString* const kPermissionsDetailIndex = @"permissionsDetailIndex";
104 NSString* const kPermissionsDetailType = @"permissionsDetailType";
106 // Computes the |control|'s desired height to fit its contents, constrained to
107 // be kMaxControlHeight at most.
108 CGFloat ComputeDesiredControlHeight(NSControl* control) {
109 NSRect rect = [control frame];
110 rect.size.height = kMaxControlHeight;
111 return [[control cell] cellSizeForBounds:rect].height;
114 // Adjust the |control|'s height so that its content is not clipped.
115 // This also adds the change in height to the |total_offset| and shifts the
116 // control down by that amount.
117 void OffsetControlVerticallyToFitContent(NSControl* control,
118 CGFloat* total_offset) {
119 // Adjust the control's height so that its content is not clipped.
120 NSRect current_rect = [control frame];
121 CGFloat desired_height = ComputeDesiredControlHeight(control);
122 CGFloat offset = desired_height - NSHeight(current_rect);
124 [control setFrameSize:NSMakeSize(NSWidth(current_rect),
125 NSHeight(current_rect) + offset)];
127 *total_offset += offset;
129 // Move the control vertically by the new total offset.
130 NSPoint origin = [control frame].origin;
131 origin.y -= *total_offset;
132 [control setFrameOrigin:origin];
135 // Adjust the |view|'s height so that its subviews are not clipped.
136 // This also adds the change in height to the |total_offset| and shifts the
137 // control down by that amount.
138 void OffsetViewVerticallyToFitContent(NSView* view, CGFloat* total_offset) {
139 // Adjust the view's height so that its subviews are not clipped.
140 CGFloat desired_height = 0;
141 for (NSView* subview in [view subviews]) {
142 int required_height = NSMaxY([subview frame]);
143 if (required_height > desired_height)
144 desired_height = required_height;
146 NSRect current_rect = [view frame];
147 CGFloat offset = desired_height - NSHeight(current_rect);
149 [view setFrameSize:NSMakeSize(NSWidth(current_rect),
150 NSHeight(current_rect) + offset)];
152 *total_offset += offset;
154 // Move the view vertically by the new total offset.
155 NSPoint origin = [view frame].origin;
156 origin.y -= *total_offset;
157 [view setFrameOrigin:origin];
160 // Gets the desired height of |outline_view|. Simply using the view's frame
161 // doesn't work if an animation is pending.
162 CGFloat GetDesiredOutlineViewHeight(NSOutlineView* outline_view) {
164 for (NSInteger i = 0; i < [outline_view numberOfRows]; ++i)
165 height += NSHeight([outline_view rectOfRow:i]);
169 void OffsetOutlineViewVerticallyToFitContent(NSOutlineView* outline_view,
170 CGFloat* total_offset) {
171 NSScrollView* scroll_view = [outline_view enclosingScrollView];
172 NSRect frame = [scroll_view frame];
173 CGFloat desired_height = GetDesiredOutlineViewHeight(outline_view);
174 if (desired_height > kMaxControlHeight)
175 desired_height = kMaxControlHeight;
176 CGFloat offset = desired_height - NSHeight(frame);
177 frame.size.height += offset;
179 *total_offset += offset;
181 // Move the control vertically by the new total offset.
182 frame.origin.y -= *total_offset;
183 [scroll_view setFrame:frame];
186 void AppendRatingStarsShim(const gfx::ImageSkia* skia_image, void* data) {
187 ExtensionInstallViewController* controller =
188 static_cast<ExtensionInstallViewController*>(data);
189 [controller appendRatingStar:skia_image];
192 void DrawBulletInFrame(NSRect frame) {
194 rect.size.width = std::min(NSWidth(frame), NSHeight(frame)) * 0.25;
195 rect.size.height = NSWidth(rect);
196 rect.origin.x = frame.origin.x + (NSWidth(frame) - NSWidth(rect)) / 2.0;
197 rect.origin.y = frame.origin.y + (NSHeight(frame) - NSHeight(rect)) / 2.0;
198 rect = NSIntegralRect(rect);
200 [[NSColor colorWithCalibratedWhite:0.0 alpha:0.42] set];
201 [[NSBezierPath bezierPathWithOvalInRect:rect] fill];
204 bool HasAttribute(id item, CellAttributesMask attributeMask) {
205 return [[item objectForKey:kCellAttributesKey] intValue] & attributeMask;
210 @implementation ExtensionInstallViewController
212 @synthesize iconView = iconView_;
213 @synthesize titleField = titleField_;
214 @synthesize itemsField = itemsField_;
215 @synthesize cancelButton = cancelButton_;
216 @synthesize okButton = okButton_;
217 @synthesize outlineView = outlineView_;
218 @synthesize warningsSeparator = warningsSeparator_;
219 @synthesize ratingStars = ratingStars_;
220 @synthesize ratingCountField = ratingCountField_;
221 @synthesize userCountField = userCountField_;
222 @synthesize storeLinkButton = storeLinkButton_;
224 - (id)initWithProfile:(Profile*)profile
225 navigator:(content::PageNavigator*)navigator
226 delegate:(ExtensionInstallPrompt::Delegate*)delegate
227 prompt:(scoped_refptr<ExtensionInstallPrompt::Prompt>)prompt {
228 // We use a different XIB in the case of bundle installs, installs with
229 // webstore data, or no permission warnings. These are laid out nicely for
230 // the data they display.
231 NSString* nibName = nil;
232 if (prompt->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT) {
233 nibName = @"ExtensionInstallPromptBundle";
234 } else if (prompt->has_webstore_data()) {
235 nibName = @"ExtensionInstallPromptWebstoreData";
236 } else if (!prompt->ShouldShowPermissions() &&
237 prompt->GetRetainedFileCount() == 0 &&
238 prompt->GetRetainedDeviceCount() == 0) {
239 nibName = @"ExtensionInstallPromptNoWarnings";
241 nibName = @"ExtensionInstallPrompt";
244 if ((self = [super initWithNibName:nibName
245 bundle:base::mac::FrameworkBundle()])) {
247 navigator_ = navigator;
248 delegate_ = delegate;
250 warnings_.reset([[self buildWarnings:*prompt] retain]);
255 - (IBAction)storeLinkClicked:(id)sender {
256 GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
257 prompt_->extension()->id());
258 OpenURLParams params(store_url, Referrer(), NEW_FOREGROUND_TAB,
259 ui::PAGE_TRANSITION_LINK, false);
261 navigator_->OpenURL(params);
263 chrome::ScopedTabbedBrowserDisplayer displayer(
264 profile_, chrome::GetActiveDesktop());
265 displayer.browser()->OpenURL(params);
268 delegate_->InstallUIAbort(/*user_initiated=*/true);
271 - (IBAction)cancel:(id)sender {
272 delegate_->InstallUIAbort(/*user_initiated=*/true);
275 - (IBAction)ok:(id)sender {
276 delegate_->InstallUIProceed();
279 - (void)awakeFromNib {
280 // Set control labels.
281 [titleField_ setStringValue:base::SysUTF16ToNSString(prompt_->GetHeading())];
283 if (prompt_->HasAcceptButtonLabel()) {
284 [okButton_ setTitle:base::SysUTF16ToNSString(
285 prompt_->GetAcceptButtonLabel())];
287 [okButton_ removeFromSuperview];
288 okButtonRect = [okButton_ frame];
291 [cancelButton_ setTitle:prompt_->HasAbortButtonLabel() ?
292 base::SysUTF16ToNSString(prompt_->GetAbortButtonLabel()) :
293 l10n_util::GetNSString(IDS_CANCEL)];
294 if ([self hasWebstoreData]) {
295 prompt_->AppendRatingStars(AppendRatingStarsShim, self);
296 [ratingCountField_ setStringValue:base::SysUTF16ToNSString(
297 prompt_->GetRatingCount())];
298 [userCountField_ setStringValue:base::SysUTF16ToNSString(
299 prompt_->GetUserCount())];
300 [[storeLinkButton_ cell] setUnderlineOnHover:YES];
301 [[storeLinkButton_ cell] setTextColor:
302 gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())];
305 [iconView_ setImage:prompt_->icon().ToNSImage()];
307 // The dialog is laid out in the NIB exactly how we want it assuming that
308 // each label fits on one line. However, for each label, we want to allow
309 // wrapping onto multiple lines. So we accumulate an offset by measuring how
310 // big each label wants to be, and comparing it to how big it actually is.
311 // Then we shift each label down and resize by the appropriate amount, then
312 // finally resize the window.
313 CGFloat totalOffset = 0.0;
315 OffsetControlVerticallyToFitContent(titleField_, &totalOffset);
317 // Resize |okButton_| and |cancelButton_| to fit the button labels, but keep
318 // them right-aligned.
321 buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_];
322 if (buttonDelta.width) {
323 [okButton_ setFrame:NSOffsetRect([okButton_ frame],
324 -buttonDelta.width, 0)];
325 [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame],
326 -buttonDelta.width, 0)];
329 // Make |cancelButton_| right-aligned in the absence of |okButton_|.
330 NSRect cancelButtonRect = [cancelButton_ frame];
331 cancelButtonRect.origin.x =
332 NSMaxX(okButtonRect) - NSWidth(cancelButtonRect);
333 [cancelButton_ setFrame:cancelButtonRect];
335 buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
336 if (buttonDelta.width) {
337 [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame],
338 -buttonDelta.width, 0)];
341 if ([self isBundleInstall]) {
342 BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState(
343 BundleInstaller::Item::STATE_PENDING);
344 PopulateBundleItemsList(items, itemsField_);
346 // Adjust the view to fit the list of extensions.
347 OffsetViewVerticallyToFitContent(itemsField_, &totalOffset);
350 // If there are any warnings, retained devices or retained files, then we
351 // have to do some special layout.
352 if (prompt_->ShouldShowPermissions() || prompt_->GetRetainedFileCount() > 0) {
353 NSSize spacing = [outlineView_ intercellSpacing];
356 [outlineView_ setIntercellSpacing:spacing];
357 [[[[outlineView_ tableColumns] objectAtIndex:0] dataCell] setWraps:YES];
358 for (id item in warnings_.get())
359 [self expandItemAndChildren:item];
361 // Adjust the outline view to fit the warnings.
362 OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset);
363 } else if ([self hasWebstoreData] || [self isBundleInstall]) {
364 // Installs with webstore data and bundle installs that don't have a
365 // permissions section need to hide controls related to that and shrink the
366 // window by the space they take up.
367 NSRect hiddenRect = NSUnionRect([warningsSeparator_ frame],
368 [[outlineView_ enclosingScrollView] frame]);
369 [warningsSeparator_ setHidden:YES];
370 [[outlineView_ enclosingScrollView] setHidden:YES];
371 totalOffset -= NSHeight(hiddenRect) + kWarningsSeparatorPadding;
374 // If necessary, adjust the window size.
376 NSRect currentRect = [[self view] bounds];
377 currentRect.size.height += totalOffset;
378 [self updateViewFrame:currentRect];
382 - (BOOL)isBundleInstall {
383 return prompt_->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT;
386 - (BOOL)hasWebstoreData {
387 return prompt_->has_webstore_data();
390 - (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage {
391 NSImage* image = gfx::NSImageFromImageSkiaWithColorSpace(
392 *skiaImage, base::mac::GetSystemColorSpace());
393 NSRect frame = NSMakeRect(0, 0, skiaImage->width(), skiaImage->height());
394 base::scoped_nsobject<NSImageView> view(
395 [[NSImageView alloc] initWithFrame:frame]);
396 [view setImage:image];
398 // Add this star after all the other ones
399 CGFloat maxStarRight = 0;
400 if ([[ratingStars_ subviews] count]) {
401 maxStarRight = NSMaxX([[[ratingStars_ subviews] lastObject] frame]);
403 NSRect starBounds = NSMakeRect(maxStarRight, 0,
404 skiaImage->width(), skiaImage->height());
405 [view setFrame:starBounds];
406 [ratingStars_ addSubview:view];
409 - (void)onOutlineViewRowCountDidChange {
410 // Force the outline view to update.
411 [outlineView_ reloadData];
413 CGFloat totalOffset = 0.0;
414 OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset);
416 NSRect currentRect = [[self view] bounds];
417 currentRect.size.height += totalOffset;
418 [self updateViewFrame:currentRect];
422 - (id)outlineView:(NSOutlineView*)outlineView
423 child:(NSInteger)index
426 return [warnings_ objectAtIndex:index];
427 if ([item isKindOfClass:[NSDictionary class]])
428 return [[item objectForKey:kChildrenKey] objectAtIndex:index];
433 - (BOOL)outlineView:(NSOutlineView*)outlineView
434 isItemExpandable:(id)item {
435 return [self outlineView:outlineView numberOfChildrenOfItem:item] > 0;
438 - (NSInteger)outlineView:(NSOutlineView*)outlineView
439 numberOfChildrenOfItem:(id)item {
441 return [warnings_ count];
443 if ([item isKindOfClass:[NSDictionary class]])
444 return [[item objectForKey:kChildrenKey] count];
450 - (id)outlineView:(NSOutlineView*)outlineView
451 objectValueForTableColumn:(NSTableColumn *)tableColumn
453 return [item objectForKey:kTitleKey];
456 - (BOOL)outlineView:(NSOutlineView *)outlineView
457 shouldExpandItem:(id)item {
458 return HasAttribute(item, kCanExpand);
461 - (void)outlineViewItemDidExpand:sender {
462 // Call via run loop to avoid animation glitches.
463 [self performSelector:@selector(onOutlineViewRowCountDidChange)
468 - (void)outlineViewItemDidCollapse:sender {
469 // Call via run loop to avoid animation glitches.
470 [self performSelector:@selector(onOutlineViewRowCountDidChange)
475 - (CGFloat)outlineView:(NSOutlineView *)outlineView
476 heightOfRowByItem:(id)item {
477 // Prevent reentrancy due to the frameOfCellAtColumn:row: call below.
478 if (isComputingRowHeight_)
480 base::AutoReset<BOOL> reset(&isComputingRowHeight_, YES);
482 NSCell* cell = [[[outlineView_ tableColumns] objectAtIndex:0] dataCell];
483 [cell setStringValue:[item objectForKey:kTitleKey]];
484 NSRect bounds = NSZeroRect;
485 NSInteger row = [outlineView_ rowForItem:item];
486 bounds.size.width = NSWidth([outlineView_ frameOfCellAtColumn:0 row:row]);
487 bounds.size.height = kMaxControlHeight;
489 return [cell cellSizeForBounds:bounds].height;
492 - (BOOL)outlineView:(NSOutlineView*)outlineView
493 shouldShowOutlineCellForItem:(id)item {
494 return !HasAttribute(item, kNoExpandMarker);
497 - (BOOL)outlineView:(NSOutlineView*)outlineView
498 shouldTrackCell:(NSCell*)cell
499 forTableColumn:(NSTableColumn*)tableColumn
501 return HasAttribute(item, kUseCustomLinkCell);
504 - (void)outlineView:(NSOutlineView*)outlineView
505 willDisplayCell:(id)cell
506 forTableColumn:(NSTableColumn *)tableColumn
508 if (HasAttribute(item, kBoldText))
509 [cell setFont:[NSFont boldSystemFontOfSize:12.0]];
511 [cell setFont:[NSFont systemFontOfSize:12.0]];
514 - (void)outlineView:(NSOutlineView *)outlineView
515 willDisplayOutlineCell:(id)cell
516 forTableColumn:(NSTableColumn *)tableColumn
518 if (HasAttribute(item, kNoExpandMarker)) {
519 [cell setImagePosition:NSNoImage];
523 if (HasAttribute(item, kUseBullet)) {
524 // Replace disclosure triangles with bullet lists for leaf nodes.
525 [cell setImagePosition:NSNoImage];
526 DrawBulletInFrame([outlineView_ frameOfOutlineCellAtRow:
527 [outlineView_ rowForItem:item]]);
531 // Reset image to default value.
532 [cell setImagePosition:NSImageOverlaps];
535 - (BOOL)outlineView:(NSOutlineView *)outlineView
536 shouldSelectItem:(id)item {
540 - (NSCell*)outlineView:(NSOutlineView*)outlineView
541 dataCellForTableColumn:(NSTableColumn*)tableColumn
543 if (HasAttribute(item, kUseCustomLinkCell)) {
544 base::scoped_nsobject<DetailToggleHyperlinkButtonCell> cell(
545 [[DetailToggleHyperlinkButtonCell alloc] initTextCell:@""]);
546 [cell setTarget:self];
547 [cell setLinkClickedAction:@selector(onToggleDetailsLinkClicked:)];
548 [cell setAlignment:NSLeftTextAlignment];
549 [cell setUnderlineOnHover:YES];
551 gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())];
553 size_t detailsIndex =
554 [[item objectForKey:kPermissionsDetailIndex] unsignedIntegerValue];
555 [cell setPermissionsDetailIndex:detailsIndex];
557 ExtensionInstallPrompt::DetailsType detailsType =
558 static_cast<ExtensionInstallPrompt::DetailsType>(
559 [[item objectForKey:kPermissionsDetailType] unsignedIntegerValue]);
560 [cell setPermissionsDetailType:detailsType];
562 if (prompt_->GetIsShowingDetails(detailsType, detailsIndex)) {
564 l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_HIDE_DETAILS)];
567 l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_SHOW_DETAILS)];
570 return cell.autorelease();
572 return [tableColumn dataCell];
576 - (void)expandItemAndChildren:(id)item {
577 if (HasAttribute(item, kAutoExpandCell))
578 [outlineView_ expandItem:item expandChildren:NO];
580 for (id child in [item objectForKey:kChildrenKey])
581 [self expandItemAndChildren:child];
584 - (void)onToggleDetailsLinkClicked:(id)sender {
585 size_t index = [sender permissionsDetailIndex];
586 ExtensionInstallPrompt::DetailsType type = [sender permissionsDetailType];
587 prompt_->SetIsShowingDetails(
588 type, index, !prompt_->GetIsShowingDetails(type, index));
590 warnings_.reset([[self buildWarnings:*prompt_] retain]);
591 [outlineView_ reloadData];
593 for (id item in warnings_.get())
594 [self expandItemAndChildren:item];
597 - (NSDictionary*)buildItemWithTitle:(NSString*)title
598 cellAttributes:(CellAttributes)cellAttributes
599 children:(NSArray*)children {
600 if (!children || ([children count] == 0 && cellAttributes & kUseBullet)) {
601 // Add a dummy child even though this is a leaf node. This will cause
602 // the outline view to show a disclosure triangle for this item.
603 // This is later overriden in willDisplayOutlineCell: to draw a bullet
604 // instead. (The bullet could be placed in the title instead but then
605 // the bullet wouldn't line up with disclosure triangles of sibling nodes.)
606 children = [NSArray arrayWithObject:[NSDictionary dictionary]];
608 cellAttributes = cellAttributes | kCanExpand;
613 kChildrenKey : children,
614 kCellAttributesKey : [NSNumber numberWithInt:cellAttributes],
615 kPermissionsDetailIndex : @0ul,
616 kPermissionsDetailType : @0ul,
620 - (NSDictionary*)buildDetailToggleItem:(size_t)type
621 permissionsDetailIndex:(size_t)index {
624 kChildrenKey : @[ @{} ],
625 kCellAttributesKey : [NSNumber numberWithInt:kUseCustomLinkCell |
627 kPermissionsDetailIndex : [NSNumber numberWithUnsignedInteger:index],
628 kPermissionsDetailType : [NSNumber numberWithUnsignedInteger:type],
632 - (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt {
633 NSMutableArray* warnings = [NSMutableArray array];
634 NSString* heading = nil;
635 NSString* withheldHeading = nil;
637 bool hasPermissions = prompt.GetPermissionCount(
638 ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS);
639 CellAttributes warningCellAttributes =
640 kBoldText | kAutoExpandCell | kNoExpandMarker;
641 if (prompt.ShouldShowPermissions()) {
642 NSMutableArray* children = [NSMutableArray array];
643 NSMutableArray* withheldChildren = [NSMutableArray array];
646 [self appendPermissionsForPrompt:prompt
647 withType:ExtensionInstallPrompt::PermissionsType
648 ::REGULAR_PERMISSIONS
651 [self appendPermissionsForPrompt:prompt
652 withType:ExtensionInstallPrompt::PermissionsType
653 ::WITHHELD_PERMISSIONS
654 children:withheldChildren];
656 if (!hasPermissions) {
658 [self buildItemWithTitle:
659 l10n_util::GetNSString(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)
660 cellAttributes:kUseBullet
666 [warnings addObject:[self buildItemWithTitle:heading
667 cellAttributes:warningCellAttributes
671 // Add withheld permissions to the prompt if they exist.
672 if (withheldHeading) {
673 [warnings addObject:[self buildItemWithTitle:withheldHeading
674 cellAttributes:warningCellAttributes
675 children:withheldChildren]];
679 if (prompt.GetRetainedFileCount() > 0) {
680 const ExtensionInstallPrompt::DetailsType type =
681 ExtensionInstallPrompt::RETAINED_FILES_DETAILS;
683 NSMutableArray* children = [NSMutableArray array];
685 if (prompt.GetIsShowingDetails(type, 0)) {
686 for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i) {
687 NSString* title = SysUTF16ToNSString(prompt.GetRetainedFile(i));
688 [children addObject:[self buildItemWithTitle:title
689 cellAttributes:kUseBullet
694 NSString* title = SysUTF16ToNSString(prompt.GetRetainedFilesHeading());
695 [warnings addObject:[self buildItemWithTitle:title
696 cellAttributes:warningCellAttributes
699 // Add a row for the link.
701 [self buildDetailToggleItem:type permissionsDetailIndex:0]];
704 if (prompt.GetRetainedDeviceCount() > 0) {
705 const ExtensionInstallPrompt::DetailsType type =
706 ExtensionInstallPrompt::RETAINED_DEVICES_DETAILS;
708 NSMutableArray* children = [NSMutableArray array];
710 if (prompt.GetIsShowingDetails(type, 0)) {
711 for (size_t i = 0; i < prompt.GetRetainedDeviceCount(); ++i) {
713 SysUTF16ToNSString(prompt.GetRetainedDeviceMessageString(i));
714 [children addObject:[self buildItemWithTitle:title
715 cellAttributes:kUseBullet
720 NSString* title = SysUTF16ToNSString(prompt.GetRetainedDevicesHeading());
721 [warnings addObject:[self buildItemWithTitle:title
722 cellAttributes:warningCellAttributes
725 // Add a row for the link.
727 addObject:[self buildDetailToggleItem:type permissionsDetailIndex:0]];
734 appendPermissionsForPrompt:(const ExtensionInstallPrompt::Prompt&)prompt
735 withType:(ExtensionInstallPrompt::PermissionsType)type
736 children:(NSMutableArray*)children {
737 size_t permissionsCount = prompt.GetPermissionCount(type);
738 if (permissionsCount == 0)
741 for (size_t i = 0; i < permissionsCount; ++i) {
742 NSDictionary* item = [self
743 buildItemWithTitle:SysUTF16ToNSString(prompt.GetPermission(i, type))
744 cellAttributes:kUseBullet
746 [children addObject:item];
748 // If there are additional details, add them below this item.
749 if (!prompt.GetPermissionsDetails(i, type).empty()) {
750 if (prompt.GetIsShowingDetails(
751 ExtensionInstallPrompt::PERMISSIONS_DETAILS, i)) {
753 [self buildItemWithTitle:SysUTF16ToNSString(
754 prompt.GetPermissionsDetails(i, type))
755 cellAttributes:kNoExpandMarker
757 [children addObject:item];
760 // Add a row for the link.
762 [self buildDetailToggleItem:type permissionsDetailIndex:i]];
766 return SysUTF16ToNSString(prompt.GetPermissionsHeading(type));
769 - (void)updateViewFrame:(NSRect)frame {
770 NSWindow* window = [[self view] window];
771 [window setFrame:[window frameRectForContentRect:frame] display:YES];
772 [[self view] setFrame:frame];
778 @implementation DetailToggleHyperlinkButtonCell
780 @synthesize permissionsDetailIndex = permissionsDetailIndex_;
781 @synthesize permissionsDetailType = permissionsDetailType_;
782 @synthesize linkClickedAction = linkClickedAction_;
784 + (BOOL)prefersTrackingUntilMouseUp {
788 - (NSRect)drawingRectForBounds:(NSRect)rect {
789 NSRect rectInset = NSMakeRect(rect.origin.x + kLinkCellPaddingLeft,
791 rect.size.width - kLinkCellPaddingLeft,
793 return [super drawingRectForBounds:rectInset];
796 - (NSUInteger)hitTestForEvent:(NSEvent*)event
797 inRect:(NSRect)cellFrame
798 ofView:(NSView*)controlView {
799 NSUInteger hitTestResult =
800 [super hitTestForEvent:event inRect:cellFrame ofView:controlView];
801 if ((hitTestResult & NSCellHitContentArea) != 0)
802 hitTestResult |= NSCellHitTrackableArea;
803 return hitTestResult;
806 - (void)handleLinkClicked {
807 [NSApp sendAction:linkClickedAction_ to:[self target] from:self];
810 - (BOOL)trackMouse:(NSEvent*)event
811 inRect:(NSRect)cellFrame
812 ofView:(NSView*)controlView
813 untilMouseUp:(BOOL)flag {
815 NSUInteger hitTestResult =
816 [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
817 if ((hitTestResult & NSCellHitContentArea) != 0) {
818 result = [super trackMouse:event
822 event = [NSApp currentEvent];
824 [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
825 if ((hitTestResult & NSCellHitContentArea) != 0)
826 [self handleLinkClicked];
831 - (NSArray*)accessibilityActionNames {
832 return [[super accessibilityActionNames]
833 arrayByAddingObject:NSAccessibilityPressAction];
836 - (void)accessibilityPerformAction:(NSString*)action {
837 if ([action isEqualToString:NSAccessibilityPressAction])
838 [self handleLinkClicked];
840 [super accessibilityPerformAction:action];