1 // Copyright (c) 2011 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 <Cocoa/Cocoa.h> 6 7 #include "base/logging.h" // for NOTREACHED() 8 #include "base/mac/mac_util.h" 9 #include "base/sys_string_conversions.h" 10 #include "chrome/browser/tab_contents/confirm_infobar_delegate.h" 11 #include "chrome/browser/tab_contents/link_infobar_delegate.h" 12 #import "chrome/browser/ui/cocoa/animatable_view.h" 13 #include "chrome/browser/ui/cocoa/event_utils.h" 14 #include "chrome/browser/ui/cocoa/infobars/infobar.h" 15 #import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h" 16 #import "chrome/browser/ui/cocoa/infobars/infobar_controller.h" 17 #include "skia/ext/skia_utils_mac.h" 18 #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 19 #include "webkit/glue/window_open_disposition.h" 20 21 namespace { 22 // Durations set to match the default SlideAnimation duration. 23 const float kAnimateOpenDuration = 0.12; 24 const float kAnimateCloseDuration = 0.12; 25 } 26 27 // This simple subclass of |NSTextView| just doesn't show the (text) cursor 28 // (|NSTextView| displays the cursor with full keyboard accessibility enabled). 29 @interface InfoBarTextView : NSTextView 30 - (void)fixupCursor; 31 @end 32 33 @implementation InfoBarTextView 34 35 // Never draw the insertion point (otherwise, it shows up without any user 36 // action if full keyboard accessibility is enabled). 37 - (BOOL)shouldDrawInsertionPoint { 38 return NO; 39 } 40 41 - (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange 42 granularity:(NSSelectionGranularity)granularity { 43 // Do not allow selections. 44 return NSMakeRange(0, 0); 45 } 46 47 // Convince NSTextView to not show an I-Beam cursor when the cursor is over the 48 // text view but not over actual text. 49 // 50 // http://www.mail-archive.com/cocoa-dev@lists.apple.com/msg10791.html 51 // "NSTextView sets the cursor over itself dynamically, based on considerations 52 // including the text under the cursor. It does so in -mouseEntered:, 53 // -mouseMoved:, and -cursorUpdate:, so those would be points to consider 54 // overriding." 55 - (void)mouseMoved:(NSEvent*)e { 56 [super mouseMoved:e]; 57 [self fixupCursor]; 58 } 59 60 - (void)mouseEntered:(NSEvent*)e { 61 [super mouseEntered:e]; 62 [self fixupCursor]; 63 } 64 65 - (void)cursorUpdate:(NSEvent*)e { 66 [super cursorUpdate:e]; 67 [self fixupCursor]; 68 } 69 70 - (void)fixupCursor { 71 if ([[NSCursor currentCursor] isEqual:[NSCursor IBeamCursor]]) 72 [[NSCursor arrowCursor] set]; 73 } 74 75 @end 76 77 @interface InfoBarController (PrivateMethods) 78 // Sets |label_| based on |labelPlaceholder_|, sets |labelPlaceholder_| to nil. 79 - (void)initializeLabel; 80 81 // Asks the container controller to remove the infobar for this delegate. This 82 // call will trigger a notification that starts the infobar animating closed. 83 - (void)removeInfoBar; 84 85 // Performs final cleanup after an animation is finished or stopped, including 86 // notifying the InfoBarDelegate that the infobar was closed and removing the 87 // infobar from its container, if necessary. 88 - (void)cleanUpAfterAnimation:(BOOL)finished; 89 90 // Sets the info bar message to the specified |message|, with a hypertext 91 // style link. |link| will be inserted into message at |linkOffset|. 92 - (void)setLabelToMessage:(NSString*)message 93 withLink:(NSString*)link 94 atOffset:(NSUInteger)linkOffset; 95 @end 96 97 @implementation InfoBarController 98 99 @synthesize containerController = containerController_; 100 @synthesize delegate = delegate_; 101 102 - (id)initWithDelegate:(InfoBarDelegate*)delegate { 103 DCHECK(delegate); 104 if ((self = [super initWithNibName:@"InfoBar" 105 bundle:base::mac::MainAppBundle()])) { 106 delegate_ = delegate; 107 } 108 return self; 109 } 110 111 // All infobars have an icon, so we set up the icon in the base class 112 // awakeFromNib. 113 - (void)awakeFromNib { 114 DCHECK(delegate_); 115 if (delegate_->GetIcon()) { 116 [image_ setImage:gfx::SkBitmapToNSImage(*(delegate_->GetIcon()))]; 117 } else { 118 // No icon, remove it from the view and grow the textfield to include the 119 // space. 120 NSRect imageFrame = [image_ frame]; 121 NSRect labelFrame = [labelPlaceholder_ frame]; 122 labelFrame.size.width += NSMinX(imageFrame) - NSMinX(labelFrame); 123 labelFrame.origin.x = imageFrame.origin.x; 124 [image_ removeFromSuperview]; 125 [labelPlaceholder_ setFrame:labelFrame]; 126 } 127 [self initializeLabel]; 128 129 [self addAdditionalControls]; 130 } 131 132 // Called when someone clicks on the embedded link. 133 - (BOOL) textView:(NSTextView*)textView 134 clickedOnLink:(id)link 135 atIndex:(NSUInteger)charIndex { 136 if ([self respondsToSelector:@selector(linkClicked)]) 137 [self performSelector:@selector(linkClicked)]; 138 return YES; 139 } 140 141 // Called when someone clicks on the ok button. 142 - (void)ok:(id)sender { 143 // Subclasses must override this method if they do not hide the ok button. 144 NOTREACHED(); 145 } 146 147 // Called when someone clicks on the cancel button. 148 - (void)cancel:(id)sender { 149 // Subclasses must override this method if they do not hide the cancel button. 150 NOTREACHED(); 151 } 152 153 // Called when someone clicks on the close button. 154 - (void)dismiss:(id)sender { 155 if (delegate_) 156 delegate_->InfoBarDismissed(); 157 158 [self removeInfoBar]; 159 } 160 161 - (AnimatableView*)animatableView { 162 return static_cast<AnimatableView*>([self view]); 163 } 164 165 - (void)open { 166 // Simply reset the frame size to its opened size, forcing a relayout. 167 CGFloat finalHeight = [[self view] frame].size.height; 168 [[self animatableView] setHeight:finalHeight]; 169 } 170 171 - (void)animateOpen { 172 // Force the frame size to be 0 and then start an animation. 173 NSRect frame = [[self view] frame]; 174 CGFloat finalHeight = frame.size.height; 175 frame.size.height = 0; 176 [[self view] setFrame:frame]; 177 [[self animatableView] animateToNewHeight:finalHeight 178 duration:kAnimateOpenDuration]; 179 } 180 181 - (void)close { 182 // Stop any running animations. 183 [[self animatableView] stopAnimation]; 184 infoBarClosing_ = YES; 185 [self cleanUpAfterAnimation:YES]; 186 } 187 188 - (void)animateClosed { 189 // Notify the container of our intentions. 190 [containerController_ willRemoveController:self]; 191 192 // Start animating closed. We will receive a notification when the animation 193 // is done, at which point we can remove our view from the hierarchy and 194 // notify the delegate that the infobar was closed. 195 [[self animatableView] animateToNewHeight:0 duration:kAnimateCloseDuration]; 196 197 // The above call may trigger an animationDidStop: notification for any 198 // currently-running animations, so do not set |infoBarClosing_| until after 199 // starting the animation. 200 infoBarClosing_ = YES; 201 } 202 203 - (void)addAdditionalControls { 204 // Default implementation does nothing. 205 } 206 207 - (void)infobarWillClose { 208 // Default implementation does nothing. 209 } 210 211 - (void)setLabelToMessage:(NSString*)message { 212 NSMutableDictionary* attributes = [NSMutableDictionary dictionary]; 213 NSFont* font = [NSFont labelFontOfSize: 214 [NSFont systemFontSizeForControlSize:NSRegularControlSize]]; 215 [attributes setObject:font 216 forKey:NSFontAttributeName]; 217 [attributes setObject:[NSCursor arrowCursor] 218 forKey:NSCursorAttributeName]; 219 scoped_nsobject<NSAttributedString> attributedString( 220 [[NSAttributedString alloc] initWithString:message 221 attributes:attributes]); 222 [[label_.get() textStorage] setAttributedString:attributedString]; 223 } 224 225 - (void)removeButtons { 226 // Extend the label all the way across. 227 NSRect labelFrame = [label_.get() frame]; 228 labelFrame.size.width = NSMaxX([cancelButton_ frame]) - NSMinX(labelFrame); 229 [okButton_ removeFromSuperview]; 230 [cancelButton_ removeFromSuperview]; 231 [label_.get() setFrame:labelFrame]; 232 } 233 234 @end 235 236 @implementation InfoBarController (PrivateMethods) 237 238 - (void)initializeLabel { 239 // Replace the label placeholder NSTextField with the real label NSTextView. 240 // The former doesn't show links in a nice way, but the latter can't be added 241 // in IB without a containing scroll view, so create the NSTextView 242 // programmatically. 243 label_.reset([[InfoBarTextView alloc] 244 initWithFrame:[labelPlaceholder_ frame]]); 245 [label_.get() setAutoresizingMask:[labelPlaceholder_ autoresizingMask]]; 246 [[labelPlaceholder_ superview] 247 replaceSubview:labelPlaceholder_ with:label_.get()]; 248 labelPlaceholder_ = nil; // Now released. 249 [label_.get() setDelegate:self]; 250 [label_.get() setEditable:NO]; 251 [label_.get() setDrawsBackground:NO]; 252 [label_.get() setHorizontallyResizable:NO]; 253 [label_.get() setVerticallyResizable:NO]; 254 } 255 256 - (void)removeInfoBar { 257 // TODO(rohitrao): This method can be called even if the infobar has already 258 // been removed and |delegate_| is NULL. Is there a way to rewrite the code 259 // so that inner event loops don't cause us to try and remove the infobar 260 // twice? http://crbug.com/54253 261 [containerController_ removeDelegate:delegate_]; 262 } 263 264 - (void)cleanUpAfterAnimation:(BOOL)finished { 265 // Don't need to do any cleanup if the bar was animating open. 266 if (!infoBarClosing_) 267 return; 268 269 // Notify the delegate that the infobar was closed. The delegate may delete 270 // itself as a result of InfoBarClosed(), so we null out its pointer. 271 if (delegate_) { 272 delegate_->InfoBarClosed(); 273 delegate_ = NULL; 274 } 275 276 // If the animation ran to completion, then we need to remove ourselves from 277 // the container. If the animation was interrupted, then the container will 278 // take care of removing us. 279 // TODO(rohitrao): UGH! This works for now, but should be cleaner. 280 if (finished) 281 [containerController_ removeController:self]; 282 } 283 284 - (void)animationDidStop:(NSAnimation*)animation { 285 [self cleanUpAfterAnimation:NO]; 286 } 287 288 - (void)animationDidEnd:(NSAnimation*)animation { 289 [self cleanUpAfterAnimation:YES]; 290 } 291 292 // TODO(joth): This method factors out some common functionality between the 293 // various derived infobar classes, however the class hierarchy itself could 294 // use refactoring to reduce this duplication. http://crbug.com/38924 295 - (void)setLabelToMessage:(NSString*)message 296 withLink:(NSString*)link 297 atOffset:(NSUInteger)linkOffset { 298 if (linkOffset == std::wstring::npos) { 299 // linkOffset == std::wstring::npos means the link should be right-aligned, 300 // which is not supported on Mac (http://crbug.com/47728). 301 NOTIMPLEMENTED(); 302 linkOffset = [message length]; 303 } 304 // Create an attributes dictionary for the entire message. We have 305 // to expicitly set the font the control's font. We also override 306 // the cursor to give us the normal cursor rather than the text 307 // insertion cursor. 308 NSMutableDictionary* linkAttributes = [NSMutableDictionary dictionary]; 309 [linkAttributes setObject:[NSCursor arrowCursor] 310 forKey:NSCursorAttributeName]; 311 NSFont* font = [NSFont labelFontOfSize: 312 [NSFont systemFontSizeForControlSize:NSRegularControlSize]]; 313 [linkAttributes setObject:font 314 forKey:NSFontAttributeName]; 315 316 // Create the attributed string for the main message text. 317 scoped_nsobject<NSMutableAttributedString> infoText( 318 [[NSMutableAttributedString alloc] initWithString:message]); 319 [infoText.get() addAttributes:linkAttributes 320 range:NSMakeRange(0, [infoText.get() length])]; 321 // Add additional attributes to style the link text appropriately as 322 // well as linkify it. 323 [linkAttributes setObject:[NSColor blueColor] 324 forKey:NSForegroundColorAttributeName]; 325 [linkAttributes setObject:[NSNumber numberWithBool:YES] 326 forKey:NSUnderlineStyleAttributeName]; 327 [linkAttributes setObject:[NSCursor pointingHandCursor] 328 forKey:NSCursorAttributeName]; 329 [linkAttributes setObject:[NSNumber numberWithInt:NSSingleUnderlineStyle] 330 forKey:NSUnderlineStyleAttributeName]; 331 [linkAttributes setObject:[NSString string] // dummy value 332 forKey:NSLinkAttributeName]; 333 334 // Insert the link text into the string at the appropriate offset. 335 scoped_nsobject<NSAttributedString> attributedString( 336 [[NSAttributedString alloc] initWithString:link 337 attributes:linkAttributes]); 338 [infoText.get() insertAttributedString:attributedString.get() 339 atIndex:linkOffset]; 340 // Update the label view with the new text. 341 [[label_.get() textStorage] setAttributedString:infoText]; 342 } 343 344 @end 345 346 347 ///////////////////////////////////////////////////////////////////////// 348 // LinkInfoBarController implementation 349 350 @implementation LinkInfoBarController 351 352 // Link infobars have a text message, of which part is linkified. We 353 // use an NSAttributedString to display styled text, and we set a 354 // NSLink attribute on the hyperlink portion of the message. Infobars 355 // use a custom NSTextField subclass, which allows us to override 356 // textView:clickedOnLink:atIndex: and intercept clicks. 357 // 358 - (void)addAdditionalControls { 359 // No buttons. 360 [self removeButtons]; 361 362 LinkInfoBarDelegate* delegate = delegate_->AsLinkInfoBarDelegate(); 363 DCHECK(delegate); 364 size_t offset = std::wstring::npos; 365 string16 message = delegate->GetMessageTextWithOffset(&offset); 366 [self setLabelToMessage:base::SysUTF16ToNSString(message) 367 withLink:base::SysUTF16ToNSString(delegate->GetLinkText()) 368 atOffset:offset]; 369 } 370 371 // Called when someone clicks on the link in the infobar. This method 372 // is called by the InfobarTextField on its delegate (the 373 // LinkInfoBarController). 374 - (void)linkClicked { 375 WindowOpenDisposition disposition = 376 event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); 377 if (delegate_ && delegate_->AsLinkInfoBarDelegate()->LinkClicked(disposition)) 378 [self removeInfoBar]; 379 } 380 381 @end 382 383 384 ///////////////////////////////////////////////////////////////////////// 385 // ConfirmInfoBarController implementation 386 387 @implementation ConfirmInfoBarController 388 389 // Called when someone clicks on the "OK" button. 390 - (IBAction)ok:(id)sender { 391 if (delegate_ && delegate_->AsConfirmInfoBarDelegate()->Accept()) 392 [self removeInfoBar]; 393 } 394 395 // Called when someone clicks on the "Cancel" button. 396 - (IBAction)cancel:(id)sender { 397 if (delegate_ && delegate_->AsConfirmInfoBarDelegate()->Cancel()) 398 [self removeInfoBar]; 399 } 400 401 // Confirm infobars can have OK and/or cancel buttons, depending on 402 // the return value of GetButtons(). We create each button if 403 // required and position them to the left of the close button. 404 - (void)addAdditionalControls { 405 ConfirmInfoBarDelegate* delegate = delegate_->AsConfirmInfoBarDelegate(); 406 DCHECK(delegate); 407 int visibleButtons = delegate->GetButtons(); 408 409 NSRect okButtonFrame = [okButton_ frame]; 410 NSRect cancelButtonFrame = [cancelButton_ frame]; 411 412 DCHECK(NSMaxX(okButtonFrame) < NSMinX(cancelButtonFrame)) 413 << "Cancel button expected to be on the right of the Ok button in nib"; 414 415 CGFloat rightEdge = NSMaxX(cancelButtonFrame); 416 CGFloat spaceBetweenButtons = 417 NSMinX(cancelButtonFrame) - NSMaxX(okButtonFrame); 418 CGFloat spaceBeforeButtons = 419 NSMinX(okButtonFrame) - NSMaxX([label_.get() frame]); 420 421 // Update and position the Cancel button if needed. Otherwise, hide it. 422 if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) { 423 [cancelButton_ setTitle:base::SysUTF16ToNSString( 424 delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_CANCEL))]; 425 [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_]; 426 cancelButtonFrame = [cancelButton_ frame]; 427 428 // Position the cancel button to the left of the Close button. 429 cancelButtonFrame.origin.x = rightEdge - cancelButtonFrame.size.width; 430 [cancelButton_ setFrame:cancelButtonFrame]; 431 432 // Update the rightEdge 433 rightEdge = NSMinX(cancelButtonFrame); 434 } else { 435 [cancelButton_ removeFromSuperview]; 436 } 437 438 // Update and position the OK button if needed. Otherwise, hide it. 439 if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK) { 440 [okButton_ setTitle:base::SysUTF16ToNSString( 441 delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_OK))]; 442 [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_]; 443 okButtonFrame = [okButton_ frame]; 444 445 // If we had a Cancel button, leave space between the buttons. 446 if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) { 447 rightEdge -= spaceBetweenButtons; 448 } 449 450 // Position the OK button on our current right edge. 451 okButtonFrame.origin.x = rightEdge - okButtonFrame.size.width; 452 [okButton_ setFrame:okButtonFrame]; 453 454 455 // Update the rightEdge 456 rightEdge = NSMinX(okButtonFrame); 457 } else { 458 [okButton_ removeFromSuperview]; 459 } 460 461 // If we had either button, leave space before the edge of the textfield. 462 if ((visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) || 463 (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK)) { 464 rightEdge -= spaceBeforeButtons; 465 } 466 467 NSRect frame = [label_.get() frame]; 468 DCHECK(rightEdge > NSMinX(frame)) 469 << "Need to make the xib larger to handle buttons with text this long"; 470 frame.size.width = rightEdge - NSMinX(frame); 471 [label_.get() setFrame:frame]; 472 473 // Set the text and link. 474 NSString* message = base::SysUTF16ToNSString(delegate->GetMessageText()); 475 string16 link = delegate->GetLinkText(); 476 if (link.empty()) { 477 // Simple case: no link, so just set the message directly. 478 [self setLabelToMessage:message]; 479 } else { 480 // Inserting the link unintentionally causes the text to have a slightly 481 // different result to the simple case above: text is truncated on word 482 // boundaries (if needed) rather than elided with ellipses. 483 484 // Add spacing between the label and the link. 485 message = [message stringByAppendingString:@" "]; 486 [self setLabelToMessage:message 487 withLink:base::SysUTF16ToNSString(link) 488 atOffset:[message length]]; 489 } 490 } 491 492 // Called when someone clicks on the link in the infobar. This method 493 // is called by the InfobarTextField on its delegate (the 494 // LinkInfoBarController). 495 - (void)linkClicked { 496 WindowOpenDisposition disposition = 497 event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); 498 if (delegate_ && 499 delegate_->AsConfirmInfoBarDelegate()->LinkClicked(disposition)) 500 [self removeInfoBar]; 501 } 502 503 @end 504 505 506 ////////////////////////////////////////////////////////////////////////// 507 // CreateInfoBar() implementations 508 509 InfoBar* LinkInfoBarDelegate::CreateInfoBar() { 510 LinkInfoBarController* controller = 511 [[LinkInfoBarController alloc] initWithDelegate:this]; 512 return new InfoBar(controller); 513 } 514 515 InfoBar* ConfirmInfoBarDelegate::CreateInfoBar() { 516 ConfirmInfoBarController* controller = 517 [[ConfirmInfoBarController alloc] initWithDelegate:this]; 518 return new InfoBar(controller); 519 } 520