// 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_constants.h" #include "content/public/browser/page_navigator.h" #include "extensions/common/extension.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(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 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 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 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( [[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