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