Index: chrome/browser/ui/cocoa/extensions/extension_install_dialog_controller.mm |
diff --git a/chrome/browser/ui/cocoa/extensions/extension_install_dialog_controller.mm b/chrome/browser/ui/cocoa/extensions/extension_install_dialog_controller.mm |
index a13a2d94bc3400e29ee3687f714763b4f6a4b92a..113bbc4e50bb1709b95b4705dfe781537cfe66a4 100644 |
--- a/chrome/browser/ui/cocoa/extensions/extension_install_dialog_controller.mm |
+++ b/chrome/browser/ui/cocoa/extensions/extension_install_dialog_controller.mm |
@@ -29,6 +29,7 @@ using extensions::BundleInstaller; |
- (BOOL)isBundleInstall; |
- (BOOL)isInlineInstall; |
- (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage; |
+- (void)onOutlineViewRowCountDidChange; |
@end |
namespace { |
@@ -41,6 +42,11 @@ const CGFloat kWarningsSeparatorPadding = 14; |
// contents. |
const CGFloat kMaxControlHeight = 400; |
+const NSString* kTitleKey = @"title"; |
+const NSString* kIsGroupItemKey = @"isGroupItem"; |
+const NSString* kChildrenKey = @"children"; |
+const NSString* kCanExpandKey = @"canExpand"; |
+ |
// Adjust the |control|'s height so that its content is not clipped. |
// This also adds the change in height to the |totalOffset| and shifts the |
// control down by that amount. |
@@ -64,12 +70,104 @@ void OffsetControlVerticallyToFitContent(NSControl* control, |
[control setFrameOrigin:origin]; |
} |
+// Gets the desired height of |outlineView|. Simply using the view's frame |
+// doesn't work if an animation is pending. |
+CGFloat GetDesiredOutlineViewHeight(NSOutlineView* outlineView) { |
+ CGFloat height = 0; |
+ for (NSInteger i = 0; i < [outlineView numberOfRows]; ++i) |
+ height += [outlineView rectOfRow:i].size.height; |
+ return height; |
+} |
+ |
+void OffsetOutlineViewVerticallyToFitContent(NSOutlineView* outlineView, |
+ CGFloat* totalOffset) { |
+ NSScrollView* scrollView = [outlineView enclosingScrollView]; |
+ NSRect frame = [scrollView frame]; |
+ CGFloat desiredHeight = GetDesiredOutlineViewHeight(outlineView); |
+ CGFloat offset = desiredHeight - frame.size.height; |
+ frame.size.height += offset; |
+ |
+ *totalOffset += offset; |
+ |
+ // Move the control vertically by the new total offset. |
+ frame.origin.y -= *totalOffset; |
+ [scrollView setFrame:frame]; |
+} |
+ |
void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { |
ExtensionInstallDialogController* controller = |
static_cast<ExtensionInstallDialogController*>(data); |
[controller appendRatingStar:skiaImage]; |
} |
+NSDictionary* BuildItem(NSString* title, BOOL isGroupItem, NSArray* children) { |
+ BOOL canExpand = YES; |
+ if (!children) { |
+ // Add a dummy child even though this is a leaf node. This will cause |
+ // the outline view to show a disclosure triangle for this item. |
+ // This is later overriden in willDisplayOutlineCell: to draw a bullet |
+ // instead. (The bullet could be placed in the title instead but then |
+ // the bullet wouldn't line up with disclosure triangles of sibling nodes.) |
+ children = [NSArray arrayWithObject:[NSDictionary dictionary]]; |
+ canExpand = false; |
+ } |
+ |
+ return [NSDictionary dictionaryWithObjectsAndKeys: |
+ title, kTitleKey, |
+ [NSNumber numberWithBool:isGroupItem], kIsGroupItemKey, |
+ children, kChildrenKey, |
+ [NSNumber numberWithBool:canExpand], kCanExpandKey, |
+ nil]; |
+} |
+ |
+NSDictionary* BuildIssue(const IssueAdviceInfoEntry& issue) { |
+ if (issue.details.empty()) |
+ return BuildItem(SysUTF16ToNSString(issue.description), NO, nil); |
+ |
+ NSMutableArray* details = [NSMutableArray array]; |
+ for (size_t j = 0; j < issue.details.size(); ++j) { |
+ [details addObject:BuildItem( |
+ SysUTF16ToNSString(issue.details[j]), NO, nil)]; |
+ } |
+ return BuildItem(SysUTF16ToNSString(issue.description), NO, details); |
+} |
+ |
+NSArray* BuildWarnings(const ExtensionInstallPrompt::Prompt& prompt) { |
+ NSMutableArray* warnings = [NSMutableArray array]; |
+ |
+ if (prompt.GetPermissionCount() > 0) { |
+ NSMutableArray* children = [NSMutableArray array]; |
+ for (size_t i = 0; i < prompt.GetPermissionCount(); ++i) { |
+ [children addObject:BuildItem( |
+ SysUTF16ToNSString(prompt.GetPermission(i)), NO, nil)]; |
+ } |
+ [warnings addObject:BuildItem( |
+ SysUTF16ToNSString(prompt.GetPermissionsHeading()), YES, children)]; |
+ } |
+ |
+ if (prompt.GetOAuthIssueCount() > 0) { |
+ NSMutableArray* children = [NSMutableArray array]; |
+ for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) |
+ [children addObject:BuildIssue(prompt.GetOAuthIssue(i))]; |
+ [warnings addObject:BuildItem( |
+ SysUTF16ToNSString(prompt.GetOAuthHeading()), YES, children)]; |
+ } |
+ |
+ return warnings; |
+} |
+ |
+void DrawBulletInFrame(NSRect frame) { |
+ NSRect rect; |
+ rect.size.width = std::min(frame.size.width, frame.size.height) * 0.38; |
+ rect.size.height = rect.size.width; |
+ rect.origin.x = frame.origin.x + (frame.size.width - rect.size.width) / 2.0; |
+ rect.origin.y = frame.origin.y + (frame.size.height - rect.size.height) / 2.0; |
+ rect = NSIntegralRect(rect); |
+ |
+ [[NSColor colorWithCalibratedWhite:0.0 alpha:0.42] set]; |
+ [[NSBezierPath bezierPathWithOvalInRect:rect] fill]; |
+} |
+ |
} |
@implementation ExtensionInstallDialogController |
@@ -77,10 +175,9 @@ void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { |
@synthesize iconView = iconView_; |
@synthesize titleField = titleField_; |
@synthesize itemsField = itemsField_; |
-@synthesize subtitleField = subtitleField_; |
-@synthesize warningsField = warningsField_; |
@synthesize cancelButton = cancelButton_; |
@synthesize okButton = okButton_; |
+@synthesize outlineView = outlineView_; |
@synthesize warningsSeparator = warningsSeparator_; |
@synthesize ratingStars = ratingStars_; |
@synthesize ratingCountField = ratingCountField_; |
@@ -91,6 +188,7 @@ void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { |
delegate:(ExtensionInstallPrompt::Delegate*)delegate |
prompt:(const ExtensionInstallPrompt::Prompt&)prompt { |
NSString* nibpath = nil; |
+ warnings_.reset([BuildWarnings(prompt) retain]); |
// We use a different XIB in the case of bundle installs, inline installs or |
// no permission warnings. These are laid out nicely for the data they |
@@ -103,7 +201,8 @@ void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { |
nibpath = [base::mac::FrameworkBundle() |
pathForResource:@"ExtensionInstallPromptInline" |
ofType:@"nib"]; |
- } else if (prompt.GetPermissionCount() == 0) { |
+ } else if (prompt.GetPermissionCount() == 0 && |
+ prompt.GetOAuthIssueCount() == 0) { |
nibpath = [base::mac::FrameworkBundle() |
pathForResource:@"ExtensionInstallPromptNoWarnings" |
ofType:@"nib"]; |
@@ -200,9 +299,6 @@ void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { |
} |
if ([self isBundleInstall]) { |
- [subtitleField_ setStringValue:base::SysUTF16ToNSString( |
- prompt_->GetPermissionsHeading())]; |
- |
// We display the list of extension names as a simple text string, seperated |
// by newlines. |
BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState( |
@@ -221,48 +317,28 @@ void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { |
OffsetControlVerticallyToFitContent(itemsField_, &totalOffset); |
} |
- // If there are any warnings, then we have to do some special layout. |
- if (prompt_->GetPermissionCount() > 0) { |
- [subtitleField_ setStringValue:base::SysUTF16ToNSString( |
- prompt_->GetPermissionsHeading())]; |
- |
- // We display the permission warnings as a simple text string, separated by |
- // newlines. |
- NSMutableString* joinedWarnings = [NSMutableString string]; |
- for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) { |
- if (i > 0) |
- [joinedWarnings appendString:@"\n"]; |
- [joinedWarnings appendString:base::SysUTF16ToNSString( |
- prompt_->GetPermission(i))]; |
+ // If there are any warnings or OAuth issues, then we have to do some special |
+ // layout. |
+ if (prompt_->GetPermissionCount() > 0 || prompt_->GetOAuthIssueCount() > 0) { |
+ NSSize spacing = [outlineView_ intercellSpacing]; |
+ spacing.width += 2; |
+ spacing.height += 2; |
+ [outlineView_ setIntercellSpacing:spacing]; |
+ [[[[outlineView_ tableColumns] objectAtIndex:0] dataCell] setWraps:YES]; |
+ for (id item in warnings_.get()) { |
+ if ([[item objectForKey:kIsGroupItemKey] boolValue]) |
+ [outlineView_ expandItem:item expandChildren:NO]; |
} |
- [warningsField_ setStringValue:joinedWarnings]; |
- |
- // In the store version of the dialog the icon extends past the one-line |
- // version of the permission field. Therefore when increasing the window |
- // size for multi-line permissions we don't have to add the full offset, |
- // only the part that extends past the icon. |
- CGFloat warningsGrowthSlack = 0; |
- if (![self isBundleInstall] && |
- [warningsField_ frame].origin.y > [iconView_ frame].origin.y) { |
- warningsGrowthSlack = |
- [warningsField_ frame].origin.y - [iconView_ frame].origin.y; |
- } |
- |
- // Adjust the controls to fit the permission warnings. |
- OffsetControlVerticallyToFitContent(subtitleField_, &totalOffset); |
- OffsetControlVerticallyToFitContent(warningsField_, &totalOffset); |
- |
- totalOffset = MAX(totalOffset - warningsGrowthSlack, 0); |
+ // Adjust the outline view to fit the warnings. |
+ OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset); |
} else if ([self isInlineInstall] || [self isBundleInstall]) { |
// Inline and bundle installs that don't have a permissions section need to |
// hide controls related to that and shrink the window by the space they |
// take up. |
NSRect hiddenRect = NSUnionRect([warningsSeparator_ frame], |
- [subtitleField_ frame]); |
- hiddenRect = NSUnionRect(hiddenRect, [warningsField_ frame]); |
+ [[outlineView_ enclosingScrollView] frame]); |
[warningsSeparator_ setHidden:YES]; |
- [subtitleField_ setHidden:YES]; |
- [warningsField_ setHidden:YES]; |
+ [[outlineView_ enclosingScrollView] setHidden:YES]; |
totalOffset -= hiddenRect.size.height + kWarningsSeparatorPadding; |
} |
@@ -313,6 +389,127 @@ void AppendRatingStarsShim(const gfx::ImageSkia* skiaImage, void* data) { |
[ratingStars_ addSubview:view]; |
} |
+- (void)onOutlineViewRowCountDidChange { |
+ // Force the outline view to update. |
+ [outlineView_ reloadData]; |
+ |
+ CGFloat totalOffset = 0.0; |
+ OffsetOutlineViewVerticallyToFitContent(outlineView_, &totalOffset); |
+ if (totalOffset) { |
+ NSRect currentRect = [[self window] frame]; |
+ [[self window] setFrame:NSMakeRect(currentRect.origin.x, |
+ currentRect.origin.y - totalOffset, |
+ currentRect.size.width, |
+ currentRect.size.height + totalOffset) |
+ display:YES]; |
+ } |
+} |
+ |
+- (id)outlineView:(NSOutlineView*)outlineView |
+ child:(NSInteger)index |
+ ofItem:(id)item { |
+ if (!item) |
+ return [warnings_ objectAtIndex:index]; |
+ if ([item isKindOfClass:[NSDictionary class]]) |
+ return [[item objectForKey:kChildrenKey] objectAtIndex:index]; |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+- (BOOL)outlineView:(NSOutlineView*)outlineView |
+ isItemExpandable:(id)item { |
+ return [self outlineView:outlineView numberOfChildrenOfItem:item] > 0; |
+} |
+ |
+- (NSInteger)outlineView:(NSOutlineView*)outlineView |
+ numberOfChildrenOfItem:(id)item { |
+ if (!item) |
+ return [warnings_ count]; |
+ if ([item isKindOfClass:[NSDictionary class]]) |
+ return [[item objectForKey:kChildrenKey] count]; |
+ NOTREACHED(); |
+ return 0; |
+} |
+ |
+- (id)outlineView:(NSOutlineView*)outlineView |
+ objectValueForTableColumn:(NSTableColumn *)tableColumn |
+ byItem:(id)item { |
+ if ([item isKindOfClass:[NSDictionary class]]) |
+ return [item objectForKey:kTitleKey]; |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+- (BOOL)outlineView:(NSOutlineView *)outlineView |
+ shouldExpandItem:(id)item { |
+ return [[item objectForKey:kCanExpandKey] boolValue]; |
+} |
+ |
+- (void)outlineViewItemDidExpand:sender { |
+ // Call via run loop to avoid animation glitches. |
+ [self performSelector:@selector(onOutlineViewRowCountDidChange) |
+ withObject:nil |
+ afterDelay:0]; |
+} |
+ |
+- (void)outlineViewItemDidCollapse:sender { |
+ // Call via run loop to avoid animation glitches. |
+ [self performSelector:@selector(onOutlineViewRowCountDidChange) |
+ withObject:nil |
+ afterDelay:0]; |
+} |
+ |
+- (CGFloat)outlineView:(NSOutlineView *)outlineView |
+ heightOfRowByItem:(id)item { |
+ // Prevent reentrancy due to the frameOfCellAtColumn:row: call below. |
+ if (isComputingRowHeight) |
+ return 1; |
+ isComputingRowHeight = YES; |
+ |
+ NSCell* cell = [[[outlineView_ tableColumns] objectAtIndex:0] dataCell]; |
+ [cell setStringValue:[item objectForKey:kTitleKey]]; |
+ NSRect bounds = NSZeroRect; |
+ NSInteger row = [outlineView_ rowForItem:item]; |
+ bounds.size.width = [outlineView_ frameOfCellAtColumn:0 |
+ row:row].size.width; |
+ bounds.size.height = kMaxControlHeight; |
+ |
+ isComputingRowHeight = NO; |
+ return [cell cellSizeForBounds:bounds].height; |
+} |
+ |
+- (BOOL)outlineView:(NSOutlineView*)outlineView |
+ shouldShowOutlineCellForItem:(id)item { |
+ // The top most group header items are always expanded so hide their |
+ // disclosure trianggles. |
+ return ![[item objectForKey:kIsGroupItemKey] boolValue]; |
+} |
+ |
+- (void)outlineView:(NSOutlineView*)outlineView |
+ willDisplayCell:(id)cell |
+ forTableColumn:(NSTableColumn *)tableColumn |
+ item:(id)item { |
+ if ([[item objectForKey:kIsGroupItemKey] boolValue]) |
+ [cell setFont:[NSFont boldSystemFontOfSize:11.0]]; |
+ else |
+ [cell setFont:[NSFont systemFontOfSize:11.0]]; |
+} |
+ |
+- (void)outlineView:(NSOutlineView *)outlineView |
+ willDisplayOutlineCell:(id)cell |
+ forTableColumn:(NSTableColumn *)tableColumn |
+ item:(id)item { |
+ // Replace disclosure triangles with bullet lists for leaf nodes. |
+ if (![[item objectForKey:kCanExpandKey] boolValue]) { |
+ [cell setImagePosition:NSNoImage]; |
+ DrawBulletInFrame([outlineView_ frameOfOutlineCellAtRow: |
+ [outlineView_ rowForItem:item]]); |
+ } else { |
+ // Reset image to default value. |
+ [cell setImagePosition:NSImageOverlaps]; |
+ } |
+} |
+ |
@end // ExtensionInstallDialogController |
void ShowExtensionInstallDialogImpl( |