Home | History | Annotate | Download | only in infobars
      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/infobars/infobar_controller.h"
      6 
      7 #include "base/logging.h"
      8 #include "base/mac/bundle_locations.h"
      9 #include "base/mac/mac_util.h"
     10 #include "chrome/browser/infobars/infobar_service.h"
     11 #import "chrome/browser/ui/cocoa/animatable_view.h"
     12 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
     13 #import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
     14 #import "chrome/browser/ui/cocoa/image_button_cell.h"
     15 #include "chrome/browser/ui/cocoa/infobars/infobar.h"
     16 #import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h"
     17 #import "chrome/browser/ui/cocoa/infobars/infobar_gradient_view.h"
     18 #import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
     19 #include "grit/theme_resources.h"
     20 #include "grit/ui_resources.h"
     21 #include "ui/gfx/image/image.h"
     22 
     23 namespace {
     24 // Durations set to match the default SlideAnimation duration.
     25 const float kAnimateOpenDuration = 0.12;
     26 const float kAnimateCloseDuration = 0.12;
     27 }
     28 
     29 @interface InfoBarController (PrivateMethods)
     30 // Sets |label_| based on |labelPlaceholder_|, sets |labelPlaceholder_| to nil.
     31 - (void)initializeLabel;
     32 
     33 // Performs final cleanup after an animation is finished or stopped, including
     34 // notifying the InfoBarDelegate that the infobar was closed and removing the
     35 // infobar from its container, if necessary.
     36 - (void)cleanUpAfterAnimation:(BOOL)finished;
     37 
     38 // Returns the point, in window coordinates, at which the apex of the infobar
     39 // tip should be drawn.
     40 - (NSPoint)pointForTipApex;
     41 @end
     42 
     43 @implementation InfoBarController
     44 
     45 @synthesize containerController = containerController_;
     46 @synthesize delegate = delegate_;
     47 
     48 - (id)initWithDelegate:(InfoBarDelegate*)delegate
     49                  owner:(InfoBarService*)owner {
     50   DCHECK(delegate);
     51   if ((self = [super initWithNibName:@"InfoBar"
     52                               bundle:base::mac::FrameworkBundle()])) {
     53     delegate_ = delegate;
     54     owner_ = owner;
     55   }
     56   return self;
     57 }
     58 
     59 // All infobars have an icon, so we set up the icon in the base class
     60 // awakeFromNib.
     61 - (void)awakeFromNib {
     62   DCHECK(delegate_);
     63 
     64   [[closeButton_ cell] setImageID:IDR_CLOSE_1
     65                    forButtonState:image_button_cell::kDefaultState];
     66   [[closeButton_ cell] setImageID:IDR_CLOSE_1_H
     67                    forButtonState:image_button_cell::kHoverState];
     68   [[closeButton_ cell] setImageID:IDR_CLOSE_1_P
     69                    forButtonState:image_button_cell::kPressedState];
     70   [[closeButton_ cell] setImageID:IDR_CLOSE_1
     71                    forButtonState:image_button_cell::kDisabledState];
     72 
     73   if (!delegate_->GetIcon().IsEmpty()) {
     74     [image_ setImage:delegate_->GetIcon().ToNSImage()];
     75   } else {
     76     // No icon, remove it from the view and grow the textfield to include the
     77     // space.
     78     NSRect imageFrame = [image_ frame];
     79     NSRect labelFrame = [labelPlaceholder_ frame];
     80     labelFrame.size.width += NSMinX(imageFrame) - NSMinX(labelFrame);
     81     labelFrame.origin.x = imageFrame.origin.x;
     82     [image_ removeFromSuperview];
     83     image_ = nil;
     84     [labelPlaceholder_ setFrame:labelFrame];
     85   }
     86   [self initializeLabel];
     87 
     88   [self addAdditionalControls];
     89 
     90   infoBarView_.tipApex = [self pointForTipApex];
     91   [infoBarView_ setInfobarType:delegate_->GetInfoBarType()];
     92 }
     93 
     94 - (void)dealloc {
     95   [okButton_ setTarget:nil];
     96   [cancelButton_ setTarget:nil];
     97   [closeButton_ setTarget:nil];
     98   [super dealloc];
     99 }
    100 
    101 // Called when someone clicks on the embedded link.
    102 - (BOOL)textView:(NSTextView*)textView
    103    clickedOnLink:(id)link
    104          atIndex:(NSUInteger)charIndex {
    105   if ([self respondsToSelector:@selector(linkClicked)])
    106     [self performSelector:@selector(linkClicked)];
    107   return YES;
    108 }
    109 
    110 - (BOOL)isOwned {
    111   return !!owner_;
    112 }
    113 
    114 // Called when someone clicks on the ok button.
    115 - (void)ok:(id)sender {
    116   // Subclasses must override this method if they do not hide the ok button.
    117   NOTREACHED();
    118 }
    119 
    120 // Called when someone clicks on the cancel button.
    121 - (void)cancel:(id)sender {
    122   // Subclasses must override this method if they do not hide the cancel button.
    123   NOTREACHED();
    124 }
    125 
    126 // Called when someone clicks on the close button.
    127 - (void)dismiss:(id)sender {
    128   if (![self isOwned])
    129     return;
    130   delegate_->InfoBarDismissed();
    131   [self removeSelf];
    132 }
    133 
    134 - (void)removeSelf {
    135   // |owner_| should never be NULL here.  If it is, then someone violated what
    136   // they were supposed to do -- e.g. a ConfirmInfoBarDelegate subclass returned
    137   // true from Accept() or Cancel() even though the infobar was already closing.
    138   // In the worst case, if we also switched tabs during that process, then
    139   // |this| has already been destroyed.  But if that's the case, then we're
    140   // going to deref a garbage |this| pointer here whether we check |owner_| or
    141   // not, and in other cases (where we're still closing and |this| is valid),
    142   // checking |owner_| here will avoid a NULL deref.
    143   if (owner_)
    144     owner_->RemoveInfoBar(delegate_);
    145 }
    146 
    147 - (AnimatableView*)animatableView {
    148   return static_cast<AnimatableView*>([self view]);
    149 }
    150 
    151 - (void)open {
    152   // Simply reset the frame size to its opened size, forcing a relayout.
    153   CGFloat finalHeight = [[self view] frame].size.height;
    154   [[self animatableView] setHeight:finalHeight];
    155 }
    156 
    157 - (void)animateOpen {
    158   // Force the frame size to be 0 and then start an animation.
    159   NSRect frame = [[self view] frame];
    160   CGFloat finalHeight = frame.size.height;
    161   frame.size.height = 0;
    162   [[self view] setFrame:frame];
    163   [[self animatableView] animateToNewHeight:finalHeight
    164                                    duration:kAnimateOpenDuration];
    165 }
    166 
    167 - (void)close {
    168   // Stop any running animations.
    169   [[self animatableView] stopAnimation];
    170   infoBarClosing_ = YES;
    171   [self cleanUpAfterAnimation:YES];
    172 }
    173 
    174 - (void)animateClosed {
    175   // Notify the container of our intentions.
    176   [containerController_ willRemoveController:self];
    177 
    178   // Start animating closed.  We will receive a notification when the animation
    179   // is done, at which point we can remove our view from the hierarchy and
    180   // notify the delegate that the infobar was closed.
    181   [[self animatableView] animateToNewHeight:0 duration:kAnimateCloseDuration];
    182 
    183   // The above call may trigger an animationDidStop: notification for any
    184   // currently-running animations, so do not set |infoBarClosing_| until after
    185   // starting the animation.
    186   infoBarClosing_ = YES;
    187 }
    188 
    189 - (void)addAdditionalControls {
    190   // Default implementation does nothing.
    191 }
    192 
    193 - (void)infobarWillClose {
    194   owner_ = NULL;
    195 }
    196 
    197 - (void)removeButtons {
    198   // Extend the label all the way across.
    199   NSRect labelFrame = [label_.get() frame];
    200   labelFrame.size.width = NSMaxX([cancelButton_ frame]) - NSMinX(labelFrame);
    201   [okButton_ removeFromSuperview];
    202   okButton_ = nil;
    203   [cancelButton_ removeFromSuperview];
    204   cancelButton_ = nil;
    205   [label_.get() setFrame:labelFrame];
    206 }
    207 
    208 - (void)setHasTip:(BOOL)hasTip {
    209   [infoBarView_ setHasTip:hasTip];
    210 }
    211 
    212 - (void)disablePopUpMenu:(NSMenu*)menu {
    213   // Remove the menu if visible.
    214   [menu cancelTracking];
    215 
    216   // If the menu is re-opened, prevent queries to update items.
    217   [menu setDelegate:nil];
    218 
    219   // Prevent target/action messages to the controller.
    220   for (NSMenuItem* item in [menu itemArray]) {
    221     [item setEnabled:NO];
    222     [item setTarget:nil];
    223   }
    224 }
    225 
    226 @end
    227 
    228 @implementation InfoBarController (PrivateMethods)
    229 
    230 - (void)initializeLabel {
    231   // Replace the label placeholder NSTextField with the real label NSTextView.
    232   // The former doesn't show links in a nice way, but the latter can't be added
    233   // in IB without a containing scroll view, so create the NSTextView
    234   // programmatically.
    235   label_.reset([[HyperlinkTextView alloc]
    236       initWithFrame:[labelPlaceholder_ frame]]);
    237   [label_.get() setAutoresizingMask:[labelPlaceholder_ autoresizingMask]];
    238   [[labelPlaceholder_ superview]
    239       replaceSubview:labelPlaceholder_ with:label_.get()];
    240   labelPlaceholder_ = nil;  // Now released.
    241   [label_.get() setDelegate:self];
    242 }
    243 
    244 - (void)cleanUpAfterAnimation:(BOOL)finished {
    245   // Don't need to do any cleanup if the bar was animating open.
    246   if (!infoBarClosing_)
    247     return;
    248 
    249   if (delegate_) {
    250     delete delegate_;
    251     delegate_ = NULL;
    252   }
    253 
    254   // If the animation ran to completion, then we need to remove ourselves from
    255   // the container.  If the animation was interrupted, then the container will
    256   // take care of removing us.
    257   // TODO(rohitrao): UGH!  This works for now, but should be cleaner.
    258   if (finished)
    259     [containerController_ removeController:self];
    260 }
    261 
    262 - (void)animationDidStop:(NSAnimation*)animation {
    263   [self cleanUpAfterAnimation:NO];
    264 }
    265 
    266 - (void)animationDidEnd:(NSAnimation*)animation {
    267   [self cleanUpAfterAnimation:YES];
    268 }
    269 
    270 - (NSPoint)pointForTipApex {
    271   BrowserWindowController* windowController =
    272       [containerController_ browserWindowController];
    273   if (!windowController) {
    274     // This should only happen in unit tests.
    275     return NSZeroPoint;
    276   }
    277 
    278   LocationBarViewMac* locationBar = [windowController locationBarBridge];
    279   return locationBar->GetPageInfoBubblePoint();
    280 }
    281 
    282 @end
    283