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