• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5#import "chrome/browser/ui/cocoa/confirm_bubble_cocoa.h"
6
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"
15
16// The width for the message text. We break lines so the specified message fits
17// into this width.
18const int kMaxMessageWidth = 400;
19
20// The corner redius of this bubble view.
21const int kBubbleCornerRadius = 3;
22
23// The color for the border of this bubble view.
24const float kBubbleWindowEdge = 0.7f;
25
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.
29const int kLabelToControlVerticalSpacing = 8;
30
31// Horizontal spacing between controls that are logically related.
32const int kRelatedControlHorizontalSpacing = 8;
33
34// Vertical spacing between controls that are logically related.
35const int kRelatedControlVerticalSpacing = 8;
36
37// Vertical spacing between the edge of the window and the
38// top or bottom of a button.
39const int kButtonVEdgeMargin = 6;
40
41// Horizontal spacing between the edge of the window and the
42// left or right of a button.
43const int kButtonHEdgeMargin = 7;
44
45namespace chrome {
46
47void 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()
56                                                model:model];
57  [view addSubview:[controller view]
58        positioned:NSWindowAbove
59        relativeTo:nil];
60  [[view window] makeFirstResponder:[controller view]];
61}
62
63}  // namespace chrome
64
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
70// responders.
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
74@end
75
76@implementation ConfirmBubbleTextView
77
78- (BOOL)acceptsFirstResponder {
79  return NO;
80}
81
82@end
83
84// Private Methods
85@interface ConfirmBubbleCocoa (Private)
86- (void)performLayout;
87- (void)closeBubble;
88@end
89
90@implementation ConfirmBubbleCocoa
91
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:.
96  NSRect bounds =
97      NSMakeRect(0, 0, kMaxMessageWidth + kButtonHEdgeMargin * 2, 0);
98  if (self = [super initWithFrame:bounds]) {
99    parent_ = parent;
100    controller_ = controller;
101    [self performLayout];
102  }
103  return self;
104}
105
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];
117  [border fill];
118  [[NSColor colorWithDeviceWhite:kBubbleWindowEdge alpha:1.0f] set];
119  [border stroke];
120}
121
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.
127  [self closeBubble];
128  return YES;
129}
130
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];
135  [self closeBubble];
136}
137
138- (IBAction)close:(id)sender {
139  [self closeBubble];
140}
141
142- (IBAction)ok:(id)sender {
143  [controller_ accept];
144  [self closeBubble];
145}
146
147// An NSTextViewDelegate method. This function is called when we click a link in
148// this bubble.
149- (BOOL)textView:(NSTextView*)textView
150   clickedOnLink:(id)link
151         atIndex:(NSUInteger)charIndex {
152  [controller_ linkClicked];
153  [self closeBubble];
154  return YES;
155}
156
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];
164
165  // Add the ok button and the cancel button to the first row if we have either
166  // of them.
167  CGFloat left = kButtonHEdgeMargin;
168  CGFloat right = NSWidth(frameRect) - kButtonHEdgeMargin;
169  CGFloat bottom = kButtonVEdgeMargin;
170  CGFloat height = 0;
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));
185  }
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));
200  }
201
202  // Add the message label (and the link label) to the second row.
203  left = kButtonHEdgeMargin;
204  right = NSWidth(frameRect);
205  bottom += height + kRelatedControlVerticalSpacing;
206  height = 0;
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];
215  if (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()];
225  }
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()];
235
236  // Add the icon and the title label to the third row.
237  left = kButtonHEdgeMargin;
238  right = NSWidth(frameRect);
239  bottom += height + kLabelToControlVerticalSpacing;
240  height = 0;
241  NSImage* iconImage = [controller_ icon];
242  if (iconImage) {
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]));
249  }
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]));
261
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];
268}
269
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];
276  parent_ = nil;
277  controller_ = nil;
278}
279
280@end
281
282@implementation ConfirmBubbleCocoa (ExposedForUnitTesting)
283
284- (void)clickOk {
285  [self ok:self];
286}
287
288- (void)clickCancel {
289  [self cancel:self];
290}
291
292- (void)clickLink {
293  [self textView:messageLabel_.get() clickedOnLink:nil atIndex:0];
294}
295
296@end
297