| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/extensions/extension_install_view_controller.h" |
| |
| #include "base/auto_reset.h" |
| #include "base/i18n/rtl.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/mac_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/extensions/bundle_installer.h" |
| #import "chrome/browser/ui/chrome_style.h" |
| #include "chrome/common/extensions/extension.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "content/public/browser/page_navigator.h" |
| #include "grit/generated_resources.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
| #import "ui/base/cocoa/controls/hyperlink_button_cell.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| #include "ui/gfx/image/image_skia_util_mac.h" |
| |
| using content::OpenURLParams; |
| using content::Referrer; |
| using extensions::BundleInstaller; |
| |
| namespace { |
| |
| // A collection of attributes (bitmask) for how to draw a cell, the expand |
| // marker and the text in the cell. |
| enum CellAttributesMask { |
| kBoldText = 1 << 0, |
| kNoExpandMarker = 1 << 1, |
| kUseBullet = 1 << 2, |
| kAutoExpandCell = 1 << 3, |
| kUseCustomLinkCell = 1 << 4, |
| kCanExpand = 1 << 5, |
| }; |
| |
| typedef NSUInteger CellAttributes; |
| |
| } // namespace. |
| |
| @interface ExtensionInstallViewController () |
| - (BOOL)isBundleInstall; |
| - (BOOL)isInlineInstall; |
| - (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage; |
| - (void)onOutlineViewRowCountDidChange; |
| - (NSDictionary*)buildItemWithTitle:(NSString*)title |
| cellAttributes:(CellAttributes)cellAttributes |
| children:(NSArray*)children; |
| - (NSDictionary*)buildDetailToggleItem:(size_t)type |
| permissionsDetailIndex:(size_t)index; |
| - (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt; |
| - (void)updateViewFrame:(NSRect)frame; |
| @end |
| |
| @interface DetailToggleHyperlinkButtonCell : HyperlinkButtonCell { |
| NSUInteger permissionsDetailIndex_; |
| ExtensionInstallPrompt::DetailsType permissionsDetailType_; |
| SEL linkClickedAction_; |
| } |
| |
| @property(assign, nonatomic) NSUInteger permissionsDetailIndex; |
| @property(assign, nonatomic) |
| ExtensionInstallPrompt::DetailsType permissionsDetailType; |
| @property(assign, nonatomic) SEL linkClickedAction; |
| |
| @end |
| |
| namespace { |
| |
| // Padding above the warnings separator, we must also subtract this when hiding |
| // it. |
| const CGFloat kWarningsSeparatorPadding = 14; |
| |
| // The left padding for the link cell. |
| const CGFloat kLinkCellPaddingLeft = 3; |
| |
| // Maximum height we will adjust controls to when trying to accomodate their |
| // contents. |
| const CGFloat kMaxControlHeight = 250; |
| |
| NSString* const kTitleKey = @"title"; |
| NSString* const kChildrenKey = @"children"; |
| NSString* const kCellAttributesKey = @"cellAttributes"; |
| NSString* const kPermissionsDetailIndex = @"permissionsDetailIndex"; |
| NSString* const kPermissionsDetailType = @"permissionsDetailType"; |
| |
| // 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. |
| void OffsetControlVerticallyToFitContent(NSControl* control, |
| CGFloat* totalOffset) { |
| // Adjust the control's height so that its content is not clipped. |
| NSRect currentRect = [control frame]; |
| NSRect fitRect = currentRect; |
| fitRect.size.height = kMaxControlHeight; |
| CGFloat desiredHeight = [[control cell] cellSizeForBounds:fitRect].height; |
| CGFloat offset = desiredHeight - NSHeight(currentRect); |
| |
| [control setFrameSize:NSMakeSize(NSWidth(currentRect), |
| NSHeight(currentRect) + offset)]; |
| |
| *totalOffset += offset; |
| |
| // Move the control vertically by the new total offset. |
| NSPoint origin = [control frame].origin; |
| origin.y -= *totalOffset; |
| [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 += NSHeight([outlineView rectOfRow:i]); |
| return height; |
| } |
| |
| void OffsetOutlineViewVerticallyToFitContent(NSOutlineView* outlineView, |
| CGFloat* totalOffset) { |
| NSScrollView* scrollView = [outlineView enclosingScrollView]; |
| NSRect frame = [scrollView frame]; |
| CGFloat desiredHeight = GetDesiredOutlineViewHeight(outlineView); |
| if (desiredHeight > kMaxControlHeight) |
| desiredHeight = kMaxControlHeight; |
| CGFloat offset = desiredHeight - NSHeight(frame); |
| 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) { |
| ExtensionInstallViewController* controller = |
| static_cast<ExtensionInstallViewController*>(data); |
| [controller appendRatingStar:skiaImage]; |
| } |
| |
| void DrawBulletInFrame(NSRect frame) { |
| NSRect rect; |
| rect.size.width = std::min(NSWidth(frame), NSHeight(frame)) * 0.25; |
| rect.size.height = NSWidth(rect); |
| rect.origin.x = frame.origin.x + (NSWidth(frame) - NSWidth(rect)) / 2.0; |
| rect.origin.y = frame.origin.y + (NSHeight(frame) - NSHeight(rect)) / 2.0; |
| rect = NSIntegralRect(rect); |
| |
| [[NSColor colorWithCalibratedWhite:0.0 alpha:0.42] set]; |
| [[NSBezierPath bezierPathWithOvalInRect:rect] fill]; |
| } |
| |
| bool HasAttribute(id item, CellAttributesMask attributeMask) { |
| return [[item objectForKey:kCellAttributesKey] intValue] & attributeMask; |
| } |
| |
| } // namespace |
| |
| @implementation ExtensionInstallViewController |
| |
| @synthesize iconView = iconView_; |
| @synthesize titleField = titleField_; |
| @synthesize itemsField = itemsField_; |
| @synthesize cancelButton = cancelButton_; |
| @synthesize okButton = okButton_; |
| @synthesize outlineView = outlineView_; |
| @synthesize warningsSeparator = warningsSeparator_; |
| @synthesize ratingStars = ratingStars_; |
| @synthesize ratingCountField = ratingCountField_; |
| @synthesize userCountField = userCountField_; |
| @synthesize storeLinkButton = storeLinkButton_; |
| |
| - (id)initWithNavigator:(content::PageNavigator*)navigator |
| delegate:(ExtensionInstallPrompt::Delegate*)delegate |
| prompt:(const ExtensionInstallPrompt::Prompt&)prompt { |
| // 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 |
| // display. |
| NSString* nibName = nil; |
| if (prompt.type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT) { |
| nibName = @"ExtensionInstallPromptBundle"; |
| } else if (prompt.type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT) { |
| nibName = @"ExtensionInstallPromptInline"; |
| } else if (!prompt.ShouldShowPermissions() && |
| prompt.GetOAuthIssueCount() == 0 && |
| prompt.GetRetainedFileCount() == 0) { |
| nibName = @"ExtensionInstallPromptNoWarnings"; |
| } else { |
| nibName = @"ExtensionInstallPrompt"; |
| } |
| |
| if ((self = [super initWithNibName:nibName |
| bundle:base::mac::FrameworkBundle()])) { |
| navigator_ = navigator; |
| delegate_ = delegate; |
| prompt_.reset(new ExtensionInstallPrompt::Prompt(prompt)); |
| warnings_.reset([[self buildWarnings:prompt] retain]); |
| } |
| return self; |
| } |
| |
| - (IBAction)storeLinkClicked:(id)sender { |
| GURL store_url(extension_urls::GetWebstoreItemDetailURLPrefix() + |
| prompt_->extension()->id()); |
| navigator_->OpenURL(OpenURLParams( |
| store_url, Referrer(), NEW_FOREGROUND_TAB, content::PAGE_TRANSITION_LINK, |
| false)); |
| |
| delegate_->InstallUIAbort(/*user_initiated=*/true); |
| } |
| |
| - (IBAction)cancel:(id)sender { |
| delegate_->InstallUIAbort(/*user_initiated=*/true); |
| } |
| |
| - (IBAction)ok:(id)sender { |
| delegate_->InstallUIProceed(); |
| } |
| |
| - (void)awakeFromNib { |
| // Set control labels. |
| [titleField_ setStringValue:base::SysUTF16ToNSString(prompt_->GetHeading())]; |
| NSRect okButtonRect; |
| if (prompt_->HasAcceptButtonLabel()) { |
| [okButton_ setTitle:base::SysUTF16ToNSString( |
| prompt_->GetAcceptButtonLabel())]; |
| } else { |
| [okButton_ removeFromSuperview]; |
| okButtonRect = [okButton_ frame]; |
| okButton_ = nil; |
| } |
| [cancelButton_ setTitle:prompt_->HasAbortButtonLabel() ? |
| base::SysUTF16ToNSString(prompt_->GetAbortButtonLabel()) : |
| l10n_util::GetNSString(IDS_CANCEL)]; |
| if ([self isInlineInstall]) { |
| prompt_->AppendRatingStars(AppendRatingStarsShim, self); |
| [ratingCountField_ setStringValue:base::SysUTF16ToNSString( |
| prompt_->GetRatingCount())]; |
| [userCountField_ setStringValue:base::SysUTF16ToNSString( |
| prompt_->GetUserCount())]; |
| [[storeLinkButton_ cell] setUnderlineOnHover:YES]; |
| [[storeLinkButton_ cell] setTextColor: |
| gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())]; |
| } |
| |
| // The bundle install dialog has no icon. |
| if (![self isBundleInstall]) |
| [iconView_ setImage:prompt_->icon().ToNSImage()]; |
| |
| // The dialog is laid out in the NIB exactly how we want it assuming that |
| // each label fits on one line. However, for each label, we want to allow |
| // wrapping onto multiple lines. So we accumulate an offset by measuring how |
| // big each label wants to be, and comparing it to how big it actually is. |
| // Then we shift each label down and resize by the appropriate amount, then |
| // finally resize the window. |
| CGFloat totalOffset = 0.0; |
| |
| OffsetControlVerticallyToFitContent(titleField_, &totalOffset); |
| |
| // Resize |okButton_| and |cancelButton_| to fit the button labels, but keep |
| // them right-aligned. |
| NSSize buttonDelta; |
| if (okButton_) { |
| buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_]; |
| if (buttonDelta.width) { |
| [okButton_ setFrame:NSOffsetRect([okButton_ frame], |
| -buttonDelta.width, 0)]; |
| [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame], |
| -buttonDelta.width, 0)]; |
| } |
| } else { |
| // Make |cancelButton_| right-aligned in the absence of |okButton_|. |
| NSRect cancelButtonRect = [cancelButton_ frame]; |
| cancelButtonRect.origin.x = |
| NSMaxX(okButtonRect) - NSWidth(cancelButtonRect); |
| [cancelButton_ setFrame:cancelButtonRect]; |
| } |
| buttonDelta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_]; |
| if (buttonDelta.width) { |
| [cancelButton_ setFrame:NSOffsetRect([cancelButton_ frame], |
| -buttonDelta.width, 0)]; |
| } |
| |
| if ([self isBundleInstall]) { |
| // We display the list of extension names as a simple text string, seperated |
| // by newlines. |
| BundleInstaller::ItemList items = prompt_->bundle()->GetItemsWithState( |
| BundleInstaller::Item::STATE_PENDING); |
| |
| NSMutableString* joinedItems = [NSMutableString string]; |
| for (size_t i = 0; i < items.size(); ++i) { |
| if (i > 0) |
| [joinedItems appendString:@"\n"]; |
| [joinedItems appendString:base::SysUTF16ToNSString( |
| items[i].GetNameForDisplay())]; |
| } |
| [itemsField_ setStringValue:joinedItems]; |
| |
| // Adjust the controls to fit the list of extensions. |
| OffsetControlVerticallyToFitContent(itemsField_, &totalOffset); |
| } |
| |
| // If there are any warnings or OAuth issues, then we have to do some special |
| // layout. |
| if (prompt_->ShouldShowPermissions() || prompt_->GetOAuthIssueCount() > 0 || |
| prompt_->GetRetainedFileCount() > 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()) |
| [self expandItemAndChildren:item]; |
| |
| // 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], |
| [[outlineView_ enclosingScrollView] frame]); |
| [warningsSeparator_ setHidden:YES]; |
| [[outlineView_ enclosingScrollView] setHidden:YES]; |
| totalOffset -= NSHeight(hiddenRect) + kWarningsSeparatorPadding; |
| } |
| |
| // If necessary, adjust the window size. |
| if (totalOffset) { |
| NSRect currentRect = [[self view] bounds]; |
| currentRect.size.height += totalOffset; |
| [self updateViewFrame:currentRect]; |
| } |
| } |
| |
| - (BOOL)isBundleInstall { |
| return prompt_->type() == ExtensionInstallPrompt::BUNDLE_INSTALL_PROMPT; |
| } |
| |
| - (BOOL)isInlineInstall { |
| return prompt_->type() == ExtensionInstallPrompt::INLINE_INSTALL_PROMPT; |
| } |
| |
| - (void)appendRatingStar:(const gfx::ImageSkia*)skiaImage { |
| NSImage* image = gfx::NSImageFromImageSkiaWithColorSpace( |
| *skiaImage, base::mac::GetSystemColorSpace()); |
| NSRect frame = NSMakeRect(0, 0, skiaImage->width(), skiaImage->height()); |
| base::scoped_nsobject<NSImageView> view( |
| [[NSImageView alloc] initWithFrame:frame]); |
| [view setImage:image]; |
| |
| // Add this star after all the other ones |
| CGFloat maxStarRight = 0; |
| if ([[ratingStars_ subviews] count]) { |
| maxStarRight = NSMaxX([[[ratingStars_ subviews] lastObject] frame]); |
| } |
| NSRect starBounds = NSMakeRect(maxStarRight, 0, |
| skiaImage->width(), skiaImage->height()); |
| [view setFrame:starBounds]; |
| [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 view] bounds]; |
| currentRect.size.height += totalOffset; |
| [self updateViewFrame:currentRect]; |
| } |
| } |
| |
| - (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 { |
| return [item objectForKey:kTitleKey]; |
| } |
| |
| - (BOOL)outlineView:(NSOutlineView *)outlineView |
| shouldExpandItem:(id)item { |
| return HasAttribute(item, kCanExpand); |
| } |
| |
| - (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; |
| base::AutoReset<BOOL> reset(&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 = NSWidth([outlineView_ frameOfCellAtColumn:0 row:row]); |
| bounds.size.height = kMaxControlHeight; |
| |
| return [cell cellSizeForBounds:bounds].height; |
| } |
| |
| - (BOOL)outlineView:(NSOutlineView*)outlineView |
| shouldShowOutlineCellForItem:(id)item { |
| return !HasAttribute(item, kNoExpandMarker); |
| } |
| |
| - (BOOL)outlineView:(NSOutlineView*)outlineView |
| shouldTrackCell:(NSCell*)cell |
| forTableColumn:(NSTableColumn*)tableColumn |
| item:(id)item { |
| return HasAttribute(item, kUseCustomLinkCell); |
| } |
| |
| - (void)outlineView:(NSOutlineView*)outlineView |
| willDisplayCell:(id)cell |
| forTableColumn:(NSTableColumn *)tableColumn |
| item:(id)item { |
| if (HasAttribute(item, kBoldText)) |
| [cell setFont:[NSFont boldSystemFontOfSize:12.0]]; |
| else |
| [cell setFont:[NSFont systemFontOfSize:12.0]]; |
| } |
| |
| - (void)outlineView:(NSOutlineView *)outlineView |
| willDisplayOutlineCell:(id)cell |
| forTableColumn:(NSTableColumn *)tableColumn |
| item:(id)item { |
| if (HasAttribute(item, kNoExpandMarker)) { |
| [cell setImagePosition:NSNoImage]; |
| return; |
| } |
| |
| if (HasAttribute(item, kUseBullet)) { |
| // Replace disclosure triangles with bullet lists for leaf nodes. |
| [cell setImagePosition:NSNoImage]; |
| DrawBulletInFrame([outlineView_ frameOfOutlineCellAtRow: |
| [outlineView_ rowForItem:item]]); |
| return; |
| } |
| |
| // Reset image to default value. |
| [cell setImagePosition:NSImageOverlaps]; |
| } |
| |
| - (BOOL)outlineView:(NSOutlineView *)outlineView |
| shouldSelectItem:(id)item { |
| return false; |
| } |
| |
| - (NSCell*)outlineView:(NSOutlineView*)outlineView |
| dataCellForTableColumn:(NSTableColumn*)tableColumn |
| item:(id)item { |
| if (HasAttribute(item, kUseCustomLinkCell)) { |
| base::scoped_nsobject<DetailToggleHyperlinkButtonCell> cell( |
| [[DetailToggleHyperlinkButtonCell alloc] initTextCell:@""]); |
| [cell setTarget:self]; |
| [cell setLinkClickedAction:@selector(onToggleDetailsLinkClicked:)]; |
| [cell setAlignment:NSLeftTextAlignment]; |
| [cell setUnderlineOnHover:YES]; |
| [cell setTextColor: |
| gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())]; |
| |
| size_t detailsIndex = |
| [[item objectForKey:kPermissionsDetailIndex] unsignedIntegerValue]; |
| [cell setPermissionsDetailIndex:detailsIndex]; |
| |
| ExtensionInstallPrompt::DetailsType detailsType = |
| static_cast<ExtensionInstallPrompt::DetailsType>( |
| [[item objectForKey:kPermissionsDetailType] unsignedIntegerValue]); |
| [cell setPermissionsDetailType:detailsType]; |
| |
| if (prompt_->GetIsShowingDetails(detailsType, detailsIndex)) { |
| [cell setTitle: |
| l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_HIDE_DETAILS)]; |
| } else { |
| [cell setTitle: |
| l10n_util::GetNSStringWithFixup(IDS_EXTENSIONS_SHOW_DETAILS)]; |
| } |
| |
| return cell.autorelease(); |
| } else { |
| return [tableColumn dataCell]; |
| } |
| } |
| |
| - (void)expandItemAndChildren:(id)item { |
| if (HasAttribute(item, kAutoExpandCell)) |
| [outlineView_ expandItem:item expandChildren:NO]; |
| |
| for (id child in [item objectForKey:kChildrenKey]) |
| [self expandItemAndChildren:child]; |
| } |
| |
| - (void)onToggleDetailsLinkClicked:(id)sender { |
| size_t index = [sender permissionsDetailIndex]; |
| ExtensionInstallPrompt::DetailsType type = [sender permissionsDetailType]; |
| prompt_->SetIsShowingDetails( |
| type, index, !prompt_->GetIsShowingDetails(type, index)); |
| |
| warnings_.reset([[self buildWarnings:*prompt_] retain]); |
| [outlineView_ reloadData]; |
| |
| for (id item in warnings_.get()) |
| [self expandItemAndChildren:item]; |
| } |
| |
| - (NSDictionary*)buildItemWithTitle:(NSString*)title |
| cellAttributes:(CellAttributes)cellAttributes |
| children:(NSArray*)children { |
| if (!children || ([children count] == 0 && cellAttributes & kUseBullet)) { |
| // 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]]; |
| } else { |
| cellAttributes = cellAttributes | kCanExpand; |
| } |
| |
| return @{ |
| kTitleKey : title, |
| kChildrenKey : children, |
| kCellAttributesKey : [NSNumber numberWithInt:cellAttributes], |
| kPermissionsDetailIndex : @0ul, |
| kPermissionsDetailType : @0ul, |
| }; |
| } |
| |
| - (NSDictionary*)buildDetailToggleItem:(size_t)type |
| permissionsDetailIndex:(size_t)index { |
| return @{ |
| kTitleKey : @"", |
| kChildrenKey : @[ @{} ], |
| kCellAttributesKey : [NSNumber numberWithInt:kUseCustomLinkCell | |
| kNoExpandMarker], |
| kPermissionsDetailIndex : [NSNumber numberWithUnsignedInteger:index], |
| kPermissionsDetailType : [NSNumber numberWithUnsignedInteger:type], |
| }; |
| } |
| |
| - (NSArray*)buildWarnings:(const ExtensionInstallPrompt::Prompt&)prompt { |
| NSMutableArray* warnings = [NSMutableArray array]; |
| NSString* heading = nil; |
| |
| ExtensionInstallPrompt::DetailsType type = |
| ExtensionInstallPrompt::PERMISSIONS_DETAILS; |
| if (prompt.ShouldShowPermissions()) { |
| NSMutableArray* children = [NSMutableArray array]; |
| if (prompt.GetPermissionCount() > 0) { |
| for (size_t i = 0; i < prompt.GetPermissionCount(); ++i) { |
| [children addObject: |
| [self buildItemWithTitle:SysUTF16ToNSString(prompt.GetPermission(i)) |
| cellAttributes:kUseBullet |
| children:nil]]; |
| |
| // If there are additional details, add them below this item. |
| if (!prompt.GetPermissionsDetails(i).empty()) { |
| if (prompt.GetIsShowingDetails( |
| ExtensionInstallPrompt::PERMISSIONS_DETAILS, i)) { |
| [children addObject: |
| [self buildItemWithTitle:SysUTF16ToNSString( |
| prompt.GetPermissionsDetails(i)) |
| cellAttributes:kNoExpandMarker |
| children:nil]]; |
| } |
| |
| // Add a row for the link. |
| [children addObject: |
| [self buildDetailToggleItem:type permissionsDetailIndex:i]]; |
| } |
| } |
| |
| heading = SysUTF16ToNSString(prompt.GetPermissionsHeading()); |
| } else { |
| [children addObject: |
| [self buildItemWithTitle: |
| l10n_util::GetNSString(IDS_EXTENSION_NO_SPECIAL_PERMISSIONS) |
| cellAttributes:kUseBullet |
| children:nil]]; |
| heading = @""; |
| } |
| |
| [warnings addObject:[self |
| buildItemWithTitle:heading |
| cellAttributes:kBoldText | kAutoExpandCell | kNoExpandMarker |
| children:children]]; |
| } |
| |
| if (prompt.GetOAuthIssueCount() > 0) { |
| type = ExtensionInstallPrompt::OAUTH_DETAILS; |
| |
| NSMutableArray* children = [NSMutableArray array]; |
| |
| for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) { |
| NSMutableArray* details = [NSMutableArray array]; |
| const IssueAdviceInfoEntry& issue = prompt.GetOAuthIssue(i); |
| if (!issue.details.empty() && prompt.GetIsShowingDetails(type, i)) { |
| for (size_t j = 0; j < issue.details.size(); ++j) { |
| [details addObject: |
| [self buildItemWithTitle:SysUTF16ToNSString(issue.details[j]) |
| cellAttributes:kNoExpandMarker |
| children:nil]]; |
| } |
| } |
| |
| [children addObject: |
| [self buildItemWithTitle:SysUTF16ToNSString(issue.description) |
| cellAttributes:kUseBullet | kAutoExpandCell |
| children:details]]; |
| |
| if (!issue.details.empty()) { |
| // Add a row for the link. |
| [children addObject: |
| [self buildDetailToggleItem:type permissionsDetailIndex:i]]; |
| } |
| } |
| |
| [warnings addObject: |
| [self buildItemWithTitle:SysUTF16ToNSString(prompt.GetOAuthHeading()) |
| cellAttributes:kBoldText | kAutoExpandCell| kNoExpandMarker |
| children:children]]; |
| } |
| |
| if (prompt.GetRetainedFileCount() > 0) { |
| type = ExtensionInstallPrompt::RETAINED_FILES_DETAILS; |
| |
| NSMutableArray* children = [NSMutableArray array]; |
| |
| if (prompt.GetIsShowingDetails(type, 0)) { |
| for (size_t i = 0; i < prompt.GetRetainedFileCount(); ++i) { |
| [children addObject: |
| [self buildItemWithTitle:SysUTF16ToNSString( |
| prompt.GetRetainedFile(i)) |
| cellAttributes:kUseBullet |
| children:nil]]; |
| } |
| } |
| |
| [warnings addObject: |
| [self buildItemWithTitle:SysUTF16ToNSString( |
| prompt.GetRetainedFilesHeading()) |
| cellAttributes:kBoldText | kAutoExpandCell | kNoExpandMarker |
| children:children]]; |
| |
| // Add a row for the link. |
| [warnings addObject: |
| [self buildDetailToggleItem:type permissionsDetailIndex:0]]; |
| } |
| |
| return warnings; |
| } |
| |
| - (void)updateViewFrame:(NSRect)frame { |
| NSWindow* window = [[self view] window]; |
| [window setFrame:[window frameRectForContentRect:frame] display:YES]; |
| [[self view] setFrame:frame]; |
| } |
| |
| @end |
| |
| |
| @implementation DetailToggleHyperlinkButtonCell |
| |
| @synthesize permissionsDetailIndex = permissionsDetailIndex_; |
| @synthesize permissionsDetailType = permissionsDetailType_; |
| @synthesize linkClickedAction = linkClickedAction_; |
| |
| + (BOOL)prefersTrackingUntilMouseUp { |
| return YES; |
| } |
| |
| - (NSRect)drawingRectForBounds:(NSRect)rect { |
| NSRect rectInset = NSMakeRect(rect.origin.x + kLinkCellPaddingLeft, |
| rect.origin.y, |
| rect.size.width - kLinkCellPaddingLeft, |
| rect.size.height); |
| return [super drawingRectForBounds:rectInset]; |
| } |
| |
| - (NSUInteger)hitTestForEvent:(NSEvent*)event |
| inRect:(NSRect)cellFrame |
| ofView:(NSView*)controlView { |
| NSUInteger hitTestResult = |
| [super hitTestForEvent:event inRect:cellFrame ofView:controlView]; |
| if ((hitTestResult & NSCellHitContentArea) != 0) |
| hitTestResult |= NSCellHitTrackableArea; |
| return hitTestResult; |
| } |
| |
| - (void)handleLinkClicked { |
| [NSApp sendAction:linkClickedAction_ to:[self target] from:self]; |
| } |
| |
| - (BOOL)trackMouse:(NSEvent*)event |
| inRect:(NSRect)cellFrame |
| ofView:(NSView*)controlView |
| untilMouseUp:(BOOL)flag { |
| BOOL result = YES; |
| NSUInteger hitTestResult = |
| [self hitTestForEvent:event inRect:cellFrame ofView:controlView]; |
| if ((hitTestResult & NSCellHitContentArea) != 0) { |
| result = [super trackMouse:event |
| inRect:cellFrame |
| ofView:controlView |
| untilMouseUp:flag]; |
| event = [NSApp currentEvent]; |
| hitTestResult = |
| [self hitTestForEvent:event inRect:cellFrame ofView:controlView]; |
| if ((hitTestResult & NSCellHitContentArea) != 0) |
| [self handleLinkClicked]; |
| } |
| return result; |
| } |
| |
| - (NSArray*)accessibilityActionNames { |
| return [[super accessibilityActionNames] |
| arrayByAddingObject:NSAccessibilityPressAction]; |
| } |
| |
| - (void)accessibilityPerformAction:(NSString*)action { |
| if ([action isEqualToString:NSAccessibilityPressAction]) |
| [self handleLinkClicked]; |
| else |
| [super accessibilityPerformAction:action]; |
| } |
| |
| @end |