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::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