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;
24 @interface ToolbarActionsBarBubbleMac ()
26 // Handles the notification that the window will close.
27 - (void)windowWillClose:(NSNotification*)notification;
29 // Creates and returns an NSAttributed string with the specified size and
31 - (NSAttributedString*)attributedStringWithString:(const base::string16&)string
32 fontSize:(CGFloat)fontSize
33 alignment:(NSTextAlignment)alignment;
35 // Creates an NSTextField with the given string, size, and alignment, and adds
37 - (NSTextField*)addTextFieldWithString:(const base::string16&)string
38 fontSize:(CGFloat)fontSize
39 alignment:(NSTextAlignment)alignment;
41 // Creates an ExtensionMessagebubbleButton the given string id, and adds it to
43 - (NSButton*)addButtonWithString:(const base::string16&)string;
45 // Initializes the bubble's content.
48 // Handles a button being clicked.
49 - (void)onButtonClicked:(id)sender;
53 @implementation ToolbarActionsBarBubbleMac
55 @synthesize actionButton = actionButton_;
56 @synthesize itemList = itemList_;
57 @synthesize dismissButton = dismissButton_;
58 @synthesize learnMoreButton = learnMoreButton_;
60 - (id)initWithParentWindow:(NSWindow*)parentWindow
61 anchorPoint:(NSPoint)anchorPoint
62 delegate:(scoped_ptr<ToolbarActionsBarBubbleDelegate>)
64 base::scoped_nsobject<InfoBubbleWindow> window(
65 [[InfoBubbleWindow alloc]
66 initWithContentRect:ui::kWindowSizeDeterminedLater
67 styleMask:NSBorderlessWindowMask
68 backing:NSBackingStoreBuffered
70 if ((self = [super initWithWindow:window
71 parentWindow:parentWindow
72 anchoredAt:anchorPoint])) {
74 [window setInfoBubbleCanBecomeKeyWindow:NO];
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];
92 + (void)setAnimationEnabledForTesting:(BOOL)enabled {
93 g_animations_enabled = enabled;
96 - (IBAction)showWindow:(id)sender {
97 delegate_->OnBubbleShown();
98 [super showWindow:sender];
101 // Private /////////////////////////////////////////////////////////////////////
103 - (void)windowWillClose:(NSNotification*)notification {
104 if (!acknowledged_) {
105 delegate_->OnBubbleClosed(
106 ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS);
109 [super windowWillClose:notification];
112 - (NSAttributedString*)attributedStringWithString:(const base::string16&)string
113 fontSize:(CGFloat)fontSize
114 alignment:(NSTextAlignment)alignment {
115 NSString* cocoaString = base::SysUTF16ToNSString(string);
116 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle(
117 [[NSMutableParagraphStyle alloc] init]);
118 [paragraphStyle setAlignment:alignment];
119 NSDictionary* attributes = @{
120 NSFontAttributeName : [NSFont systemFontOfSize:fontSize],
121 NSForegroundColorAttributeName :
122 [NSColor colorWithCalibratedWhite:0.2 alpha:1.0],
123 NSParagraphStyleAttributeName : paragraphStyle.get()
125 return [[[NSAttributedString alloc] initWithString:cocoaString
126 attributes:attributes] autorelease];
129 - (NSTextField*)addTextFieldWithString:(const base::string16&)string
130 fontSize:(CGFloat)fontSize
131 alignment:(NSTextAlignment)alignment {
132 NSAttributedString* attributedString =
133 [self attributedStringWithString:string
135 alignment:alignment];
137 base::scoped_nsobject<NSTextField> textField(
138 [[NSTextField alloc] initWithFrame:NSZeroRect]);
139 [textField setEditable:NO];
140 [textField setBordered:NO];
141 [textField setDrawsBackground:NO];
142 [textField setAttributedStringValue:attributedString];
143 [[[self window] contentView] addSubview:textField];
144 [textField sizeToFit];
145 return textField.autorelease();
148 - (NSButton*)addButtonWithString:(const base::string16&)string {
149 NSButton* button = [[NSButton alloc] initWithFrame:NSZeroRect];
150 NSAttributedString* buttonString =
151 [self attributedStringWithString:string
153 alignment:NSCenterTextAlignment];
154 [button setAttributedTitle:buttonString];
155 [button setBezelStyle:NSRoundedBezelStyle];
156 [button setTarget:self];
157 [button setAction:@selector(onButtonClicked:)];
158 [[[self window] contentView] addSubview:button];
164 // First, construct the pieces of the bubble that have a fixed width: the
165 // heading, and the button strip (the learn more link, the action button, and
166 // the dismiss button).
167 NSTextField* heading =
168 [self addTextFieldWithString:delegate_->GetHeadingText()
170 alignment:NSLeftTextAlignment];
171 NSSize headingSize = [heading frame].size;
173 base::string16 learnMore = delegate_->GetLearnMoreButtonText();
174 NSSize learnMoreSize = NSZeroSize;
175 if (!learnMore.empty()) { // The "learn more" link is optional.
176 NSAttributedString* learnMoreString =
177 [self attributedStringWithString:learnMore
179 alignment:NSLeftTextAlignment];
181 [[HyperlinkButtonCell buttonWithString:learnMoreString.string] retain];
182 [learnMoreButton_ setTarget:self];
183 [learnMoreButton_ setAction:@selector(onButtonClicked:)];
184 [[[self window] contentView] addSubview:learnMoreButton_];
185 [learnMoreButton_ sizeToFit];
186 learnMoreSize = NSMakeSize(NSWidth([learnMoreButton_ frame]),
187 NSHeight([learnMoreButton_ frame]));
190 base::string16 cancelStr = delegate_->GetDismissButtonText();
191 NSSize dismissButtonSize = NSZeroSize;
192 if (!cancelStr.empty()) { // A cancel/dismiss button is optional.
193 dismissButton_ = [self addButtonWithString:cancelStr];
195 NSMakeSize(NSWidth([dismissButton_ frame]),
196 NSHeight([dismissButton_ frame]));
199 base::string16 actionStr = delegate_->GetActionButtonText();
200 NSSize actionButtonSize = NSZeroSize;
201 if (!actionStr.empty()) { // The action button is optional.
202 actionButton_ = [self addButtonWithString:actionStr];
204 NSMakeSize(NSWidth([actionButton_ frame]),
205 NSHeight([actionButton_ frame]));
208 DCHECK(actionButton_ || dismissButton_);
209 CGFloat buttonStripHeight =
210 std::max(actionButtonSize.height, dismissButtonSize.height);
212 const CGFloat kButtonPadding = 5.0;
213 CGFloat buttonStripWidth = 0;
215 buttonStripWidth += actionButtonSize.width + kButtonPadding;
217 buttonStripWidth += dismissButtonSize.width + kButtonPadding;
218 if (learnMoreButton_)
219 buttonStripWidth += learnMoreSize.width + kButtonPadding;
221 CGFloat headingWidth = headingSize.width + 50.0;
223 CGFloat windowWidth = std::max(buttonStripWidth, headingWidth);
225 NSTextField* content =
226 [self addTextFieldWithString:delegate_->GetBodyText()
228 alignment:NSLeftTextAlignment];
229 [content setFrame:NSMakeRect(0, 0, windowWidth, 0)];
230 // The content should have the same (max) width as the heading, which means
231 // the text will most likely wrap.
232 NSSize contentSize = NSMakeSize(windowWidth,
233 [GTMUILocalizerAndLayoutTweaker
234 sizeToFitFixedWidthTextField:content]);
236 const CGFloat kItemListIndentation = 10.0;
237 base::string16 itemListStr = delegate_->GetItemListText();
239 if (!itemListStr.empty()) {
241 [self addTextFieldWithString:itemListStr
243 alignment:NSLeftTextAlignment];
244 CGFloat listWidth = windowWidth - kItemListIndentation;
245 [itemList_ setFrame:NSMakeRect(0, 0, listWidth, 0)];
246 itemListSize = NSMakeSize(listWidth,
247 [GTMUILocalizerAndLayoutTweaker
248 sizeToFitFixedWidthTextField:itemList_]);
251 const CGFloat kHorizontalPadding = 15.0;
252 const CGFloat kVerticalPadding = 10.0;
254 // Next, we set frame for all the different pieces of the bubble, from bottom
256 windowWidth += kHorizontalPadding * 2;
257 CGFloat currentHeight = kVerticalPadding;
258 CGFloat currentMaxWidth = windowWidth - kHorizontalPadding;
260 [actionButton_ setFrame:NSMakeRect(
261 currentMaxWidth - actionButtonSize.width,
263 actionButtonSize.width,
264 actionButtonSize.height)];
265 currentMaxWidth -= (actionButtonSize.width + kButtonPadding);
267 if (dismissButton_) {
268 [dismissButton_ setFrame:NSMakeRect(
269 currentMaxWidth - dismissButtonSize.width,
271 dismissButtonSize.width,
272 dismissButtonSize.height)];
273 currentMaxWidth -= (dismissButtonSize.width + kButtonPadding);
275 if (learnMoreButton_) {
276 CGFloat learnMoreHeight =
277 currentHeight + (buttonStripHeight - learnMoreSize.height) / 2.0;
278 [learnMoreButton_ setFrame:NSMakeRect(kHorizontalPadding,
281 learnMoreSize.height)];
283 // Buttons have some inherit padding of their own, so we don't need quite as
285 currentHeight += buttonStripHeight + kVerticalPadding / 2;
288 [itemList_ setFrame:NSMakeRect(kHorizontalPadding + kItemListIndentation,
291 itemListSize.height)];
292 currentHeight += itemListSize.height + kVerticalPadding;
295 [content setFrame:NSMakeRect(kHorizontalPadding,
298 contentSize.height)];
299 currentHeight += contentSize.height + kVerticalPadding;
300 [heading setFrame:NSMakeRect(kHorizontalPadding,
303 headingSize.height)];
305 // Update window frame.
306 NSRect windowFrame = [[self window] frame];
308 NSMakeSize(windowWidth,
309 currentHeight + headingSize.height + kVerticalPadding * 2);
310 // We need to convert the size to be in the window's coordinate system. Since
311 // all we're doing is converting a size, and all views within a window share
312 // the same size metrics, it's okay that the size calculation came from
313 // multiple different views. Pick a view to convert it.
314 windowSize = [heading convertSize:windowSize toView:nil];
315 windowFrame.size = windowSize;
316 [[self window] setFrame:windowFrame display:YES];
319 - (void)onButtonClicked:(id)sender {
322 ToolbarActionsBarBubbleDelegate::CloseAction action =
323 ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE;
324 if (learnMoreButton_ && sender == learnMoreButton_) {
325 action = ToolbarActionsBarBubbleDelegate::CLOSE_LEARN_MORE;
326 } else if (dismissButton_ && sender == dismissButton_) {
327 action = ToolbarActionsBarBubbleDelegate::CLOSE_DISMISS;
329 DCHECK_EQ(sender, actionButton_);
330 action = ToolbarActionsBarBubbleDelegate::CLOSE_EXECUTE;
333 delegate_->OnBubbleClosed(action);