Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / confirm_bubble_cocoa.mm
blob27b722a8d32ae8be7d467172c0fa837be79aeb51
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/geometry/point.h"
14 #include "ui/gfx/image/image.h"
16 // The width for the message text. We break lines so the specified message fits
17 // into this width.
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 // Vertical spacing between controls that are logically related.
32 const int kRelatedControlVerticalSpacing = 8;
34 // Vertical spacing between the edge of the window and the
35 // top or bottom of a button.
36 const int kButtonVEdgeMargin = 6;
38 // Horizontal spacing between the edge of the window and the
39 // left or right of a button.
40 const int kButtonHEdgeMargin = 7;
42 namespace chrome {
44 void ShowConfirmBubble(gfx::NativeWindow window,
45                        gfx::NativeView anchor_view,
46                        const gfx::Point& origin,
47                        scoped_ptr<ConfirmBubbleModel> model) {
48   // Create a custom NSViewController that manages a bubble view, and add it to
49   // a child to the specified |anchor_view|. This controller will be
50   // automatically deleted when it loses first-responder status.
51   ConfirmBubbleController* controller =
52       [[ConfirmBubbleController alloc] initWithParent:anchor_view
53                                                origin:origin.ToCGPoint()
54                                                 model:model.Pass()];
55   [anchor_view addSubview:[controller view]
56                positioned:NSWindowAbove
57                relativeTo:nil];
58   [[anchor_view window] makeFirstResponder:[controller view]];
61 }  // namespace chrome
63 // An interface that is derived from NSTextView and does not accept
64 // first-responder status, i.e. a NSTextView-derived class that never becomes
65 // the first responder. When we click a NSTextView object, it becomes the first
66 // responder. Unfortunately, we delete the ConfirmBubbleCocoa object anytime
67 // when it loses first-responder status not to prevent disturbing other
68 // responders.
69 // To prevent text views in this ConfirmBubbleCocoa object from stealing the
70 // first-responder status, we use this view in the ConfirmBubbleCocoa object.
71 @interface ConfirmBubbleTextView : NSTextView
72 @end
74 @implementation ConfirmBubbleTextView
76 - (BOOL)acceptsFirstResponder {
77   return NO;
80 @end
82 // Private Methods
83 @interface ConfirmBubbleCocoa (Private)
84 - (void)performLayout;
85 - (void)closeBubble;
86 @end
88 @implementation ConfirmBubbleCocoa
90 - (id)initWithParent:(NSView*)parent
91           controller:(ConfirmBubbleController*)controller {
92   // Create a NSView and set its width. We will set its position and height
93   // after finish layouting controls in performLayout:.
94   NSRect bounds =
95       NSMakeRect(0, 0, kMaxMessageWidth + kButtonHEdgeMargin * 2, 0);
96   if (self = [super initWithFrame:bounds]) {
97     parent_ = parent;
98     controller_ = controller;
99     [self performLayout];
100   }
101   return self;
104 - (void)drawRect:(NSRect)dirtyRect {
105   // Fill the background rectangle in white and draw its edge.
106   NSRect bounds = [self bounds];
107   bounds = NSInsetRect(bounds, 0.5, 0.5);
108   NSBezierPath* border =
109       [NSBezierPath gtm_bezierPathWithRoundRect:bounds
110                             topLeftCornerRadius:kBubbleCornerRadius
111                            topRightCornerRadius:kBubbleCornerRadius
112                          bottomLeftCornerRadius:kBubbleCornerRadius
113                         bottomRightCornerRadius:kBubbleCornerRadius];
114   [[NSColor colorWithDeviceWhite:1.0f alpha:1.0f] set];
115   [border fill];
116   [[NSColor colorWithDeviceWhite:kBubbleWindowEdge alpha:1.0f] set];
117   [border stroke];
120 // An NSResponder method.
121 - (BOOL)resignFirstResponder {
122   // We do not only accept this request but also close this bubble when we are
123   // asked to resign the first responder. This bubble should be displayed only
124   // while it is the first responder.
125   [self closeBubble];
126   return YES;
129 // NSControl action handlers. These handlers are called when we click a cancel
130 // button, a close icon, and an OK button, respectively.
131 - (IBAction)cancel:(id)sender {
132   [controller_ cancel];
133   [self closeBubble];
136 - (IBAction)close:(id)sender {
137   [self closeBubble];
140 - (IBAction)ok:(id)sender {
141   [controller_ accept];
142   [self closeBubble];
145 // An NSTextViewDelegate method. This function is called when we click a link in
146 // this bubble.
147 - (BOOL)textView:(NSTextView*)textView
148    clickedOnLink:(id)link
149          atIndex:(NSUInteger)charIndex {
150   [controller_ linkClicked];
151   [self closeBubble];
152   return YES;
155 // Initializes controls specified by the ConfirmBubbleModel object and layouts
156 // them into this bubble. This function retrieves text and images from the
157 // ConfirmBubbleModel object (via the ConfirmBubbleController object) and
158 // layouts them programmatically. This function layouts controls in the botom-up
159 // order since NSView uses bottom-up coordinate.
160 - (void)performLayout {
161   NSRect frameRect = [self frame];
163   // Add the ok button and the cancel button to the first row if we have either
164   // of them.
165   CGFloat left = kButtonHEdgeMargin;
166   CGFloat right = NSWidth(frameRect) - kButtonHEdgeMargin;
167   CGFloat bottom = kButtonVEdgeMargin;
168   CGFloat height = 0;
169   if ([controller_ hasOkButton]) {
170     okButton_.reset([[NSButton alloc]
171         initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
172     [okButton_.get() setBezelStyle:NSRoundedBezelStyle];
173     [okButton_.get() setTitle:[controller_ okButtonText]];
174     [okButton_.get() setTarget:self];
175     [okButton_.get() setAction:@selector(ok:)];
176     [okButton_.get() sizeToFit];
177     NSRect okButtonRect = [okButton_.get() frame];
178     right -= NSWidth(okButtonRect);
179     okButtonRect.origin.x = right;
180     [okButton_.get() setFrame:okButtonRect];
181     [self addSubview:okButton_.get()];
182     height = std::max(height, NSHeight(okButtonRect));
183   }
184   if ([controller_ hasCancelButton]) {
185     cancelButton_.reset([[NSButton alloc]
186         initWithFrame:NSMakeRect(0, bottom, 0, 0)]);
187     [cancelButton_.get() setBezelStyle:NSRoundedBezelStyle];
188     [cancelButton_.get() setTitle:[controller_ cancelButtonText]];
189     [cancelButton_.get() setTarget:self];
190     [cancelButton_.get() setAction:@selector(cancel:)];
191     [cancelButton_.get() sizeToFit];
192     NSRect cancelButtonRect = [cancelButton_.get() frame];
193     right -= NSWidth(cancelButtonRect) + kButtonHEdgeMargin;
194     cancelButtonRect.origin.x = right;
195     [cancelButton_.get() setFrame:cancelButtonRect];
196     [self addSubview:cancelButton_.get()];
197     height = std::max(height, NSHeight(cancelButtonRect));
198   }
200   // Add the message label (and the link label) to the second row.
201   left = kButtonHEdgeMargin;
202   right = NSWidth(frameRect);
203   bottom += height + kRelatedControlVerticalSpacing;
204   height = 0;
205   messageLabel_.reset([[ConfirmBubbleTextView alloc]
206       initWithFrame:NSMakeRect(left, bottom, kMaxMessageWidth, 0)]);
207   NSString* messageText = [controller_ messageText];
208   NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
209   base::scoped_nsobject<NSMutableAttributedString> attributedMessage(
210       [[NSMutableAttributedString alloc] initWithString:messageText
211                                              attributes:attributes]);
212   NSString* linkText = [controller_ linkText];
213   if (linkText) {
214     base::scoped_nsobject<NSAttributedString> whiteSpace(
215         [[NSAttributedString alloc] initWithString:@" "]);
216     [attributedMessage.get() appendAttributedString:whiteSpace.get()];
217     [attributes setObject:[NSString string]
218                    forKey:NSLinkAttributeName];
219     base::scoped_nsobject<NSAttributedString> attributedLink(
220         [[NSAttributedString alloc] initWithString:linkText
221                                         attributes:attributes]);
222     [attributedMessage.get() appendAttributedString:attributedLink.get()];
223   }
224   [[messageLabel_.get() textStorage] setAttributedString:attributedMessage];
225   [messageLabel_.get() setHorizontallyResizable:NO];
226   [messageLabel_.get() setVerticallyResizable:YES];
227   [messageLabel_.get() setEditable:NO];
228   [messageLabel_.get() setDrawsBackground:NO];
229   [messageLabel_.get() setDelegate:self];
230   [messageLabel_.get() sizeToFit];
231   height = NSHeight([messageLabel_.get() frame]);
232   [self addSubview:messageLabel_.get()];
234   // Add the icon and the title label to the third row.
235   left = kButtonHEdgeMargin;
236   right = NSWidth(frameRect);
237   bottom += height + kLabelToControlVerticalSpacing;
238   titleLabel_.reset([[NSTextView alloc]
239       initWithFrame:NSMakeRect(left, bottom, right - left, 0)]);
240   [titleLabel_.get() setString:[controller_ title]];
241   [titleLabel_.get() setHorizontallyResizable:NO];
242   [titleLabel_.get() setVerticallyResizable:YES];
243   [titleLabel_.get() setEditable:NO];
244   [titleLabel_.get() setSelectable:NO];
245   [titleLabel_.get() setDrawsBackground:NO];
246   [titleLabel_.get() sizeToFit];
247   [self addSubview:titleLabel_.get()];
248   height = NSHeight([titleLabel_.get() frame]);
250   // Adjust the frame rectangle of this bubble so we can show all controls.
251   NSRect parentRect = [parent_ frame];
252   frameRect.size.height = bottom + height + kButtonVEdgeMargin;
253   frameRect.origin.x = (NSWidth(parentRect) - NSWidth(frameRect)) / 2;
254   frameRect.origin.y = NSHeight(parentRect) - NSHeight(frameRect);
255   [self setFrame:frameRect];
258 // Closes this bubble and releases all resources. This function just puts the
259 // owner ConfirmBubbleController object to the current autorelease pool. (This
260 // view will be deleted when the owner object is deleted.)
261 - (void)closeBubble {
262   [self removeFromSuperview];
263   [controller_ autorelease];
264   parent_ = nil;
265   controller_ = nil;
268 @end
270 @implementation ConfirmBubbleCocoa (ExposedForUnitTesting)
272 - (void)clickOk {
273   [self ok:self];
276 - (void)clickCancel {
277   [self cancel:self];
280 - (void)clickLink {
281   [self textView:messageLabel_.get() clickedOnLink:nil atIndex:0];
284 @end