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