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. 18 const int kMaxMessageWidth = 400; 19 20 // The corner redius of this bubble view. 21 const int kBubbleCornerRadius = 3; 22 23 // The color for the border of this bubble view. 24 const 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. 29 const int kLabelToControlVerticalSpacing = 8; 30 31 // Horizontal spacing between controls that are logically related. 32 const int kRelatedControlHorizontalSpacing = 8; 33 34 // Vertical spacing between controls that are logically related. 35 const int kRelatedControlVerticalSpacing = 8; 36 37 // Vertical spacing between the edge of the window and the 38 // top or bottom of a button. 39 const int kButtonVEdgeMargin = 6; 40 41 // Horizontal spacing between the edge of the window and the 42 // left or right of a button. 43 const int kButtonHEdgeMargin = 7; 44 45 namespace chrome { 46 47 void 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