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