1 // Copyright (c) 2012 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/confirm_bubble_cocoa.h"
7 #include "base/strings/string16.h"
8 #include "chrome/browser/themes/theme_service.h"
9 #import "chrome/browser/ui/cocoa/confirm_bubble_controller.h"
10 #include "chrome/browser/ui/confirm_bubble.h"
11 #include "chrome/browser/ui/confirm_bubble_model.h"
12 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSBezierPath+RoundRect.h"
13 #include "ui/gfx/image/image.h"
14 #include "ui/gfx/point.h"
16 // The width for the message text. We break lines so the specified message fits
18 const int kMaxMessageWidth = 400;
20 // The corner redius of this bubble view.
21 const int kBubbleCornerRadius = 3;
23 // The color for the border of this bubble view.
24 const float kBubbleWindowEdge = 0.7f;
26 // Constants used for layouting controls. These variables are copied from
27 // "ui/views/layout/layout_constants.h".
28 // Vertical spacing between a label and some control.
29 const int kLabelToControlVerticalSpacing = 8;
31 // Horizontal spacing between controls that are logically related.
32 const int kRelatedControlHorizontalSpacing = 8;
34 // Vertical spacing between controls that are logically related.
35 const int kRelatedControlVerticalSpacing = 8;
37 // Vertical spacing between the edge of the window and the
38 // top or bottom of a button.
39 const int kButtonVEdgeMargin = 6;
41 // Horizontal spacing between the edge of the window and the
42 // left or right of a button.
43 const int kButtonHEdgeMargin = 7;
47 void ShowConfirmBubble(gfx::NativeView view,
48 const gfx::Point& origin,
49 ConfirmBubbleModel* model) {
50 // Create a custom NSViewController that manages a bubble view, and add it to
51 // a child to the specified view. This controller will be automatically
52 // deleted when it loses first-responder status.
53 ConfirmBubbleController* controller =
54 [[ConfirmBubbleController alloc] initWithParent:view
55 origin:origin.ToCGPoint()
57 [view addSubview:[controller view]
58 positioned:NSWindowAbove
60 [[view window] makeFirstResponder:[controller view]];
65 // An interface that is derived from NSTextView and does not accept
66 // first-responder status, i.e. a NSTextView-derived class that never becomes
67 // the first responder. When we click a NSTextView object, it becomes the first
68 // responder. Unfortunately, we delete the ConfirmBubbleCocoa object anytime
69 // when it loses first-responder status not to prevent disturbing other
71 // To prevent text views in this ConfirmBubbleCocoa object from stealing the
72 // first-responder status, we use this view in the ConfirmBubbleCocoa object.
73 @interface ConfirmBubbleTextView : NSTextView
76 @implementation ConfirmBubbleTextView
78 - (BOOL)acceptsFirstResponder {
85 @interface ConfirmBubbleCocoa (Private)
86 - (void)performLayout;
90 @implementation ConfirmBubbleCocoa
92 - (id)initWithParent:(NSView*)parent
93 controller:(ConfirmBubbleController*)controller {
94 // Create a NSView and set its width. We will set its position and height
95 // after finish layouting controls in performLayout:.
97 NSMakeRect(0, 0, kMaxMessageWidth + kButtonHEdgeMargin * 2, 0);
98 if (self = [super initWithFrame:bounds]) {
100 controller_ = controller;
101 [self performLayout];
106 - (void)drawRect:(NSRect)dirtyRect {
107 // Fill the background rectangle in white and draw its edge.
108 NSRect bounds = [self bounds];
109 bounds = NSInsetRect(bounds, 0.5, 0.5);
110 NSBezierPath* border =
111 [NSBezierPath gtm_bezierPathWithRoundRect:bounds
112 topLeftCornerRadius:kBubbleCornerRadius
113 topRightCornerRadius:kBubbleCornerRadius
114 bottomLeftCornerRadius:kBubbleCornerRadius
115 bottomRightCornerRadius:kBubbleCornerRadius];
116 [[NSColor colorWithDeviceWhite:1.0f alpha:1.0f] set];
118 [[NSColor colorWithDeviceWhite:kBubbleWindowEdge alpha:1.0f] set];
122 // An NSResponder method.
123 - (BOOL)resignFirstResponder {
124 // We do not only accept this request but also close this bubble when we are
125 // asked to resign the first responder. This bubble should be displayed only
126 // while it is the first responder.
131 // NSControl action handlers. These handlers are called when we click a cancel
132 // button, a close icon, and an OK button, respectively.
133 - (IBAction)cancel:(id)sender {
134 [controller_ cancel];
138 - (IBAction)close:(id)sender {
142 - (IBAction)ok:(id)sender {
143 [controller_ accept];
147 // An NSTextViewDelegate method. This function is called when we click a link in
149 - (BOOL)textView:(NSTextView*)textView
150 clickedOnLink:(id)link
151 atIndex:(NSUInteger)charIndex {
152 [controller_ linkClicked];
157 // Initializes controls specified by the ConfirmBubbleModel object and layouts
158 // them into this bubble. This function retrieves text and images from the
159 // ConfirmBubbleModel object (via the ConfirmBubbleController object) and
160 // layouts them programmatically. This function layouts controls in the botom-up
161 // order since NSView uses bottom-up coordinate.
162 - (void)performLayout {
163 NSRect frameRect = [self frame];
165 // Add the ok button and the cancel button to the first row if we have either
167 CGFloat left = kButtonHEdgeMargin;
168 CGFloat right = NSWidth(frameRect) - kButtonHEdgeMargin;
169 CGFloat bottom = kButtonVEdgeMargin;
171 if ([controller_ hasOkButton]) {
172 okButton_.reset([[NSButton alloc]
173 initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
174 [okButton_.get() setBezelStyle:NSRoundedBezelStyle];
175 [okButton_.get() setTitle:[controller_ okButtonText]];
176 [okButton_.get() setTarget:self];
177 [okButton_.get() setAction:@selector(ok:)];
178 [okButton_.get() sizeToFit];
179 NSRect okButtonRect = [okButton_.get() frame];
180 right -= NSWidth(okButtonRect);
181 okButtonRect.origin.x = right;
182 [okButton_.get() setFrame:okButtonRect];
183 [self addSubview:okButton_.get()];
184 height = std::max(height, NSHeight(okButtonRect));
186 if ([controller_ hasCancelButton]) {
187 cancelButton_.reset([[NSButton alloc]
188 initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
189 [cancelButton_.get() setBezelStyle:NSRoundedBezelStyle];
190 [cancelButton_.get() setTitle:[controller_ cancelButtonText]];
191 [cancelButton_.get() setTarget:self];
192 [cancelButton_.get() setAction:@selector(cancel:)];
193 [cancelButton_.get() sizeToFit];
194 NSRect cancelButtonRect = [cancelButton_.get() frame];
195 right -= NSWidth(cancelButtonRect) + kButtonHEdgeMargin;
196 cancelButtonRect.origin.x = right;
197 [cancelButton_.get() setFrame:cancelButtonRect];
198 [self addSubview:cancelButton_.get()];
199 height = std::max(height, NSHeight(cancelButtonRect));
202 // Add the message label (and the link label) to the second row.
203 left = kButtonHEdgeMargin;
204 right = NSWidth(frameRect);
205 bottom += height + kRelatedControlVerticalSpacing;
207 messageLabel_.reset([[ConfirmBubbleTextView alloc]
208 initWithFrame:NSMakeRect(left, bottom, kMaxMessageWidth, 0)]);
209 NSString* messageText = [controller_ messageText];
210 NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
211 base::scoped_nsobject<NSMutableAttributedString> attributedMessage(
212 [[NSMutableAttributedString alloc] initWithString:messageText
213 attributes:attributes]);
214 NSString* linkText = [controller_ linkText];
216 base::scoped_nsobject<NSAttributedString> whiteSpace(
217 [[NSAttributedString alloc] initWithString:@" "]);
218 [attributedMessage.get() appendAttributedString:whiteSpace.get()];
219 [attributes setObject:[NSString string]
220 forKey:NSLinkAttributeName];
221 base::scoped_nsobject<NSAttributedString> attributedLink(
222 [[NSAttributedString alloc] initWithString:linkText
223 attributes:attributes]);
224 [attributedMessage.get() appendAttributedString:attributedLink.get()];
226 [[messageLabel_.get() textStorage] setAttributedString:attributedMessage];
227 [messageLabel_.get() setHorizontallyResizable:NO];
228 [messageLabel_.get() setVerticallyResizable:YES];
229 [messageLabel_.get() setEditable:NO];
230 [messageLabel_.get() setDrawsBackground:NO];
231 [messageLabel_.get() setDelegate:self];
232 [messageLabel_.get() sizeToFit];
233 height = NSHeight([messageLabel_.get() frame]);
234 [self addSubview:messageLabel_.get()];
236 // Add the icon and the title label to the third row.
237 left = kButtonHEdgeMargin;
238 right = NSWidth(frameRect);
239 bottom += height + kLabelToControlVerticalSpacing;
241 NSImage* iconImage = [controller_ icon];
243 icon_.reset([[NSImageView alloc] initWithFrame:NSMakeRect(
244 left, bottom, [iconImage size].width, [iconImage size].height)]);
245 [icon_.get() setImage:iconImage];
246 [self addSubview:icon_.get()];
247 left += NSWidth([icon_.get() frame]) + kRelatedControlHorizontalSpacing;
248 height = std::max(height, NSHeight([icon_.get() frame]));
250 titleLabel_.reset([[NSTextView alloc]
251 initWithFrame:NSMakeRect(left, bottom, right - left, 0)]);
252 [titleLabel_.get() setString:[controller_ title]];
253 [titleLabel_.get() setHorizontallyResizable:NO];
254 [titleLabel_.get() setVerticallyResizable:YES];
255 [titleLabel_.get() setEditable:NO];
256 [titleLabel_.get() setSelectable:NO];
257 [titleLabel_.get() setDrawsBackground:NO];
258 [titleLabel_.get() sizeToFit];
259 [self addSubview:titleLabel_.get()];
260 height = std::max(height, NSHeight([titleLabel_.get() frame]));
262 // Adjust the frame rectangle of this bubble so we can show all controls.
263 NSRect parentRect = [parent_ frame];
264 frameRect.size.height = bottom + height + kButtonVEdgeMargin;
265 frameRect.origin.x = (NSWidth(parentRect) - NSWidth(frameRect)) / 2;
266 frameRect.origin.y = NSHeight(parentRect) - NSHeight(frameRect);
267 [self setFrame:frameRect];
270 // Closes this bubble and releases all resources. This function just puts the
271 // owner ConfirmBubbleController object to the current autorelease pool. (This
272 // view will be deleted when the owner object is deleted.)
273 - (void)closeBubble {
274 [self removeFromSuperview];
275 [controller_ autorelease];
282 @implementation ConfirmBubbleCocoa (ExposedForUnitTesting)
288 - (void)clickCancel {
293 [self textView:messageLabel_.get() clickedOnLink:nil atIndex:0];