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