// Copyright 2015 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/toolbar_actions_bar_bubble_mac.h" #include <utility> #include "base/mac/foundation_util.h" #include "base/strings/sys_string_conversions.h" #import "chrome/browser/ui/cocoa/info_bubble_view.h" #import "chrome/browser/ui/cocoa/info_bubble_window.h" #include "chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h" #include "skia/ext/skia_utils_mac.h" #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h" #include "third_party/skia/include/core/SkColor.h" #import "ui/base/cocoa/controls/hyperlink_button_cell.h" #import "ui/base/cocoa/hover_button.h" #import "ui/base/cocoa/window_size_constants.h" #include "ui/native_theme/native_theme.h" #include "ui/native_theme/native_theme_mac.h" namespace { BOOL g_animations_enabled = false; CGFloat kMinWidth = 320.0; } @interface ToolbarActionsBarBubbleMac () // Handles the notification that the window will close. - (void)windowWillClose:(NSNotification*)notification; // Creates and returns an NSAttributed string with the specified size and // alignment. - (NSAttributedString*)attributedStringWithString:(const base::string16&)string fontSize:(CGFloat)fontSize alignment:(NSTextAlignment)alignment; // Creates an NSTextField with the given string, size, and alignment, and adds // it to the window. - (NSTextField*)addTextFieldWithString:(const base::string16&)string fontSize:(CGFloat)fontSize alignment:(NSTextAlignment)alignment; // Creates an ExtensionMessagebubbleButton the given string id, and adds it to // the window. - (NSButton*)addButtonWithString:(const base::string16&)string; // Initializes the bubble's content. - (void)layout; // Handles a button being clicked. - (void)onButtonClicked:(id)sender; @end @implementation ToolbarActionsBarBubbleMac @synthesize actionButton = actionButton_; @synthesize itemList = itemList_; @synthesize dismissButton = dismissButton_; @synthesize learnMoreButton = learnMoreButton_; - (id)initWithParentWindow:(NSWindow*)parentWindow anchorPoint:(NSPoint)anchorPoint delegate:(scoped_ptr<ToolbarActionsBarBubbleDelegate>) delegate { base::scoped_nsobject<InfoBubbleWindow> window( [[InfoBubbleWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]); if ((self = [super initWithWindow:window parentWindow:parentWindow anchoredAt:anchorPoint])) { acknowledged_ = NO; delegate_ = std::move(delegate); ui::NativeTheme* nativeTheme = ui::NativeThemeMac::instance(); [[self bubble] setAlignment:info_bubble::kAlignArrowToAnchor]; [[self bubble] setArrowLocation:info_bubble::kTopRight]; [[self bubble] setBackgroundColor: skia::SkColorToCalibratedNSColor(nativeTheme->GetSystemColor( ui::NativeTheme::kColorId_DialogBackground))]; if (!g_animations_enabled) [window setAllowedAnimations:info_bubble::kAnimateNone]; [self layout]; [[self window] makeFirstResponder: (actionButton_ ? actionButton_ : dismissButton_)]; } return self; } + (void)setAnimationEnabledForTesting:(BOOL)enabled { g_animations_enabled = enabled; } - (IBAction)showWindow:(id)sender { delegate_->OnBubbleShown(); [super showWindow:sender]; } // Private ///////////////////////////////////////////////////////////////////// - (void)windowWillClose:(NSNotification*)notification { if (!acknowledged_) { delegate_->OnBubbleClosed( ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS_DEACTIVATION); acknowledged_ = YES; } [super windowWillClose:notification]; } - (NSAttributedString*)attributedStringWithString:(const base::string16&)string fontSize:(CGFloat)fontSize alignment:(NSTextAlignment)alignment { NSString* cocoaString = base::SysUTF16ToNSString(string); base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle( [[NSMutableParagraphStyle alloc] init]); [paragraphStyle setAlignment:alignment]; NSDictionary* attributes = @{ NSFontAttributeName : [NSFont systemFontOfSize:fontSize], NSForegroundColorAttributeName : [NSColor colorWithCalibratedWhite:0.2 alpha:1.0], NSParagraphStyleAttributeName : paragraphStyle.get() }; return [[[NSAttributedString alloc] initWithString:cocoaString attributes:attributes] autorelease]; } - (NSTextField*)addTextFieldWithString:(const base::string16&)string fontSize:(CGFloat)fontSize alignment:(NSTextAlignment)alignment { NSAttributedString* attributedString = [self attributedStringWithString:string fontSize:fontSize alignment:alignment]; base::scoped_nsobject<NSTextField> textField( [[NSTextField alloc] initWithFrame:NSZeroRect]); [textField setEditable:NO]; [textField setBordered:NO]; [textField setDrawsBackground:NO]; [textField setAttributedStringValue:attributedString]; [[[self window] contentView] addSubview:textField]; [textField sizeToFit]; return textField.autorelease(); } - (NSButton*)addButtonWithString:(const base::string16&)string { NSButton* button = [[NSButton alloc] initWithFrame:NSZeroRect]; NSAttributedString* buttonString = [self attributedStringWithString:string fontSize:13.0 alignment:NSCenterTextAlignment]; [button setAttributedTitle:buttonString]; [button setBezelStyle:NSRoundedBezelStyle]; [button setTarget:self]; [button setAction:@selector(onButtonClicked:)]; [[[self window] contentView] addSubview:button]; [button sizeToFit]; return button; } - (void)layout { // First, construct the pieces of the bubble that have a fixed width: the // heading, and the button strip (the learn more link, the action button, and // the dismiss button). NSTextField* heading = [self addTextFieldWithString:delegate_->GetHeadingText() fontSize:13.0 alignment:NSLeftTextAlignment]; NSSize headingSize = [heading frame].size; base::string16 learnMore = delegate_->GetLearnMoreButtonText(); NSSize learnMoreSize = NSZeroSize; if (!learnMore.empty()) { // The "learn more" link is optional. NSAttributedString* learnMoreString = [self attributedStringWithString:learnMore fontSize:13.0 alignment:NSLeftTextAlignment]; learnMoreButton_ = [[HyperlinkButtonCell buttonWithString:learnMoreString.string] retain]; [learnMoreButton_ setTarget:self]; [learnMoreButton_ setAction:@selector(onButtonClicked:)]; [[[self window] contentView] addSubview:learnMoreButton_]; [learnMoreButton_ sizeToFit]; learnMoreSize = NSMakeSize(NSWidth([learnMoreButton_ frame]), NSHeight([learnMoreButton_ frame])); } base::string16 cancelStr = delegate_->GetDismissButtonText(); NSSize dismissButtonSize = NSZeroSize; if (!cancelStr.empty()) { // A cancel/dismiss button is optional. dismissButton_ = [self addButtonWithString:cancelStr]; dismissButtonSize = NSMakeSize(NSWidth([dismissButton_ frame]), NSHeight([dismissButton_ frame])); } base::string16 actionStr = delegate_->GetActionButtonText(); NSSize actionButtonSize = NSZeroSize; if (!actionStr.empty()) { // The action button is optional. actionButton_ = [self addButtonWithString:actionStr]; actionButtonSize = NSMakeSize(NSWidth([actionButton_ frame]), NSHeight([actionButton_ frame])); } DCHECK(actionButton_ || dismissButton_); CGFloat buttonStripHeight = std::max(actionButtonSize.height, dismissButtonSize.height); const CGFloat kButtonPadding = 5.0; CGFloat buttonStripWidth = 0; if (actionButton_) buttonStripWidth += actionButtonSize.width + kButtonPadding; if (dismissButton_) buttonStripWidth += dismissButtonSize.width + kButtonPadding; if (learnMoreButton_) buttonStripWidth += learnMoreSize.width + kButtonPadding; CGFloat headingWidth = headingSize.width; CGFloat windowWidth = std::max(std::max(kMinWidth, buttonStripWidth), headingWidth); NSTextField* content = [self addTextFieldWithString:delegate_->GetBodyText() fontSize:12.0 alignment:NSLeftTextAlignment]; [content setFrame:NSMakeRect(0, 0, windowWidth, 0)]; // The content should have the same (max) width as the heading, which means // the text will most likely wrap. NSSize contentSize = NSMakeSize(windowWidth, [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:content]); const CGFloat kItemListIndentation = 10.0; base::string16 itemListStr = delegate_->GetItemListText(); NSSize itemListSize; if (!itemListStr.empty()) { itemList_ = [self addTextFieldWithString:itemListStr fontSize:12.0 alignment:NSLeftTextAlignment]; CGFloat listWidth = windowWidth - kItemListIndentation; [itemList_ setFrame:NSMakeRect(0, 0, listWidth, 0)]; itemListSize = NSMakeSize(listWidth, [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:itemList_]); } const CGFloat kHorizontalPadding = 15.0; const CGFloat kVerticalPadding = 10.0; // Next, we set frame for all the different pieces of the bubble, from bottom // to top. windowWidth += kHorizontalPadding * 2; CGFloat currentHeight = kVerticalPadding; CGFloat currentMaxWidth = windowWidth - kHorizontalPadding; if (actionButton_) { [actionButton_ setFrame:NSMakeRect( currentMaxWidth - actionButtonSize.width, currentHeight, actionButtonSize.width, actionButtonSize.height)]; currentMaxWidth -= (actionButtonSize.width + kButtonPadding); } if (dismissButton_) { [dismissButton_ setFrame:NSMakeRect( currentMaxWidth - dismissButtonSize.width, currentHeight, dismissButtonSize.width, dismissButtonSize.height)]; currentMaxWidth -= (dismissButtonSize.width + kButtonPadding); } if (learnMoreButton_) { CGFloat learnMoreHeight = currentHeight + (buttonStripHeight - learnMoreSize.height) / 2.0; [learnMoreButton_ setFrame:NSMakeRect(kHorizontalPadding, learnMoreHeight, learnMoreSize.width, learnMoreSize.height)]; } // Buttons have some inherit padding of their own, so we don't need quite as // much space here. currentHeight += buttonStripHeight + kVerticalPadding / 2; if (itemList_) { [itemList_ setFrame:NSMakeRect(kHorizontalPadding + kItemListIndentation, currentHeight, itemListSize.width, itemListSize.height)]; currentHeight += itemListSize.height + kVerticalPadding; } [content setFrame:NSMakeRect(kHorizontalPadding, currentHeight, contentSize.width, contentSize.height)]; currentHeight += contentSize.height + kVerticalPadding; [heading setFrame:NSMakeRect(kHorizontalPadding, currentHeight, headingSize.width, headingSize.height)]; // Update window frame. NSRect windowFrame = [[self window] frame]; NSSize windowSize = NSMakeSize(windowWidth, currentHeight + headingSize.height + kVerticalPadding * 2); // We need to convert the size to be in the window's coordinate system. Since // all we're doing is converting a size, and all views within a window share // the same size metrics, it's okay that the size calculation came from // multiple different views. Pick a view to convert it. windowSize = [heading convertSize:windowSize toView:nil]; windowFrame.size = windowSize; [[self window] setFrame:windowFrame display:YES]; } - (void)onButtonClicked:(id)sender { if (acknowledged_) return; ToolbarActionsBarBubbleDelegate::CloseAction action = ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE; if (learnMoreButton_ && sender == learnMoreButton_) { action = ToolbarActionsBarBubbleDelegate::CLOSE_LEARN_MORE; } else if (dismissButton_ && sender == dismissButton_) { action = ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS_USER_ACTION; } else { DCHECK_EQ(sender, actionButton_); action = ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE; } acknowledged_ = YES; delegate_->OnBubbleClosed(action); [self close]; } @end