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(
282 prompt_->GetDialogTitle())];
284 base::string16 acceptButtonLabel = prompt_->GetAcceptButtonLabel();
285 if (!acceptButtonLabel.empty()) {
286 [okButton_ setTitle:base::SysUTF16ToNSString(acceptButtonLabel)];
288 [okButton_ removeFromSuperview];
289 okButtonRect = [okButton_ frame];
292 [cancelButton_ setTitle:base::SysUTF16ToNSString(
293 prompt_->GetAbortButtonLabel())];
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 if ([self hasWebstoreData]) {
318 OffsetControlVerticallyToFitContent(ratingCountField_, &totalOffset);
319 OffsetViewVerticallyToFitContent(ratingStars_, &totalOffset);
320 OffsetControlVerticallyToFitContent(userCountField_, &totalOffset);
321 OffsetControlVerticallyToFitContent(storeLinkButton_, &totalOffset);
322 NSPoint separatorOrigin = [warningsSeparator_ frame].origin;
323 separatorOrigin.y -= totalOffset;
324 [warningsSeparator_ setFrameOrigin:separatorOrigin];
327 // Resize |okButton_| and |cancelButton_| to fit the button labels, but keep
328 // them right-aligned.
331 buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_];
332 if (buttonDelta.width) {
333 [okButton_ setFrame:NSOffsetRect([okButton_ frame],
334 -buttonDelta.width, 0)];
335 [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame],
336 -buttonDelta.width, 0)];
339 // Make |cancelButton_| right-aligned in the absence of |okButton_|.
340 NSRect cancelButtonRect = [cancelButton_ frame];
341 cancelButtonRect.origin.x =
342 NSMaxX(okButtonRect) - NSWidth(cancelButtonRect);
343 [cancelButton_ setFrame:cancelButtonRect];
345 buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
346 if (buttonDelta.width) {
347 [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame],
348 -buttonDelta.width, 0)];
351 if ([self isBundleInstall]) {
352 BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState(
353 BundleInstaller::Item::STATE_PENDING);
354 PopulateBundleItemsList(items, itemsField_);
356 // Adjust the view to fit the list of extensions.
357 OffsetViewVerticallyToFitContent(itemsField_, &totalOffset);
360 // If there are any warnings, retained devices or retained files, then we
361 // have to do some special layout.
362 if (prompt_->ShouldShowPermissions() || prompt_->GetRetainedFileCount() > 0) {
363 NSSize spacing = [outlineView_ intercellSpacing];
366 [outlineView_ setIntercellSpacing:spacing];
367 [[[[outlineView_ tableColumns] objectAtIndex:0] dataCell] setWraps:YES];
368 for (id item in warnings_.get())
369 [self expandItemAndChildren:item];
371 // Adjust the outline view to fit the warnings.
372 OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset);
373 } else if ([self hasWebstoreData] || [self isBundleInstall]) {
374 // Installs with webstore data and bundle installs that don't have a
375 // permissions section need to hide controls related to that and shrink the
376 // window by the space they take up.
377 NSRect hiddenRect = NSUnionRect([warningsSeparator_ frame],
378 [[outlineView_ enclosingScrollView] frame]);
379 [warningsSeparator_ setHidden:YES];
380 [[outlineView_ enclosingScrollView] setHidden:YES];
381 totalOffset -= NSHeight(hiddenRect) + kWarningsSeparatorPadding;
384 // If necessary, adjust the window size.
386 NSRect currentRect = [[self view] bounds];
387 currentRect.size.height += totalOffset;
388 [self updateViewFrame:currentRect];
392 - (BOOL)isBundleInstall {
393 return prompt_->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT;
396 - (BOOL)hasWebstoreData {
397 return prompt_->has_webstore_data();
400 - (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage {
401 NSImage* image = gfx::NSImageFromImageSkiaWithColorSpace(
402 *skiaImage, base::mac::GetSystemColorSpace());
403 NSRect frame = NSMakeRect(0, 0, skiaImage->width(), skiaImage->height());
404 base::scoped_nsobject<NSImageView> view(
405 [[NSImageView alloc] initWithFrame:frame]);
406 [view setImage:image];
408 // Add this star after all the other ones
409 CGFloat maxStarRight = 0;
410 if ([[ratingStars_ subviews] count]) {
411 maxStarRight = NSMaxX([[[ratingStars_ subviews] lastObject] frame]);
413 NSRect starBounds = NSMakeRect(maxStarRight, 0,
414 skiaImage->width(), skiaImage->height());
415 [view setFrame:starBounds];
416 [ratingStars_ addSubview:view];
419 - (void)onOutlineViewRowCountDidChange {
420 // Force the outline view to update.
421 [outlineView_ reloadData];
423 CGFloat totalOffset = 0.0;
424 OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset);
426 NSRect currentRect = [[self view] bounds];
427 currentRect.size.height += totalOffset;
428 [self updateViewFrame:currentRect];
432 - (id)outlineView:(NSOutlineView*)outlineView
433 child:(NSInteger)index
436 return [warnings_ objectAtIndex:index];
437 if ([item isKindOfClass:[NSDictionary class]])
438 return [[item objectForKey:kChildrenKey] objectAtIndex:index];
443 - (BOOL)outlineView:(NSOutlineView*)outlineView
444 isItemExpandable:(id)item {
445 return [self outlineView:outlineView numberOfChildrenOfItem:item] > 0;
448 - (NSInteger)outlineView:(NSOutlineView*)outlineView
449 numberOfChildrenOfItem:(id)item {
451 return [warnings_ count];
453 if ([item isKindOfClass:[NSDictionary class]])
454 return [[item objectForKey:kChildrenKey] count];
460 - (id)outlineView:(NSOutlineView*)outlineView
461 objectValueForTableColumn:(NSTableColumn *)tableColumn
463 return [item objectForKey:kTitleKey];
466 - (BOOL)outlineView:(NSOutlineView *)outlineView
467 shouldExpandItem:(id)item {
468 return HasAttribute(item, kCanExpand);
471 - (void)outlineViewItemDidExpand:sender {
472 // Call via run loop to avoid animation glitches.
473 [self performSelector:@selector(onOutlineViewRowCountDidChange)
478 - (void)outlineViewItemDidCollapse:sender {
479 // Call via run loop to avoid animation glitches.
480 [self performSelector:@selector(onOutlineViewRowCountDidChange)
485 - (CGFloat)outlineView:(NSOutlineView *)outlineView
486 heightOfRowByItem:(id)item {
487 // Prevent reentrancy due to the frameOfCellAtColumn:row: call below.
488 if (isComputingRowHeight_)
490 base::AutoReset<BOOL> reset(&isComputingRowHeight_, YES);
492 NSCell* cell = [[[outlineView_ tableColumns] objectAtIndex:0] dataCell];
493 [cell setStringValue:[item objectForKey:kTitleKey]];
494 NSRect bounds = NSZeroRect;
495 NSInteger row = [outlineView_ rowForItem:item];
496 bounds.size.width = NSWidth([outlineView_ frameOfCellAtColumn:0 row:row]);
497 bounds.size.height = kMaxControlHeight;
499 return [cell cellSizeForBounds:bounds].height;
502 - (BOOL)outlineView:(NSOutlineView*)outlineView
503 shouldShowOutlineCellForItem:(id)item {
504 return !HasAttribute(item, kNoExpandMarker);
507 - (BOOL)outlineView:(NSOutlineView*)outlineView
508 shouldTrackCell:(NSCell*)cell
509 forTableColumn:(NSTableColumn*)tableColumn
511 return HasAttribute(item, kUseCustomLinkCell);
514 - (void)outlineView:(NSOutlineView*)outlineView
515 willDisplayCell:(id)cell
516 forTableColumn:(NSTableColumn *)tableColumn
518 if (HasAttribute(item, kBoldText))
519 [cell setFont:[NSFont boldSystemFontOfSize:12.0]];
521 [cell setFont:[NSFont systemFontOfSize:12.0]];
524 - (void)outlineView:(NSOutlineView *)outlineView
525 willDisplayOutlineCell:(id)cell
526 forTableColumn:(NSTableColumn *)tableColumn
528 if (HasAttribute(item, kNoExpandMarker)) {
529 [cell setImagePosition:NSNoImage];
533 if (HasAttribute(item, kUseBullet)) {
534 // Replace disclosure triangles with bullet lists for leaf nodes.
535 [cell setImagePosition:NSNoImage];
536 DrawBulletInFrame([outlineView_ frameOfOutlineCellAtRow:
537 [outlineView_ rowForItem:item]]);
541 // Reset image to default value.
542 [cell setImagePosition:NSImageOverlaps];
545 - (BOOL)outlineView:(NSOutlineView *)outlineView
546 shouldSelectItem:(id)item {
550 - (NSCell*)outlineView:(NSOutlineView*)outlineView
551 dataCellForTableColumn:(NSTableColumn*)tableColumn
553 if (HasAttribute(item, kUseCustomLinkCell)) {
554 base::scoped_nsobject<DetailToggleHyperlinkButtonCell> cell(
555 [[DetailToggleHyperlinkButtonCell alloc] initTextCell:@""]);
556 [cell setTarget:self];
557 [cell setLinkClickedAction:@selector(onToggleDetailsLinkClicked:)];
558 [cell setAlignment:NSLeftTextAlignment];
559 [cell setUnderlineOnHover:YES];
561 gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())];
563 size_t detailsIndex =
564 [[item objectForKey:kPermissionsDetailIndex] unsignedIntegerValue];
565 [cell setPermissionsDetailIndex:detailsIndex];
567 ExtensionInstallPrompt::DetailsType detailsType =
568 static_cast<ExtensionInstallPrompt::DetailsType>(
569 [[item objectForKey:kPermissionsDetailType] unsignedIntegerValue]);
570 [cell setPermissionsDetailType:detailsType];
572 if (prompt_->GetIsShowingDetails(detailsType, detailsIndex)) {
574 l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_HIDE_DETAILS)];
577 l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_SHOW_DETAILS)];
580 return cell.autorelease();
582 return [tableColumn dataCell];
586 - (void)expandItemAndChildren:(id)item {
587 if (HasAttribute(item, kAutoExpandCell))
588 [outlineView_ expandItem:item expandChildren:NO];
590 for (id child in [item objectForKey:kChildrenKey])
591 [self expandItemAndChildren:child];
594 - (void)onToggleDetailsLinkClicked:(id)sender {
595 size_t index = [sender permissionsDetailIndex];
596 ExtensionInstallPrompt::DetailsType type = [sender permissionsDetailType];
597 prompt_->SetIsShowingDetails(
598 type, index, !prompt_->GetIsShowingDetails(type, index));
600 warnings_.reset([[self buildWarnings:*prompt_] retain]);
601 [outlineView_ reloadData];
603 for (id item in warnings_.get())
604 [self expandItemAndChildren:item];
607 - (NSDictionary*)buildItemWithTitle:(NSString*)title
608 cellAttributes:(CellAttributes)cellAttributes
609 children:(NSArray*)children {
610 if (!children || ([children count] == 0 && cellAttributes & kUseBullet)) {
611 // Add a dummy child even though this is a leaf node. This will cause
612 // the outline view to show a disclosure triangle for this item.
613 // This is later overriden in willDisplayOutlineCell: to draw a bullet
614 // instead. (The bullet could be placed in the title instead but then
615 // the bullet wouldn't line up with disclosure triangles of sibling nodes.)
616 children = [NSArray arrayWithObject:[NSDictionary dictionary]];
618 cellAttributes = cellAttributes | kCanExpand;
623 kChildrenKey : children,
624 kCellAttributesKey : [NSNumber numberWithInt:cellAttributes],
625 kPermissionsDetailIndex : @0ul,
626 kPermissionsDetailType : @0ul,
630 - (NSDictionary*)buildDetailToggleItem:(size_t)type
631 permissionsDetailIndex:(size_t)index {
634 kChildrenKey : @[ @{} ],
635 kCellAttributesKey : [NSNumber numberWithInt:kUseCustomLinkCell |
637 kPermissionsDetailIndex : [NSNumber numberWithUnsignedInteger:index],
638 kPermissionsDetailType : [NSNumber numberWithUnsignedInteger:type],
642 - (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt {
643 NSMutableArray* warnings = [NSMutableArray array];
644 NSString* heading = nil;
645 NSString* withheldHeading = nil;
647 bool hasPermissions = prompt.GetPermissionCount(
648 ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS);
649 CellAttributes warningCellAttributes =
650 kBoldText | kAutoExpandCell | kNoExpandMarker;
651 if (prompt.ShouldShowPermissions()) {
652 NSMutableArray* children = [NSMutableArray array];
653 NSMutableArray* withheldChildren = [NSMutableArray array];
656 [self appendPermissionsForPrompt:prompt
657 withType:ExtensionInstallPrompt::PermissionsType
658 ::REGULAR_PERMISSIONS
661 [self appendPermissionsForPrompt:prompt
662 withType:ExtensionInstallPrompt::PermissionsType
663 ::WITHHELD_PERMISSIONS
664 children:withheldChildren];
666 if (!hasPermissions) {
668 [self buildItemWithTitle:
669 l10n_util::GetNSString(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)
670 cellAttributes:kUseBullet
676 [warnings addObject:[self buildItemWithTitle:heading
677 cellAttributes:warningCellAttributes
681 // Add withheld permissions to the prompt if they exist.
682 if (withheldHeading) {
683 [warnings addObject:[self buildItemWithTitle:withheldHeading
684 cellAttributes:warningCellAttributes
685 children:withheldChildren]];
689 if (prompt.GetRetainedFileCount() > 0) {
690 const ExtensionInstallPrompt::DetailsType type =
691 ExtensionInstallPrompt::RETAINED_FILES_DETAILS;
693 NSMutableArray* children = [NSMutableArray array];
695 if (prompt.GetIsShowingDetails(type, 0)) {
696 for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i) {
697 NSString* title = SysUTF16ToNSString(prompt.GetRetainedFile(i));
698 [children addObject:[self buildItemWithTitle:title
699 cellAttributes:kUseBullet
704 NSString* title = SysUTF16ToNSString(prompt.GetRetainedFilesHeading());
705 [warnings addObject:[self buildItemWithTitle:title
706 cellAttributes:warningCellAttributes
709 // Add a row for the link.
711 [self buildDetailToggleItem:type permissionsDetailIndex:0]];
714 if (prompt.GetRetainedDeviceCount() > 0) {
715 const ExtensionInstallPrompt::DetailsType type =
716 ExtensionInstallPrompt::RETAINED_DEVICES_DETAILS;
718 NSMutableArray* children = [NSMutableArray array];
720 if (prompt.GetIsShowingDetails(type, 0)) {
721 for (size_t i = 0; i < prompt.GetRetainedDeviceCount(); ++i) {
723 SysUTF16ToNSString(prompt.GetRetainedDeviceMessageString(i));
724 [children addObject:[self buildItemWithTitle:title
725 cellAttributes:kUseBullet
730 NSString* title = SysUTF16ToNSString(prompt.GetRetainedDevicesHeading());
731 [warnings addObject:[self buildItemWithTitle:title
732 cellAttributes:warningCellAttributes
735 // Add a row for the link.
737 addObject:[self buildDetailToggleItem:type permissionsDetailIndex:0]];
744 appendPermissionsForPrompt:(const ExtensionInstallPrompt::Prompt&)prompt
745 withType:(ExtensionInstallPrompt::PermissionsType)type
746 children:(NSMutableArray*)children {
747 size_t permissionsCount = prompt.GetPermissionCount(type);
748 if (permissionsCount == 0)
751 for (size_t i = 0; i < permissionsCount; ++i) {
752 NSDictionary* item = [self
753 buildItemWithTitle:SysUTF16ToNSString(prompt.GetPermission(i, type))
754 cellAttributes:kUseBullet
756 [children addObject:item];
758 // If there are additional details, add them below this item.
759 if (!prompt.GetPermissionsDetails(i, type).empty()) {
760 if (prompt.GetIsShowingDetails(
761 ExtensionInstallPrompt::PERMISSIONS_DETAILS, i)) {
763 [self buildItemWithTitle:SysUTF16ToNSString(
764 prompt.GetPermissionsDetails(i, type))
765 cellAttributes:kNoExpandMarker
767 [children addObject:item];
770 // Add a row for the link.
772 [self buildDetailToggleItem:type permissionsDetailIndex:i]];
776 return SysUTF16ToNSString(prompt.GetPermissionsHeading(type));
779 - (void)updateViewFrame:(NSRect)frame {
780 NSWindow* window = [[self view] window];
781 [window setFrame:[window frameRectForContentRect:frame] display:YES];
782 [[self view] setFrame:frame];
788 @implementation DetailToggleHyperlinkButtonCell
790 @synthesize permissionsDetailIndex = permissionsDetailIndex_;
791 @synthesize permissionsDetailType = permissionsDetailType_;
792 @synthesize linkClickedAction = linkClickedAction_;
794 + (BOOL)prefersTrackingUntilMouseUp {
798 - (NSRect)drawingRectForBounds:(NSRect)rect {
799 NSRect rectInset = NSMakeRect(rect.origin.x + kLinkCellPaddingLeft,
801 rect.size.width - kLinkCellPaddingLeft,
803 return [super drawingRectForBounds:rectInset];
806 - (NSUInteger)hitTestForEvent:(NSEvent*)event
807 inRect:(NSRect)cellFrame
808 ofView:(NSView*)controlView {
809 NSUInteger hitTestResult =
810 [super hitTestForEvent:event inRect:cellFrame ofView:controlView];
811 if ((hitTestResult & NSCellHitContentArea) != 0)
812 hitTestResult |= NSCellHitTrackableArea;
813 return hitTestResult;
816 - (void)handleLinkClicked {
817 [NSApp sendAction:linkClickedAction_ to:[self target] from:self];
820 - (BOOL)trackMouse:(NSEvent*)event
821 inRect:(NSRect)cellFrame
822 ofView:(NSView*)controlView
823 untilMouseUp:(BOOL)flag {
825 NSUInteger hitTestResult =
826 [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
827 if ((hitTestResult & NSCellHitContentArea) != 0) {
828 result = [super trackMouse:event
832 event = [NSApp currentEvent];
834 [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
835 if ((hitTestResult & NSCellHitContentArea) != 0)
836 [self handleLinkClicked];
841 - (NSArray*)accessibilityActionNames {
842 return [[super accessibilityActionNames]
843 arrayByAddingObject:NSAccessibilityPressAction];
846 - (void)accessibilityPerformAction:(NSString*)action {
847 if ([action isEqualToString:NSAccessibilityPressAction])
848 [self handleLinkClicked];
850 [super accessibilityPerformAction:action];