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