Home | History | Annotate | Download | only in infobars
      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