Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / extension_install_view_controller.mm
blob6654389b31e7d075878f753b21a903517b726380
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;
36 namespace {
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 {
41   kBoldText                = 1 << 0,
42   kNoExpandMarker          = 1 << 1,
43   kUseBullet               = 1 << 2,
44   kAutoExpandCell          = 1 << 3,
45   kUseCustomLinkCell       = 1 << 4,
46   kCanExpand               = 1 << 5,
49 typedef NSUInteger CellAttributes;
51 }  // namespace.
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
66 // returned.
67 - (NSString*)
68 appendPermissionsForPrompt:(const ExtensionInstallPrompt::Prompt&)prompt
69                   withType:(ExtensionInstallPrompt::PermissionsType)type
70                   children:(NSMutableArray*)children;
71 - (void)updateViewFrame:(NSRect)frame;
72 @end
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;
85 @end
87 namespace {
89 // Padding above the warnings separator, we must also subtract this when hiding
90 // it.
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
97 // contents.
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;
145   }
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) {
163   CGFloat height = 0;
164   for (NSInteger i = 0; i < [outline_view numberOfRows]; ++i)
165     height += NSHeight([outline_view rectOfRow:i]);
166   return height;
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) {
193   NSRect rect;
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;
208 }  // namespace
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";
240   } else {
241     nibName = @"ExtensionInstallPrompt";
242   }
244   if ((self = [super initWithNibName:nibName
245                               bundle:base::mac::FrameworkBundle()])) {
246     profile_ = profile;
247     navigator_ = navigator;
248     delegate_ = delegate;
249     prompt_ = prompt;
250     warnings_.reset([[self buildWarnings:*prompt] retain]);
251   }
252   return self;
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);
260   if (navigator_) {
261     navigator_->OpenURL(params);
262   } else {
263     chrome::ScopedTabbedBrowserDisplayer displayer(
264         profile_, chrome::GetActiveDesktop());
265     displayer.browser()->OpenURL(params);
266   }
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())];
283   NSRect okButtonRect;
284   base::string16 acceptButtonLabel = prompt_->GetAcceptButtonLabel();
285   if (!acceptButtonLabel.empty()) {
286     [okButton_ setTitle:base::SysUTF16ToNSString(acceptButtonLabel)];
287   } else {
288     [okButton_ removeFromSuperview];
289     okButtonRect = [okButton_ frame];
290     okButton_ = nil;
291   }
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())];
303   }
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];
325   }
327   // Resize |okButton_| and |cancelButton_| to fit the button labels, but keep
328   // them right-aligned.
329   NSSize buttonDelta;
330   if (okButton_) {
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)];
337     }
338   } else {
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];
344   }
345   buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
346   if (buttonDelta.width) {
347     [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame],
348                                          -buttonDelta.width, 0)];
349   }
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);
358   }
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];
364     spacing.width += 2;
365     spacing.height += 2;
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;
382   }
384   // If necessary, adjust the window size.
385   if (totalOffset) {
386     NSRect currentRect = [[self view] bounds];
387     currentRect.size.height += totalOffset;
388     [self updateViewFrame:currentRect];
389   }
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]);
412   }
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);
425   if (totalOffset) {
426     NSRect currentRect = [[self view] bounds];
427     currentRect.size.height += totalOffset;
428     [self updateViewFrame:currentRect];
429   }
432 - (id)outlineView:(NSOutlineView*)outlineView
433             child:(NSInteger)index
434            ofItem:(id)item {
435   if (!item)
436     return [warnings_ objectAtIndex:index];
437   if ([item isKindOfClass:[NSDictionary class]])
438     return [[item objectForKey:kChildrenKey] objectAtIndex:index];
439   NOTREACHED();
440   return nil;
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 {
450   if (!item)
451     return [warnings_ count];
453   if ([item isKindOfClass:[NSDictionary class]])
454     return [[item objectForKey:kChildrenKey] count];
456   NOTREACHED();
457   return 0;
460 - (id)outlineView:(NSOutlineView*)outlineView
461     objectValueForTableColumn:(NSTableColumn *)tableColumn
462                        byItem:(id)item {
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)
474              withObject:nil
475              afterDelay:0];
478 - (void)outlineViewItemDidCollapse:sender {
479   // Call via run loop to avoid animation glitches.
480   [self performSelector:@selector(onOutlineViewRowCountDidChange)
481              withObject:nil
482              afterDelay:0];
485 - (CGFloat)outlineView:(NSOutlineView *)outlineView
486      heightOfRowByItem:(id)item {
487   // Prevent reentrancy due to the frameOfCellAtColumn:row: call below.
488   if (isComputingRowHeight_)
489     return 1;
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
510                item:(id)item {
511   return HasAttribute(item, kUseCustomLinkCell);
514 - (void)outlineView:(NSOutlineView*)outlineView
515     willDisplayCell:(id)cell
516      forTableColumn:(NSTableColumn *)tableColumn
517                item:(id)item {
518   if (HasAttribute(item, kBoldText))
519     [cell setFont:[NSFont boldSystemFontOfSize:12.0]];
520   else
521     [cell setFont:[NSFont systemFontOfSize:12.0]];
524 - (void)outlineView:(NSOutlineView *)outlineView
525     willDisplayOutlineCell:(id)cell
526             forTableColumn:(NSTableColumn *)tableColumn
527                       item:(id)item {
528   if (HasAttribute(item, kNoExpandMarker)) {
529     [cell setImagePosition:NSNoImage];
530     return;
531   }
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]]);
538     return;
539   }
541   // Reset image to default value.
542   [cell setImagePosition:NSImageOverlaps];
545 - (BOOL)outlineView:(NSOutlineView *)outlineView
546    shouldSelectItem:(id)item {
547   return false;
550 - (NSCell*)outlineView:(NSOutlineView*)outlineView
551     dataCellForTableColumn:(NSTableColumn*)tableColumn
552                   item:(id)item {
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];
560     [cell setTextColor:
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)) {
573       [cell setTitle:
574           l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_HIDE_DETAILS)];
575     } else {
576       [cell setTitle:
577           l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_SHOW_DETAILS)];
578     }
580     return cell.autorelease();
581   } else {
582     return [tableColumn dataCell];
583   }
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]];
617   } else {
618     cellAttributes = cellAttributes | kCanExpand;
619   }
621   return @{
622     kTitleKey : title,
623     kChildrenKey : children,
624     kCellAttributesKey : [NSNumber numberWithInt:cellAttributes],
625     kPermissionsDetailIndex : @0ul,
626     kPermissionsDetailType : @0ul,
627   };
630 - (NSDictionary*)buildDetailToggleItem:(size_t)type
631                 permissionsDetailIndex:(size_t)index {
632   return @{
633     kTitleKey : @"",
634     kChildrenKey : @[ @{} ],
635     kCellAttributesKey : [NSNumber numberWithInt:kUseCustomLinkCell |
636                                                  kNoExpandMarker],
637     kPermissionsDetailIndex : [NSNumber numberWithUnsignedInteger:index],
638     kPermissionsDetailType : [NSNumber numberWithUnsignedInteger:type],
639   };
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];
655     heading =
656         [self appendPermissionsForPrompt:prompt
657                                 withType:ExtensionInstallPrompt::PermissionsType
658                                              ::REGULAR_PERMISSIONS
659                                 children:children];
660     withheldHeading =
661         [self appendPermissionsForPrompt:prompt
662                                 withType:ExtensionInstallPrompt::PermissionsType
663                                              ::WITHHELD_PERMISSIONS
664                                 children:withheldChildren];
666     if (!hasPermissions) {
667       [children addObject:
668           [self buildItemWithTitle:
669               l10n_util::GetNSString(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)
670                     cellAttributes:kUseBullet
671                           children:nil]];
672       heading = @"";
673     }
675     if (heading) {
676       [warnings addObject:[self buildItemWithTitle:heading
677                                     cellAttributes:warningCellAttributes
678                                           children:children]];
679     }
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]];
686     }
687   }
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
700                                             children:nil]];
701       }
702     }
704     NSString* title = SysUTF16ToNSString(prompt.GetRetainedFilesHeading());
705     [warnings addObject:[self buildItemWithTitle:title
706                                   cellAttributes:warningCellAttributes
707                                         children:children]];
709     // Add a row for the link.
710     [warnings addObject:
711         [self buildDetailToggleItem:type permissionsDetailIndex:0]];
712   }
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) {
722         NSString* title =
723             SysUTF16ToNSString(prompt.GetRetainedDeviceMessageString(i));
724         [children addObject:[self buildItemWithTitle:title
725                                       cellAttributes:kUseBullet
726                                             children:nil]];
727       }
728     }
730     NSString* title = SysUTF16ToNSString(prompt.GetRetainedDevicesHeading());
731     [warnings addObject:[self buildItemWithTitle:title
732                                   cellAttributes:warningCellAttributes
733                                         children:children]];
735     // Add a row for the link.
736     [warnings
737         addObject:[self buildDetailToggleItem:type permissionsDetailIndex:0]];
738   }
740   return warnings;
743 - (NSString*)
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)
749     return NULL;
751   for (size_t i = 0; i < permissionsCount; ++i) {
752     NSDictionary* item = [self
753         buildItemWithTitle:SysUTF16ToNSString(prompt.GetPermission(i, type))
754             cellAttributes:kUseBullet
755                   children:nil];
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)) {
762         item =
763             [self buildItemWithTitle:SysUTF16ToNSString(
764                                          prompt.GetPermissionsDetails(i, type))
765                       cellAttributes:kNoExpandMarker
766                             children:nil];
767         [children addObject:item];
768       }
770       // Add a row for the link.
771       [children addObject:
772           [self buildDetailToggleItem:type permissionsDetailIndex:i]];
773     }
774   }
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];
785 @end
788 @implementation DetailToggleHyperlinkButtonCell
790 @synthesize permissionsDetailIndex = permissionsDetailIndex_;
791 @synthesize permissionsDetailType = permissionsDetailType_;
792 @synthesize linkClickedAction = linkClickedAction_;
794 + (BOOL)prefersTrackingUntilMouseUp {
795   return YES;
798 - (NSRect)drawingRectForBounds:(NSRect)rect {
799   NSRect rectInset = NSMakeRect(rect.origin.x + kLinkCellPaddingLeft,
800                                 rect.origin.y,
801                                 rect.size.width - kLinkCellPaddingLeft,
802                                 rect.size.height);
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 {
824   BOOL result = YES;
825   NSUInteger hitTestResult =
826       [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
827   if ((hitTestResult & NSCellHitContentArea) != 0) {
828     result = [super trackMouse:event
829                         inRect:cellFrame
830                         ofView:controlView
831                   untilMouseUp:flag];
832     event = [NSApp currentEvent];
833     hitTestResult =
834         [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
835     if ((hitTestResult & NSCellHitContentArea) != 0)
836       [self handleLinkClicked];
837   }
838   return result;
841 - (NSArray*)accessibilityActionNames {
842   return [[super accessibilityActionNames]
843       arrayByAddingObject:NSAccessibilityPressAction];
846 - (void)accessibilityPerformAction:(NSString*)action {
847   if ([action isEqualToString:NSAccessibilityPressAction])
848     [self handleLinkClicked];
849   else
850     [super accessibilityPerformAction:action];
853 @end