Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / extension_install_view_controller.mm
blob75c72aef65c71a7903f3a30431670ac49aa9e0a8
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/extensions/extension_install_view_controller.h"
7 #include "base/auto_reset.h"
8 #include "base/i18n/rtl.h"
9 #include "base/mac/bundle_locations.h"
10 #include "base/mac/mac_util.h"
11 #include "base/strings/string_util.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/extensions/bundle_installer.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/ui/browser.h"
17 #import "chrome/browser/ui/chrome_style.h"
18 #include "chrome/browser/ui/cocoa/extensions/bundle_util.h"
19 #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
20 #include "chrome/common/extensions/extension_constants.h"
21 #include "chrome/grit/generated_resources.h"
22 #include "content/public/browser/page_navigator.h"
23 #include "extensions/common/extension.h"
24 #include "extensions/common/extension_urls.h"
25 #include "skia/ext/skia_utils_mac.h"
26 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
27 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
28 #include "ui/base/l10n/l10n_util.h"
29 #include "ui/base/l10n/l10n_util_mac.h"
30 #include "ui/gfx/image/image_skia_util_mac.h"
32 using content::OpenURLParams;
33 using content::Referrer;
34 using extensions::BundleInstaller;
36 namespace {
38 // A collection of attributes (bitmask) for how to draw a cell, the expand
39 // marker and the text in the cell.
40 enum CellAttributesMask {
41   kBoldText                = 1 << 0,
42   kNoExpandMarker          = 1 << 1,
43   kUseBullet               = 1 << 2,
44   kAutoExpandCell          = 1 << 3,
45   kUseCustomLinkCell       = 1 << 4,
46   kCanExpand               = 1 << 5,
49 typedef NSUInteger CellAttributes;
51 }  // namespace.
53 @interface ExtensionInstallViewController ()
54 - (BOOL)isBundleInstall;
55 - (BOOL)hasWebstoreData;
56 - (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage;
57 - (void)onOutlineViewRowCountDidChange;
58 - (NSDictionary*)buildItemWithTitle:(NSString*)title
59                      cellAttributes:(CellAttributes)cellAttributes
60                            children:(NSArray*)children;
61 - (NSDictionary*)buildDetailToggleItem:(size_t)type
62                  permissionsDetailIndex:(size_t)index;
63 - (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt;
64 // Adds permissions of |type| from |prompt| to |children| and returns the
65 // the appropriate permissions header. If no permissions are found, NULL is
66 // returned.
67 - (NSString*)
68 appendPermissionsForPrompt:(const ExtensionInstallPrompt::Prompt&)prompt
69                   withType:(ExtensionInstallPrompt::PermissionsType)type
70                   children:(NSMutableArray*)children;
71 - (void)updateViewFrame:(NSRect)frame;
72 @end
74 @interface DetailToggleHyperlinkButtonCell : HyperlinkButtonCell {
75   NSUInteger permissionsDetailIndex_;
76   ExtensionInstallPrompt::DetailsType permissionsDetailType_;
77   SEL linkClickedAction_;
80 @property(assign, nonatomic) NSUInteger permissionsDetailIndex;
81 @property(assign, nonatomic)
82     ExtensionInstallPrompt::DetailsType permissionsDetailType;
83 @property(assign, nonatomic) SEL linkClickedAction;
85 @end
87 namespace {
89 // Padding above the warnings separator, we must also subtract this when hiding
90 // it.
91 const CGFloat kWarningsSeparatorPadding = 14;
93 // The left padding for the link cell.
94 const CGFloat kLinkCellPaddingLeft = 3;
96 // Maximum height we will adjust controls to when trying to accomodate their
97 // contents.
98 const CGFloat kMaxControlHeight = 250;
100 NSString* const kTitleKey = @"title";
101 NSString* const kChildrenKey = @"children";
102 NSString* const kCellAttributesKey = @"cellAttributes";
103 NSString* const kPermissionsDetailIndex = @"permissionsDetailIndex";
104 NSString* const kPermissionsDetailType = @"permissionsDetailType";
106 // Computes the |control|'s desired height to fit its contents, constrained to
107 // be kMaxControlHeight at most.
108 CGFloat ComputeDesiredControlHeight(NSControl* control) {
109   NSRect rect = [control frame];
110   rect.size.height = kMaxControlHeight;
111   return [[control cell] cellSizeForBounds:rect].height;
114 // Adjust the |control|'s height so that its content is not clipped.
115 // This also adds the change in height to the |total_offset| and shifts the
116 // control down by that amount.
117 void OffsetControlVerticallyToFitContent(NSControl* control,
118                                          CGFloat* total_offset) {
119   // Adjust the control's height so that its content is not clipped.
120   NSRect current_rect = [control frame];
121   CGFloat desired_height = ComputeDesiredControlHeight(control);
122   CGFloat offset = desired_height - NSHeight(current_rect);
124   [control setFrameSize:NSMakeSize(NSWidth(current_rect),
125                                    NSHeight(current_rect) + offset)];
127   *total_offset += offset;
129   // Move the control vertically by the new total offset.
130   NSPoint origin = [control frame].origin;
131   origin.y -= *total_offset;
132   [control setFrameOrigin:origin];
135 // Adjust the |view|'s height so that its subviews are not clipped.
136 // This also adds the change in height to the |total_offset| and shifts the
137 // control down by that amount.
138 void OffsetViewVerticallyToFitContent(NSView* view, CGFloat* total_offset) {
139   // Adjust the view's height so that its subviews are not clipped.
140   CGFloat desired_height = 0;
141   for (NSView* subview in [view subviews]) {
142     int required_height = NSMaxY([subview frame]);
143     if (required_height > desired_height)
144       desired_height = required_height;
145   }
146   NSRect current_rect = [view frame];
147   CGFloat offset = desired_height - NSHeight(current_rect);
149   [view setFrameSize:NSMakeSize(NSWidth(current_rect),
150                                 NSHeight(current_rect) + offset)];
152   *total_offset += offset;
154   // Move the view vertically by the new total offset.
155   NSPoint origin = [view frame].origin;
156   origin.y -= *total_offset;
157   [view setFrameOrigin:origin];
160 // Gets the desired height of |outline_view|. Simply using the view's frame
161 // doesn't work if an animation is pending.
162 CGFloat GetDesiredOutlineViewHeight(NSOutlineView* outline_view) {
163   CGFloat height = 0;
164   for (NSInteger i = 0; i < [outline_view numberOfRows]; ++i)
165     height += NSHeight([outline_view rectOfRow:i]);
166   return height;
169 void OffsetOutlineViewVerticallyToFitContent(NSOutlineView* outline_view,
170                                              CGFloat* total_offset) {
171   NSScrollView* scroll_view = [outline_view enclosingScrollView];
172   NSRect frame = [scroll_view frame];
173   CGFloat desired_height = GetDesiredOutlineViewHeight(outline_view);
174   if (desired_height > kMaxControlHeight)
175     desired_height = kMaxControlHeight;
176   CGFloat offset = desired_height - NSHeight(frame);
177   frame.size.height += offset;
179   *total_offset += offset;
181   // Move the control vertically by the new total offset.
182   frame.origin.y -= *total_offset;
183   [scroll_view setFrame:frame];
186 void AppendRatingStarsShim(const gfx::ImageSkia* skia_image, void* data) {
187   ExtensionInstallViewController* controller =
188       static_cast<ExtensionInstallViewController*>(data);
189   [controller appendRatingStar:skia_image];
192 void DrawBulletInFrame(NSRect frame) {
193   NSRect rect;
194   rect.size.width = std::min(NSWidth(frame), NSHeight(frame)) * 0.25;
195   rect.size.height = NSWidth(rect);
196   rect.origin.x = frame.origin.x + (NSWidth(frame) - NSWidth(rect)) / 2.0;
197   rect.origin.y = frame.origin.y + (NSHeight(frame) - NSHeight(rect)) / 2.0;
198   rect = NSIntegralRect(rect);
200   [[NSColor colorWithCalibratedWhite:0.0 alpha:0.42] set];
201   [[NSBezierPath bezierPathWithOvalInRect:rect] fill];
204 bool HasAttribute(id item, CellAttributesMask attributeMask) {
205   return [[item objectForKey:kCellAttributesKey] intValue] & attributeMask;
208 }  // namespace
210 @implementation ExtensionInstallViewController
212 @synthesize iconView = iconView_;
213 @synthesize titleField = titleField_;
214 @synthesize itemsField = itemsField_;
215 @synthesize cancelButton = cancelButton_;
216 @synthesize okButton = okButton_;
217 @synthesize outlineView = outlineView_;
218 @synthesize warningsSeparator = warningsSeparator_;
219 @synthesize ratingStars = ratingStars_;
220 @synthesize ratingCountField = ratingCountField_;
221 @synthesize userCountField = userCountField_;
222 @synthesize storeLinkButton = storeLinkButton_;
224 - (id)initWithProfile:(Profile*)profile
225             navigator:(content::PageNavigator*)navigator
226              delegate:(ExtensionInstallPrompt::Delegate*)delegate
227                prompt:(scoped_refptr<ExtensionInstallPrompt::Prompt>)prompt {
228   // We use a different XIB in the case of bundle installs, installs with
229   // webstore data, or no permission warnings. These are laid out nicely for
230   // the data they display.
231   NSString* nibName = nil;
232   if (prompt->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT) {
233     nibName = @"ExtensionInstallPromptBundle";
234   } else if (prompt->has_webstore_data()) {
235     nibName = @"ExtensionInstallPromptWebstoreData";
236   } else if (!prompt->ShouldShowPermissions() &&
237              prompt->GetRetainedFileCount() == 0 &&
238              prompt->GetRetainedDeviceCount() == 0) {
239     nibName = @"ExtensionInstallPromptNoWarnings";
240   } else {
241     nibName = @"ExtensionInstallPrompt";
242   }
244   if ((self = [super initWithNibName:nibName
245                               bundle:base::mac::FrameworkBundle()])) {
246     profile_ = profile;
247     navigator_ = navigator;
248     delegate_ = delegate;
249     prompt_ = prompt;
250     warnings_.reset([[self buildWarnings:*prompt] retain]);
251   }
252   return self;
255 - (IBAction)storeLinkClicked:(id)sender {
256   GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() +
257                  prompt_->extension()->id());
258   OpenURLParams params(store_url, Referrer(), NEW_FOREGROUND_TAB,
259       ui::PAGE_TRANSITION_LINK, false);
260   if (navigator_) {
261     navigator_->OpenURL(params);
262   } else {
263     chrome::ScopedTabbedBrowserDisplayer displayer(
264         profile_, chrome::GetActiveDesktop());
265     displayer.browser()->OpenURL(params);
266   }
268   delegate_->InstallUIAbort(/*user_initiated=*/true);
271 - (IBAction)cancel:(id)sender {
272   delegate_->InstallUIAbort(/*user_initiated=*/true);
275 - (IBAction)ok:(id)sender {
276   delegate_->InstallUIProceed();
279 - (void)awakeFromNib {
280   // Set control labels.
281   [titleField_ setStringValue:base::SysUTF16ToNSString(prompt_->GetHeading())];
282   NSRect okButtonRect;
283   if (prompt_->HasAcceptButtonLabel()) {
284     [okButton_ setTitle:base::SysUTF16ToNSString(
285         prompt_->GetAcceptButtonLabel())];
286   } else {
287     [okButton_ removeFromSuperview];
288     okButtonRect = [okButton_ frame];
289     okButton_ = nil;
290   }
291   [cancelButton_ setTitle:prompt_->HasAbortButtonLabel() ?
292       base::SysUTF16ToNSString(prompt_->GetAbortButtonLabel()) :
293       l10n_util::GetNSString(IDS_CANCEL)];
294   if ([self hasWebstoreData]) {
295     prompt_->AppendRatingStars(AppendRatingStarsShim, self);
296     [ratingCountField_ setStringValue:base::SysUTF16ToNSString(
297         prompt_->GetRatingCount())];
298     [userCountField_ setStringValue:base::SysUTF16ToNSString(
299         prompt_->GetUserCount())];
300     [[storeLinkButton_ cell] setUnderlineOnHover:YES];
301     [[storeLinkButton_ cell] setTextColor:
302         gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())];
303   }
305   [iconView_ setImage:prompt_->icon().ToNSImage()];
307   // The dialog is laid out in the NIB exactly how we want it assuming that
308   // each label fits on one line. However, for each label, we want to allow
309   // wrapping onto multiple lines. So we accumulate an offset by measuring how
310   // big each label wants to be, and comparing it to how big it actually is.
311   // Then we shift each label down and resize by the appropriate amount, then
312   // finally resize the window.
313   CGFloat totalOffset = 0.0;
315   OffsetControlVerticallyToFitContent(titleField_, &totalOffset);
317   // Resize |okButton_| and |cancelButton_| to fit the button labels, but keep
318   // them right-aligned.
319   NSSize buttonDelta;
320   if (okButton_) {
321     buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_];
322     if (buttonDelta.width) {
323       [okButton_ setFrame:NSOffsetRect([okButton_ frame],
324                                        -buttonDelta.width, 0)];
325       [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame],
326                                            -buttonDelta.width, 0)];
327     }
328   } else {
329     // Make |cancelButton_| right-aligned in the absence of |okButton_|.
330     NSRect cancelButtonRect = [cancelButton_ frame];
331     cancelButtonRect.origin.x =
332         NSMaxX(okButtonRect) - NSWidth(cancelButtonRect);
333     [cancelButton_ setFrame:cancelButtonRect];
334   }
335   buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_];
336   if (buttonDelta.width) {
337     [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame],
338                                          -buttonDelta.width, 0)];
339   }
341   if ([self isBundleInstall]) {
342     BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState(
343         BundleInstaller::Item::STATE_PENDING);
344     PopulateBundleItemsList(items, itemsField_);
346     // Adjust the view to fit the list of extensions.
347     OffsetViewVerticallyToFitContent(itemsField_, &totalOffset);
348   }
350   // If there are any warnings, retained devices or retained files, then we
351   // have to do some special layout.
352   if (prompt_->ShouldShowPermissions() || prompt_->GetRetainedFileCount() > 0) {
353     NSSize spacing = [outlineView_ intercellSpacing];
354     spacing.width += 2;
355     spacing.height += 2;
356     [outlineView_ setIntercellSpacing:spacing];
357     [[[[outlineView_ tableColumns] objectAtIndex:0] dataCell] setWraps:YES];
358     for (id item in warnings_.get())
359       [self expandItemAndChildren:item];
361     // Adjust the outline view to fit the warnings.
362     OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset);
363   } else if ([self hasWebstoreData] || [self isBundleInstall]) {
364     // Installs with webstore data and bundle installs that don't have a
365     // permissions section need to hide controls related to that and shrink the
366     // window by the space they take up.
367     NSRect hiddenRect = NSUnionRect([warningsSeparator_ frame],
368                                     [[outlineView_ enclosingScrollView] frame]);
369     [warningsSeparator_ setHidden:YES];
370     [[outlineView_ enclosingScrollView] setHidden:YES];
371     totalOffset -= NSHeight(hiddenRect) + kWarningsSeparatorPadding;
372   }
374   // If necessary, adjust the window size.
375   if (totalOffset) {
376     NSRect currentRect = [[self view] bounds];
377     currentRect.size.height += totalOffset;
378     [self updateViewFrame:currentRect];
379   }
382 - (BOOL)isBundleInstall {
383   return prompt_->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT;
386 - (BOOL)hasWebstoreData {
387   return prompt_->has_webstore_data();
390 - (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage {
391   NSImage* image = gfx::NSImageFromImageSkiaWithColorSpace(
392       *skiaImage, base::mac::GetSystemColorSpace());
393   NSRect frame = NSMakeRect(0, 0, skiaImage->width(), skiaImage->height());
394   base::scoped_nsobject<NSImageView> view(
395       [[NSImageView alloc] initWithFrame:frame]);
396   [view setImage:image];
398   // Add this star after all the other ones
399   CGFloat maxStarRight = 0;
400   if ([[ratingStars_ subviews] count]) {
401     maxStarRight = NSMaxX([[[ratingStars_ subviews] lastObject] frame]);
402   }
403   NSRect starBounds = NSMakeRect(maxStarRight, 0,
404                                  skiaImage->width(), skiaImage->height());
405   [view setFrame:starBounds];
406   [ratingStars_ addSubview:view];
409 - (void)onOutlineViewRowCountDidChange {
410   // Force the outline view to update.
411   [outlineView_ reloadData];
413   CGFloat totalOffset = 0.0;
414   OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset);
415   if (totalOffset) {
416     NSRect currentRect = [[self view] bounds];
417     currentRect.size.height += totalOffset;
418     [self updateViewFrame:currentRect];
419   }
422 - (id)outlineView:(NSOutlineView*)outlineView
423             child:(NSInteger)index
424            ofItem:(id)item {
425   if (!item)
426     return [warnings_ objectAtIndex:index];
427   if ([item isKindOfClass:[NSDictionary class]])
428     return [[item objectForKey:kChildrenKey] objectAtIndex:index];
429   NOTREACHED();
430   return nil;
433 - (BOOL)outlineView:(NSOutlineView*)outlineView
434    isItemExpandable:(id)item {
435   return [self outlineView:outlineView numberOfChildrenOfItem:item] > 0;
438 - (NSInteger)outlineView:(NSOutlineView*)outlineView
439   numberOfChildrenOfItem:(id)item {
440   if (!item)
441     return [warnings_ count];
443   if ([item isKindOfClass:[NSDictionary class]])
444     return [[item objectForKey:kChildrenKey] count];
446   NOTREACHED();
447   return 0;
450 - (id)outlineView:(NSOutlineView*)outlineView
451     objectValueForTableColumn:(NSTableColumn *)tableColumn
452                        byItem:(id)item {
453   return [item objectForKey:kTitleKey];
456 - (BOOL)outlineView:(NSOutlineView *)outlineView
457    shouldExpandItem:(id)item {
458   return HasAttribute(item, kCanExpand);
461 - (void)outlineViewItemDidExpand:sender {
462   // Call via run loop to avoid animation glitches.
463   [self performSelector:@selector(onOutlineViewRowCountDidChange)
464              withObject:nil
465              afterDelay:0];
468 - (void)outlineViewItemDidCollapse:sender {
469   // Call via run loop to avoid animation glitches.
470   [self performSelector:@selector(onOutlineViewRowCountDidChange)
471              withObject:nil
472              afterDelay:0];
475 - (CGFloat)outlineView:(NSOutlineView *)outlineView
476      heightOfRowByItem:(id)item {
477   // Prevent reentrancy due to the frameOfCellAtColumn:row: call below.
478   if (isComputingRowHeight_)
479     return 1;
480   base::AutoReset<BOOL> reset(&isComputingRowHeight_, YES);
482   NSCell* cell = [[[outlineView_ tableColumns] objectAtIndex:0] dataCell];
483   [cell setStringValue:[item objectForKey:kTitleKey]];
484   NSRect bounds = NSZeroRect;
485   NSInteger row = [outlineView_ rowForItem:item];
486   bounds.size.width = NSWidth([outlineView_ frameOfCellAtColumn:0 row:row]);
487   bounds.size.height = kMaxControlHeight;
489   return [cell cellSizeForBounds:bounds].height;
492 - (BOOL)outlineView:(NSOutlineView*)outlineView
493     shouldShowOutlineCellForItem:(id)item {
494   return !HasAttribute(item, kNoExpandMarker);
497 - (BOOL)outlineView:(NSOutlineView*)outlineView
498     shouldTrackCell:(NSCell*)cell
499      forTableColumn:(NSTableColumn*)tableColumn
500                item:(id)item {
501   return HasAttribute(item, kUseCustomLinkCell);
504 - (void)outlineView:(NSOutlineView*)outlineView
505     willDisplayCell:(id)cell
506      forTableColumn:(NSTableColumn *)tableColumn
507                item:(id)item {
508   if (HasAttribute(item, kBoldText))
509     [cell setFont:[NSFont boldSystemFontOfSize:12.0]];
510   else
511     [cell setFont:[NSFont systemFontOfSize:12.0]];
514 - (void)outlineView:(NSOutlineView *)outlineView
515     willDisplayOutlineCell:(id)cell
516             forTableColumn:(NSTableColumn *)tableColumn
517                       item:(id)item {
518   if (HasAttribute(item, kNoExpandMarker)) {
519     [cell setImagePosition:NSNoImage];
520     return;
521   }
523   if (HasAttribute(item, kUseBullet)) {
524     // Replace disclosure triangles with bullet lists for leaf nodes.
525     [cell setImagePosition:NSNoImage];
526     DrawBulletInFrame([outlineView_ frameOfOutlineCellAtRow:
527         [outlineView_ rowForItem:item]]);
528     return;
529   }
531   // Reset image to default value.
532   [cell setImagePosition:NSImageOverlaps];
535 - (BOOL)outlineView:(NSOutlineView *)outlineView
536    shouldSelectItem:(id)item {
537   return false;
540 - (NSCell*)outlineView:(NSOutlineView*)outlineView
541     dataCellForTableColumn:(NSTableColumn*)tableColumn
542                   item:(id)item {
543   if (HasAttribute(item, kUseCustomLinkCell)) {
544     base::scoped_nsobject<DetailToggleHyperlinkButtonCell> cell(
545         [[DetailToggleHyperlinkButtonCell alloc] initTextCell:@""]);
546     [cell setTarget:self];
547     [cell setLinkClickedAction:@selector(onToggleDetailsLinkClicked:)];
548     [cell setAlignment:NSLeftTextAlignment];
549     [cell setUnderlineOnHover:YES];
550     [cell setTextColor:
551         gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())];
553     size_t detailsIndex =
554         [[item objectForKey:kPermissionsDetailIndex] unsignedIntegerValue];
555     [cell setPermissionsDetailIndex:detailsIndex];
557     ExtensionInstallPrompt::DetailsType detailsType =
558         static_cast<ExtensionInstallPrompt::DetailsType>(
559             [[item objectForKey:kPermissionsDetailType] unsignedIntegerValue]);
560     [cell setPermissionsDetailType:detailsType];
562     if (prompt_->GetIsShowingDetails(detailsType, detailsIndex)) {
563       [cell setTitle:
564           l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_HIDE_DETAILS)];
565     } else {
566       [cell setTitle:
567           l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_SHOW_DETAILS)];
568     }
570     return cell.autorelease();
571   } else {
572     return [tableColumn dataCell];
573   }
576 - (void)expandItemAndChildren:(id)item {
577   if (HasAttribute(item, kAutoExpandCell))
578     [outlineView_ expandItem:item expandChildren:NO];
580   for (id child in [item objectForKey:kChildrenKey])
581     [self expandItemAndChildren:child];
584 - (void)onToggleDetailsLinkClicked:(id)sender {
585   size_t index = [sender permissionsDetailIndex];
586   ExtensionInstallPrompt::DetailsType type = [sender permissionsDetailType];
587   prompt_->SetIsShowingDetails(
588       type, index, !prompt_->GetIsShowingDetails(type, index));
590   warnings_.reset([[self buildWarnings:*prompt_] retain]);
591   [outlineView_ reloadData];
593   for (id item in warnings_.get())
594     [self expandItemAndChildren:item];
597 - (NSDictionary*)buildItemWithTitle:(NSString*)title
598                      cellAttributes:(CellAttributes)cellAttributes
599                            children:(NSArray*)children {
600   if (!children || ([children count] == 0 && cellAttributes & kUseBullet)) {
601     // Add a dummy child even though this is a leaf node. This will cause
602     // the outline view to show a disclosure triangle for this item.
603     // This is later overriden in willDisplayOutlineCell: to draw a bullet
604     // instead. (The bullet could be placed in the title instead but then
605     // the bullet wouldn't line up with disclosure triangles of sibling nodes.)
606     children = [NSArray arrayWithObject:[NSDictionary dictionary]];
607   } else {
608     cellAttributes = cellAttributes | kCanExpand;
609   }
611   return @{
612     kTitleKey : title,
613     kChildrenKey : children,
614     kCellAttributesKey : [NSNumber numberWithInt:cellAttributes],
615     kPermissionsDetailIndex : @0ul,
616     kPermissionsDetailType : @0ul,
617   };
620 - (NSDictionary*)buildDetailToggleItem:(size_t)type
621                 permissionsDetailIndex:(size_t)index {
622   return @{
623     kTitleKey : @"",
624     kChildrenKey : @[ @{} ],
625     kCellAttributesKey : [NSNumber numberWithInt:kUseCustomLinkCell |
626                                                  kNoExpandMarker],
627     kPermissionsDetailIndex : [NSNumber numberWithUnsignedInteger:index],
628     kPermissionsDetailType : [NSNumber numberWithUnsignedInteger:type],
629   };
632 - (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt {
633   NSMutableArray* warnings = [NSMutableArray array];
634   NSString* heading = nil;
635   NSString* withheldHeading = nil;
637   bool hasPermissions = prompt.GetPermissionCount(
638       ExtensionInstallPrompt::PermissionsType::ALL_PERMISSIONS);
639   CellAttributes warningCellAttributes =
640       kBoldText | kAutoExpandCell | kNoExpandMarker;
641   if (prompt.ShouldShowPermissions()) {
642     NSMutableArray* children = [NSMutableArray array];
643     NSMutableArray* withheldChildren = [NSMutableArray array];
645     heading =
646         [self appendPermissionsForPrompt:prompt
647                                 withType:ExtensionInstallPrompt::PermissionsType
648                                              ::REGULAR_PERMISSIONS
649                                 children:children];
650     withheldHeading =
651         [self appendPermissionsForPrompt:prompt
652                                 withType:ExtensionInstallPrompt::PermissionsType
653                                              ::WITHHELD_PERMISSIONS
654                                 children:withheldChildren];
656     if (!hasPermissions) {
657       [children addObject:
658           [self buildItemWithTitle:
659               l10n_util::GetNSString(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS)
660                     cellAttributes:kUseBullet
661                           children:nil]];
662       heading = @"";
663     }
665     if (heading) {
666       [warnings addObject:[self buildItemWithTitle:heading
667                                     cellAttributes:warningCellAttributes
668                                           children:children]];
669     }
671     // Add withheld permissions to the prompt if they exist.
672     if (withheldHeading) {
673       [warnings addObject:[self buildItemWithTitle:withheldHeading
674                                     cellAttributes:warningCellAttributes
675                                           children:withheldChildren]];
676     }
677   }
679   if (prompt.GetRetainedFileCount() > 0) {
680     const ExtensionInstallPrompt::DetailsType type =
681         ExtensionInstallPrompt::RETAINED_FILES_DETAILS;
683     NSMutableArray* children = [NSMutableArray array];
685     if (prompt.GetIsShowingDetails(type, 0)) {
686       for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i) {
687         NSString* title = SysUTF16ToNSString(prompt.GetRetainedFile(i));
688         [children addObject:[self buildItemWithTitle:title
689                                       cellAttributes:kUseBullet
690                                             children:nil]];
691       }
692     }
694     NSString* title = SysUTF16ToNSString(prompt.GetRetainedFilesHeading());
695     [warnings addObject:[self buildItemWithTitle:title
696                                   cellAttributes:warningCellAttributes
697                                         children:children]];
699     // Add a row for the link.
700     [warnings addObject:
701         [self buildDetailToggleItem:type permissionsDetailIndex:0]];
702   }
704   if (prompt.GetRetainedDeviceCount() > 0) {
705     const ExtensionInstallPrompt::DetailsType type =
706         ExtensionInstallPrompt::RETAINED_DEVICES_DETAILS;
708     NSMutableArray* children = [NSMutableArray array];
710     if (prompt.GetIsShowingDetails(type, 0)) {
711       for (size_t i = 0; i < prompt.GetRetainedDeviceCount(); ++i) {
712         NSString* title =
713             SysUTF16ToNSString(prompt.GetRetainedDeviceMessageString(i));
714         [children addObject:[self buildItemWithTitle:title
715                                       cellAttributes:kUseBullet
716                                             children:nil]];
717       }
718     }
720     NSString* title = SysUTF16ToNSString(prompt.GetRetainedDevicesHeading());
721     [warnings addObject:[self buildItemWithTitle:title
722                                   cellAttributes:warningCellAttributes
723                                         children:children]];
725     // Add a row for the link.
726     [warnings
727         addObject:[self buildDetailToggleItem:type permissionsDetailIndex:0]];
728   }
730   return warnings;
733 - (NSString*)
734 appendPermissionsForPrompt:(const ExtensionInstallPrompt::Prompt&)prompt
735                  withType:(ExtensionInstallPrompt::PermissionsType)type
736                  children:(NSMutableArray*)children {
737   size_t permissionsCount = prompt.GetPermissionCount(type);
738   if (permissionsCount == 0)
739     return NULL;
741   for (size_t i = 0; i < permissionsCount; ++i) {
742     NSDictionary* item = [self
743         buildItemWithTitle:SysUTF16ToNSString(prompt.GetPermission(i, type))
744             cellAttributes:kUseBullet
745                   children:nil];
746     [children addObject:item];
748     // If there are additional details, add them below this item.
749     if (!prompt.GetPermissionsDetails(i, type).empty()) {
750       if (prompt.GetIsShowingDetails(
751               ExtensionInstallPrompt::PERMISSIONS_DETAILS, i)) {
752         item =
753             [self buildItemWithTitle:SysUTF16ToNSString(
754                                          prompt.GetPermissionsDetails(i, type))
755                       cellAttributes:kNoExpandMarker
756                             children:nil];
757         [children addObject:item];
758       }
760       // Add a row for the link.
761       [children addObject:
762           [self buildDetailToggleItem:type permissionsDetailIndex:i]];
763     }
764   }
766   return SysUTF16ToNSString(prompt.GetPermissionsHeading(type));
769 - (void)updateViewFrame:(NSRect)frame {
770   NSWindow* window = [[self view] window];
771   [window setFrame:[window frameRectForContentRect:frame] display:YES];
772   [[self view] setFrame:frame];
775 @end
778 @implementation DetailToggleHyperlinkButtonCell
780 @synthesize permissionsDetailIndex = permissionsDetailIndex_;
781 @synthesize permissionsDetailType = permissionsDetailType_;
782 @synthesize linkClickedAction = linkClickedAction_;
784 + (BOOL)prefersTrackingUntilMouseUp {
785   return YES;
788 - (NSRect)drawingRectForBounds:(NSRect)rect {
789   NSRect rectInset = NSMakeRect(rect.origin.x + kLinkCellPaddingLeft,
790                                 rect.origin.y,
791                                 rect.size.width - kLinkCellPaddingLeft,
792                                 rect.size.height);
793   return [super drawingRectForBounds:rectInset];
796 - (NSUInteger)hitTestForEvent:(NSEvent*)event
797                        inRect:(NSRect)cellFrame
798                        ofView:(NSView*)controlView {
799   NSUInteger hitTestResult =
800       [super hitTestForEvent:event inRect:cellFrame ofView:controlView];
801   if ((hitTestResult & NSCellHitContentArea) != 0)
802     hitTestResult |= NSCellHitTrackableArea;
803   return hitTestResult;
806 - (void)handleLinkClicked {
807   [NSApp sendAction:linkClickedAction_ to:[self target] from:self];
810 - (BOOL)trackMouse:(NSEvent*)event
811             inRect:(NSRect)cellFrame
812             ofView:(NSView*)controlView
813       untilMouseUp:(BOOL)flag {
814   BOOL result = YES;
815   NSUInteger hitTestResult =
816       [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
817   if ((hitTestResult & NSCellHitContentArea) != 0) {
818     result = [super trackMouse:event
819                         inRect:cellFrame
820                         ofView:controlView
821                   untilMouseUp:flag];
822     event = [NSApp currentEvent];
823     hitTestResult =
824         [self hitTestForEvent:event inRect:cellFrame ofView:controlView];
825     if ((hitTestResult & NSCellHitContentArea) != 0)
826       [self handleLinkClicked];
827   }
828   return result;
831 - (NSArray*)accessibilityActionNames {
832   return [[super accessibilityActionNames]
833       arrayByAddingObject:NSAccessibilityPressAction];
836 - (void)accessibilityPerformAction:(NSString*)action {
837   if ([action isEqualToString:NSAccessibilityPressAction])
838     [self handleLinkClicked];
839   else
840     [super accessibilityPerformAction:action];
843 @end