1 // Copyright 2015 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/toolbar_actions_bar_bubble_mac.h"
7 #include "base/mac/foundation_util.h"
8 #include "base/strings/sys_string_conversions.h"
9 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
10 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
11 #include "chrome/browser/ui/toolbar/toolbar_actions_bar_bubble_delegate.h"
12 #include "skia/ext/skia_utils_mac.h"
13 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
14 #include "third_party/skia/include/core/SkColor.h"
15 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
16 #import "ui/base/cocoa/hover_button.h"
17 #import "ui/base/cocoa/window_size_constants.h"
18 #include "ui/native_theme/native_theme.h"
21 BOOL g_animations_enabled = false;
22 CGFloat kMinWidth = 320.0;
25 @interface ToolbarActionsBarBubbleMac ()
27 // Handles the notification that the window will close.
28 - (void)windowWillClose:(NSNotification*)notification;
30 // Creates and returns an NSAttributed string with the specified size and
32 - (NSAttributedString*)attributedStringWithString:(const base::string16&)string
33 fontSize:(CGFloat)fontSize
34 alignment:(NSTextAlignment)alignment;
36 // Creates an NSTextField with the given string, size, and alignment, and adds
38 - (NSTextField*)addTextFieldWithString:(const base::string16&)string
39 fontSize:(CGFloat)fontSize
40 alignment:(NSTextAlignment)alignment;
42 // Creates an ExtensionMessagebubbleButton the given string id, and adds it to
44 - (NSButton*)addButtonWithString:(const base::string16&)string;
46 // Initializes the bubble's content.
49 // Handles a button being clicked.
50 - (void)onButtonClicked:(id)sender;
54 @implementation ToolbarActionsBarBubbleMac
56 @synthesize actionButton = actionButton_;
57 @synthesize itemList = itemList_;
58 @synthesize dismissButton = dismissButton_;
59 @synthesize learnMoreButton = learnMoreButton_;
61 - (id)initWithParentWindow:(NSWindow*)parentWindow
62 anchorPoint:(NSPoint)anchorPoint
63 delegate:(scoped_ptr<ToolbarActionsBarBubbleDelegate>)
65 base::scoped_nsobject<InfoBubbleWindow> window(
66 [[InfoBubbleWindow alloc]
67 initWithContentRect:ui::kWindowSizeDeterminedLater
68 styleMask:NSBorderlessWindowMask
69 backing:NSBackingStoreBuffered
71 if ((self = [super initWithWindow:window
72 parentWindow:parentWindow
73 anchoredAt:anchorPoint])) {
75 delegate_ = delegate.Pass();
77 ui::NativeTheme* nativeTheme = ui::NativeTheme::instance();
78 [[self bubble] setAlignment:info_bubble::kAlignArrowToAnchor];
79 [[self bubble] setArrowLocation:info_bubble::kTopRight];
80 [[self bubble] setBackgroundColor:
81 gfx::SkColorToCalibratedNSColor(nativeTheme->GetSystemColor(
82 ui::NativeTheme::kColorId_DialogBackground))];
84 if (!g_animations_enabled)
85 [window setAllowedAnimations:info_bubble::kAnimateNone];
89 [[self window] makeFirstResponder:
90 (actionButton_ ? actionButton_ : dismissButton_)];
95 + (void)setAnimationEnabledForTesting:(BOOL)enabled {
96 g_animations_enabled = enabled;
99 - (IBAction)showWindow:(id)sender {
100 delegate_->OnBubbleShown();
101 [super showWindow:sender];
104 // Private /////////////////////////////////////////////////////////////////////
106 - (void)windowWillClose:(NSNotification*)notification {
107 if (!acknowledged_) {
108 delegate_->OnBubbleClosed(
109 ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS);
112 [super windowWillClose:notification];
115 - (NSAttributedString*)attributedStringWithString:(const base::string16&)string
116 fontSize:(CGFloat)fontSize
117 alignment:(NSTextAlignment)alignment {
118 NSString* cocoaString = base::SysUTF16ToNSString(string);
119 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
120 [[NSMutableParagraphStyle alloc] init]);
121 [paragraphStyle setAlignment:alignment];
122 NSDictionary* attributes = @{
123 NSFontAttributeName : [NSFont systemFontOfSize:fontSize],
124 NSForegroundColorAttributeName :
125 [NSColor colorWithCalibratedWhite:0.2 alpha:1.0],
126 NSParagraphStyleAttributeName : paragraphStyle.get()
128 return [[[NSAttributedString alloc] initWithString:cocoaString
129 attributes:attributes] autorelease];
132 - (NSTextField*)addTextFieldWithString:(const base::string16&)string
133 fontSize:(CGFloat)fontSize
134 alignment:(NSTextAlignment)alignment {
135 NSAttributedString* attributedString =
136 [self attributedStringWithString:string
138 alignment:alignment];
140 base::scoped_nsobject<NSTextField> textField(
141 [[NSTextField alloc] initWithFrame:NSZeroRect]);
142 [textField setEditable:NO];
143 [textField setBordered:NO];
144 [textField setDrawsBackground:NO];
145 [textField setAttributedStringValue:attributedString];
146 [[[self window] contentView] addSubview:textField];
147 [textField sizeToFit];
148 return textField.autorelease();
151 - (NSButton*)addButtonWithString:(const base::string16&)string {
152 NSButton* button = [[NSButton alloc] initWithFrame:NSZeroRect];
153 NSAttributedString* buttonString =
154 [self attributedStringWithString:string
156 alignment:NSCenterTextAlignment];
157 [button setAttributedTitle:buttonString];
158 [button setBezelStyle:NSRoundedBezelStyle];
159 [button setTarget:self];
160 [button setAction:@selector(onButtonClicked:)];
161 [[[self window] contentView] addSubview:button];
167 // First, construct the pieces of the bubble that have a fixed width: the
168 // heading, and the button strip (the learn more link, the action button, and
169 // the dismiss button).
170 NSTextField* heading =
171 [self addTextFieldWithString:delegate_->GetHeadingText()
173 alignment:NSLeftTextAlignment];
174 NSSize headingSize = [heading frame].size;
176 base::string16 learnMore = delegate_->GetLearnMoreButtonText();
177 NSSize learnMoreSize = NSZeroSize;
178 if (!learnMore.empty()) { // The "learn more" link is optional.
179 NSAttributedString* learnMoreString =
180 [self attributedStringWithString:learnMore
182 alignment:NSLeftTextAlignment];
184 [[HyperlinkButtonCell buttonWithString:learnMoreString.string] retain];
185 [learnMoreButton_ setTarget:self];
186 [learnMoreButton_ setAction:@selector(onButtonClicked:)];
187 [[[self window] contentView] addSubview:learnMoreButton_];
188 [learnMoreButton_ sizeToFit];
189 learnMoreSize = NSMakeSize(NSWidth([learnMoreButton_ frame]),
190 NSHeight([learnMoreButton_ frame]));
193 base::string16 cancelStr = delegate_->GetDismissButtonText();
194 NSSize dismissButtonSize = NSZeroSize;
195 if (!cancelStr.empty()) { // A cancel/dismiss button is optional.
196 dismissButton_ = [self addButtonWithString:cancelStr];
198 NSMakeSize(NSWidth([dismissButton_ frame]),
199 NSHeight([dismissButton_ frame]));
202 base::string16 actionStr = delegate_->GetActionButtonText();
203 NSSize actionButtonSize = NSZeroSize;
204 if (!actionStr.empty()) { // The action button is optional.
205 actionButton_ = [self addButtonWithString:actionStr];
207 NSMakeSize(NSWidth([actionButton_ frame]),
208 NSHeight([actionButton_ frame]));
211 DCHECK(actionButton_ || dismissButton_);
212 CGFloat buttonStripHeight =
213 std::max(actionButtonSize.height, dismissButtonSize.height);
215 const CGFloat kButtonPadding = 5.0;
216 CGFloat buttonStripWidth = 0;
218 buttonStripWidth += actionButtonSize.width + kButtonPadding;
220 buttonStripWidth += dismissButtonSize.width + kButtonPadding;
221 if (learnMoreButton_)
222 buttonStripWidth += learnMoreSize.width + kButtonPadding;
224 CGFloat headingWidth = headingSize.width;
225 CGFloat windowWidth =
226 std::max(std::max(kMinWidth, buttonStripWidth), headingWidth);
228 NSTextField* content =
229 [self addTextFieldWithString:delegate_->GetBodyText()
231 alignment:NSLeftTextAlignment];
232 [content setFrame:NSMakeRect(0, 0, windowWidth, 0)];
233 // The content should have the same (max) width as the heading, which means
234 // the text will most likely wrap.
235 NSSize contentSize = NSMakeSize(windowWidth,
236 [GTMUILocalizerAndLayoutTweaker
237 sizeToFitFixedWidthTextField:content]);
239 const CGFloat kItemListIndentation = 10.0;
240 base::string16 itemListStr = delegate_->GetItemListText();
242 if (!itemListStr.empty()) {
244 [self addTextFieldWithString:itemListStr
246 alignment:NSLeftTextAlignment];
247 CGFloat listWidth = windowWidth - kItemListIndentation;
248 [itemList_ setFrame:NSMakeRect(0, 0, listWidth, 0)];
249 itemListSize = NSMakeSize(listWidth,
250 [GTMUILocalizerAndLayoutTweaker
251 sizeToFitFixedWidthTextField:itemList_]);
254 const CGFloat kHorizontalPadding = 15.0;
255 const CGFloat kVerticalPadding = 10.0;
257 // Next, we set frame for all the different pieces of the bubble, from bottom
259 windowWidth += kHorizontalPadding * 2;
260 CGFloat currentHeight = kVerticalPadding;
261 CGFloat currentMaxWidth = windowWidth - kHorizontalPadding;
263 [actionButton_ setFrame:NSMakeRect(
264 currentMaxWidth - actionButtonSize.width,
266 actionButtonSize.width,
267 actionButtonSize.height)];
268 currentMaxWidth -= (actionButtonSize.width + kButtonPadding);
270 if (dismissButton_) {
271 [dismissButton_ setFrame:NSMakeRect(
272 currentMaxWidth - dismissButtonSize.width,
274 dismissButtonSize.width,
275 dismissButtonSize.height)];
276 currentMaxWidth -= (dismissButtonSize.width + kButtonPadding);
278 if (learnMoreButton_) {
279 CGFloat learnMoreHeight =
280 currentHeight + (buttonStripHeight - learnMoreSize.height) / 2.0;
281 [learnMoreButton_ setFrame:NSMakeRect(kHorizontalPadding,
284 learnMoreSize.height)];
286 // Buttons have some inherit padding of their own, so we don't need quite as
288 currentHeight += buttonStripHeight + kVerticalPadding / 2;
291 [itemList_ setFrame:NSMakeRect(kHorizontalPadding + kItemListIndentation,
294 itemListSize.height)];
295 currentHeight += itemListSize.height + kVerticalPadding;
298 [content setFrame:NSMakeRect(kHorizontalPadding,
301 contentSize.height)];
302 currentHeight += contentSize.height + kVerticalPadding;
303 [heading setFrame:NSMakeRect(kHorizontalPadding,
306 headingSize.height)];
308 // Update window frame.
309 NSRect windowFrame = [[self window] frame];
311 NSMakeSize(windowWidth,
312 currentHeight + headingSize.height + kVerticalPadding * 2);
313 // We need to convert the size to be in the window's coordinate system. Since
314 // all we're doing is converting a size, and all views within a window share
315 // the same size metrics, it's okay that the size calculation came from
316 // multiple different views. Pick a view to convert it.
317 windowSize = [heading convertSize:windowSize toView:nil];
318 windowFrame.size = windowSize;
319 [[self window] setFrame:windowFrame display:YES];
322 - (void)onButtonClicked:(id)sender {
325 ToolbarActionsBarBubbleDelegate::CloseAction action =
326 ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE;
327 if (learnMoreButton_ && sender == learnMoreButton_) {
328 action = ToolbarActionsBarBubbleDelegate::CLOSE_LEARN_MORE;
329 } else if (dismissButton_ && sender == dismissButton_) {
330 action = ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS;
332 DCHECK_EQ(sender, actionButton_);
333 action = ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE;
336 delegate_->OnBubbleClosed(action);