| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include "base/logging.h" // for NOTREACHED() |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/mac_util.h" |
| #include "base/sys_string_conversions.h" |
| #include "grit/ui_resources.h" |
| #include "chrome/browser/api/infobars/confirm_infobar_delegate.h" |
| #include "chrome/browser/api/infobars/infobar_service.h" |
| #include "chrome/browser/infobars/alternate_nav_infobar_delegate.h" |
| #import "chrome/browser/ui/cocoa/animatable_view.h" |
| #import "chrome/browser/ui/cocoa/browser_window_controller.h" |
| #include "chrome/browser/ui/cocoa/event_utils.h" |
| #import "chrome/browser/ui/cocoa/hyperlink_text_view.h" |
| #import "chrome/browser/ui/cocoa/image_button_cell.h" |
| #include "chrome/browser/ui/cocoa/infobars/infobar.h" |
| #import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h" |
| #import "chrome/browser/ui/cocoa/infobars/infobar_controller.h" |
| #import "chrome/browser/ui/cocoa/infobars/infobar_gradient_view.h" |
| #import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" |
| #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
| #include "ui/gfx/image/image.h" |
| #include "webkit/glue/window_open_disposition.h" |
| |
| namespace { |
| // Durations set to match the default SlideAnimation duration. |
| const float kAnimateOpenDuration = 0.12; |
| const float kAnimateCloseDuration = 0.12; |
| } |
| |
| @interface InfoBarController (PrivateMethods) |
| // Sets |label_| based on |labelPlaceholder_|, sets |labelPlaceholder_| to nil. |
| - (void)initializeLabel; |
| |
| // Performs final cleanup after an animation is finished or stopped, including |
| // notifying the InfoBarDelegate that the infobar was closed and removing the |
| // infobar from its container, if necessary. |
| - (void)cleanUpAfterAnimation:(BOOL)finished; |
| |
| // Returns the point, in window coordinates, at which the apex of the infobar |
| // tip should be drawn. |
| - (NSPoint)pointForTipApex; |
| @end |
| |
| @implementation InfoBarController |
| |
| @synthesize containerController = containerController_; |
| @synthesize delegate = delegate_; |
| |
| - (id)initWithDelegate:(InfoBarDelegate*)delegate |
| owner:(InfoBarService*)owner { |
| DCHECK(delegate); |
| if ((self = [super initWithNibName:@"InfoBar" |
| bundle:base::mac::FrameworkBundle()])) { |
| delegate_ = delegate; |
| owner_ = owner; |
| } |
| return self; |
| } |
| |
| // All infobars have an icon, so we set up the icon in the base class |
| // awakeFromNib. |
| - (void)awakeFromNib { |
| DCHECK(delegate_); |
| |
| [[closeButton_ cell] setImageID:IDR_CLOSE_BAR |
| forButtonState:image_button_cell::kDefaultState]; |
| [[closeButton_ cell] setImageID:IDR_CLOSE_BAR_H |
| forButtonState:image_button_cell::kHoverState]; |
| [[closeButton_ cell] setImageID:IDR_CLOSE_BAR_P |
| forButtonState:image_button_cell::kPressedState]; |
| [[closeButton_ cell] setImageID:IDR_CLOSE_BAR |
| forButtonState:image_button_cell::kDisabledState]; |
| |
| if (delegate_->GetIcon()) { |
| [image_ setImage:delegate_->GetIcon()->ToNSImage()]; |
| } else { |
| // No icon, remove it from the view and grow the textfield to include the |
| // space. |
| NSRect imageFrame = [image_ frame]; |
| NSRect labelFrame = [labelPlaceholder_ frame]; |
| labelFrame.size.width += NSMinX(imageFrame) - NSMinX(labelFrame); |
| labelFrame.origin.x = imageFrame.origin.x; |
| [image_ removeFromSuperview]; |
| image_ = nil; |
| [labelPlaceholder_ setFrame:labelFrame]; |
| } |
| [self initializeLabel]; |
| |
| [self addAdditionalControls]; |
| |
| infoBarView_.tipApex = [self pointForTipApex]; |
| [infoBarView_ setInfobarType:delegate_->GetInfoBarType()]; |
| } |
| |
| - (void)dealloc { |
| [okButton_ setTarget:nil]; |
| [cancelButton_ setTarget:nil]; |
| [closeButton_ setTarget:nil]; |
| [super dealloc]; |
| } |
| |
| // Called when someone clicks on the embedded link. |
| - (BOOL)textView:(NSTextView*)textView |
| clickedOnLink:(id)link |
| atIndex:(NSUInteger)charIndex { |
| if ([self respondsToSelector:@selector(linkClicked)]) |
| [self performSelector:@selector(linkClicked)]; |
| return YES; |
| } |
| |
| - (BOOL)isOwned { |
| return !!owner_; |
| } |
| |
| // Called when someone clicks on the ok button. |
| - (void)ok:(id)sender { |
| // Subclasses must override this method if they do not hide the ok button. |
| NOTREACHED(); |
| } |
| |
| // Called when someone clicks on the cancel button. |
| - (void)cancel:(id)sender { |
| // Subclasses must override this method if they do not hide the cancel button. |
| NOTREACHED(); |
| } |
| |
| // Called when someone clicks on the close button. |
| - (void)dismiss:(id)sender { |
| if (![self isOwned]) |
| return; |
| delegate_->InfoBarDismissed(); |
| [self removeSelf]; |
| } |
| |
| - (void)removeSelf { |
| // |owner_| should never be NULL here. If it is, then someone violated what |
| // they were supposed to do -- e.g. a ConfirmInfoBarDelegate subclass returned |
| // true from Accept() or Cancel() even though the infobar was already closing. |
| // In the worst case, if we also switched tabs during that process, then |
| // |this| has already been destroyed. But if that's the case, then we're |
| // going to deref a garbage |this| pointer here whether we check |owner_| or |
| // not, and in other cases (where we're still closing and |this| is valid), |
| // checking |owner_| here will avoid a NULL deref. |
| if (owner_) |
| owner_->RemoveInfoBar(delegate_); |
| } |
| |
| - (AnimatableView*)animatableView { |
| return static_cast<AnimatableView*>([self view]); |
| } |
| |
| - (void)open { |
| // Simply reset the frame size to its opened size, forcing a relayout. |
| CGFloat finalHeight = [[self view] frame].size.height; |
| [[self animatableView] setHeight:finalHeight]; |
| } |
| |
| - (void)animateOpen { |
| // Force the frame size to be 0 and then start an animation. |
| NSRect frame = [[self view] frame]; |
| CGFloat finalHeight = frame.size.height; |
| frame.size.height = 0; |
| [[self view] setFrame:frame]; |
| [[self animatableView] animateToNewHeight:finalHeight |
| duration:kAnimateOpenDuration]; |
| } |
| |
| - (void)close { |
| // Stop any running animations. |
| [[self animatableView] stopAnimation]; |
| infoBarClosing_ = YES; |
| [self cleanUpAfterAnimation:YES]; |
| } |
| |
| - (void)animateClosed { |
| // Notify the container of our intentions. |
| [containerController_ willRemoveController:self]; |
| |
| // Start animating closed. We will receive a notification when the animation |
| // is done, at which point we can remove our view from the hierarchy and |
| // notify the delegate that the infobar was closed. |
| [[self animatableView] animateToNewHeight:0 duration:kAnimateCloseDuration]; |
| |
| // The above call may trigger an animationDidStop: notification for any |
| // currently-running animations, so do not set |infoBarClosing_| until after |
| // starting the animation. |
| infoBarClosing_ = YES; |
| } |
| |
| - (void)addAdditionalControls { |
| // Default implementation does nothing. |
| } |
| |
| - (void)infobarWillClose { |
| owner_ = NULL; |
| } |
| |
| - (void)removeButtons { |
| // Extend the label all the way across. |
| NSRect labelFrame = [label_.get() frame]; |
| labelFrame.size.width = NSMaxX([cancelButton_ frame]) - NSMinX(labelFrame); |
| [okButton_ removeFromSuperview]; |
| okButton_ = nil; |
| [cancelButton_ removeFromSuperview]; |
| cancelButton_ = nil; |
| [label_.get() setFrame:labelFrame]; |
| } |
| |
| - (void)disablePopUpMenu:(NSMenu*)menu { |
| // Remove the menu if visible. |
| [menu cancelTracking]; |
| |
| // If the menu is re-opened, prevent queries to update items. |
| [menu setDelegate:nil]; |
| |
| // Prevent target/action messages to the controller. |
| for (NSMenuItem* item in [menu itemArray]) { |
| [item setEnabled:NO]; |
| [item setTarget:nil]; |
| } |
| } |
| |
| @end |
| |
| @implementation InfoBarController (PrivateMethods) |
| |
| - (void)initializeLabel { |
| // Replace the label placeholder NSTextField with the real label NSTextView. |
| // The former doesn't show links in a nice way, but the latter can't be added |
| // in IB without a containing scroll view, so create the NSTextView |
| // programmatically. |
| label_.reset([[HyperlinkTextView alloc] |
| initWithFrame:[labelPlaceholder_ frame]]); |
| [label_.get() setAutoresizingMask:[labelPlaceholder_ autoresizingMask]]; |
| [[labelPlaceholder_ superview] |
| replaceSubview:labelPlaceholder_ with:label_.get()]; |
| labelPlaceholder_ = nil; // Now released. |
| [label_.get() setDelegate:self]; |
| } |
| |
| - (void)cleanUpAfterAnimation:(BOOL)finished { |
| // Don't need to do any cleanup if the bar was animating open. |
| if (!infoBarClosing_) |
| return; |
| |
| if (delegate_) { |
| delete delegate_; |
| delegate_ = NULL; |
| } |
| |
| // If the animation ran to completion, then we need to remove ourselves from |
| // the container. If the animation was interrupted, then the container will |
| // take care of removing us. |
| // TODO(rohitrao): UGH! This works for now, but should be cleaner. |
| if (finished) |
| [containerController_ removeController:self]; |
| } |
| |
| - (void)animationDidStop:(NSAnimation*)animation { |
| [self cleanUpAfterAnimation:NO]; |
| } |
| |
| - (void)animationDidEnd:(NSAnimation*)animation { |
| [self cleanUpAfterAnimation:YES]; |
| } |
| |
| - (NSPoint)pointForTipApex { |
| BrowserWindowController* windowController = |
| [containerController_ browserWindowController]; |
| if (!windowController) { |
| // This should only happen in unit tests. |
| return NSZeroPoint; |
| } |
| |
| LocationBarViewMac* locationBar = [windowController locationBarBridge]; |
| return locationBar->GetPageInfoBubblePoint(); |
| } |
| |
| @end |
| |
| |
| ///////////////////////////////////////////////////////////////////////// |
| // AlternateNavInfoBarController implementation |
| |
| @implementation AlternateNavInfoBarController |
| |
| // Link infobars have a text message, of which part is linkified. We |
| // use an NSAttributedString to display styled text, and we set a |
| // NSLink attribute on the hyperlink portion of the message. Infobars |
| // use a custom NSTextField subclass, which allows us to override |
| // textView:clickedOnLink:atIndex: and intercept clicks. |
| // |
| - (void)addAdditionalControls { |
| // No buttons. |
| [self removeButtons]; |
| |
| AlternateNavInfoBarDelegate* delegate = |
| delegate_->AsAlternateNavInfoBarDelegate(); |
| DCHECK(delegate); |
| size_t offset = string16::npos; |
| string16 message = delegate->GetMessageTextWithOffset(&offset); |
| string16 link = delegate->GetLinkText(); |
| NSFont* font = [NSFont labelFontOfSize: |
| [NSFont systemFontSizeForControlSize:NSRegularControlSize]]; |
| HyperlinkTextView* view = (HyperlinkTextView*)label_.get(); |
| [view setMessageAndLink:base::SysUTF16ToNSString(message) |
| withLink:base::SysUTF16ToNSString(link) |
| atOffset:offset |
| font:font |
| messageColor:[NSColor blackColor] |
| linkColor:[NSColor blueColor]]; |
| } |
| |
| // Called when someone clicks on the link in the infobar. This method |
| // is called by the InfobarTextField on its delegate (the |
| // AlternateNavInfoBarController). |
| - (void)linkClicked { |
| if (![self isOwned]) |
| return; |
| WindowOpenDisposition disposition = |
| event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); |
| if (delegate_->AsAlternateNavInfoBarDelegate()->LinkClicked(disposition)) |
| [self removeSelf]; |
| } |
| |
| @end |
| |
| |
| ///////////////////////////////////////////////////////////////////////// |
| // ConfirmInfoBarController implementation |
| |
| @implementation ConfirmInfoBarController |
| |
| // Called when someone clicks on the "OK" button. |
| - (IBAction)ok:(id)sender { |
| if (![self isOwned]) |
| return; |
| if (delegate_->AsConfirmInfoBarDelegate()->Accept()) |
| [self removeSelf]; |
| } |
| |
| // Called when someone clicks on the "Cancel" button. |
| - (IBAction)cancel:(id)sender { |
| if (![self isOwned]) |
| return; |
| if (delegate_->AsConfirmInfoBarDelegate()->Cancel()) |
| [self removeSelf]; |
| } |
| |
| // Confirm infobars can have OK and/or cancel buttons, depending on |
| // the return value of GetButtons(). We create each button if |
| // required and position them to the left of the close button. |
| - (void)addAdditionalControls { |
| ConfirmInfoBarDelegate* delegate = delegate_->AsConfirmInfoBarDelegate(); |
| DCHECK(delegate); |
| int visibleButtons = delegate->GetButtons(); |
| |
| NSRect okButtonFrame = [okButton_ frame]; |
| NSRect cancelButtonFrame = [cancelButton_ frame]; |
| |
| DCHECK(NSMaxX(cancelButtonFrame) < NSMinX(okButtonFrame)) |
| << "Ok button expected to be on the right of the Cancel button in nib"; |
| |
| CGFloat rightEdge = NSMaxX(okButtonFrame); |
| CGFloat spaceBetweenButtons = |
| NSMinX(okButtonFrame) - NSMaxX(cancelButtonFrame); |
| CGFloat spaceBeforeButtons = |
| NSMinX(cancelButtonFrame) - NSMaxX([label_.get() frame]); |
| |
| // Update and position the OK button if needed. Otherwise, hide it. |
| if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK) { |
| [okButton_ setTitle:base::SysUTF16ToNSString( |
| delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_OK))]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitView:okButton_]; |
| okButtonFrame = [okButton_ frame]; |
| |
| // Position the ok button to the left of the Close button. |
| okButtonFrame.origin.x = rightEdge - okButtonFrame.size.width; |
| [okButton_ setFrame:okButtonFrame]; |
| |
| // Update the rightEdge |
| rightEdge = NSMinX(okButtonFrame); |
| } else { |
| [okButton_ removeFromSuperview]; |
| okButton_ = nil; |
| } |
| |
| // Update and position the Cancel button if needed. Otherwise, hide it. |
| if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) { |
| [cancelButton_ setTitle:base::SysUTF16ToNSString( |
| delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_CANCEL))]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitView:cancelButton_]; |
| cancelButtonFrame = [cancelButton_ frame]; |
| |
| // If we had a Ok button, leave space between the buttons. |
| if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK) { |
| rightEdge -= spaceBetweenButtons; |
| } |
| |
| // Position the Cancel button on our current right edge. |
| cancelButtonFrame.origin.x = rightEdge - cancelButtonFrame.size.width; |
| [cancelButton_ setFrame:cancelButtonFrame]; |
| |
| // Update the rightEdge. |
| rightEdge = NSMinX(cancelButtonFrame); |
| } else { |
| [cancelButton_ removeFromSuperview]; |
| cancelButton_ = nil; |
| } |
| |
| // If we had either button, leave space before the edge of the textfield. |
| if ((visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) || |
| (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK)) { |
| rightEdge -= spaceBeforeButtons; |
| } |
| |
| NSRect frame = [label_.get() frame]; |
| DCHECK(rightEdge > NSMinX(frame)) |
| << "Need to make the xib larger to handle buttons with text this long"; |
| frame.size.width = rightEdge - NSMinX(frame); |
| [label_.get() setFrame:frame]; |
| |
| // Set the text and link. |
| NSString* message = base::SysUTF16ToNSString(delegate->GetMessageText()); |
| string16 link = delegate->GetLinkText(); |
| if (!link.empty()) { |
| // Add spacing between the label and the link. |
| message = [message stringByAppendingString:@" "]; |
| } |
| NSFont* font = [NSFont labelFontOfSize: |
| [NSFont systemFontSizeForControlSize:NSRegularControlSize]]; |
| HyperlinkTextView* view = (HyperlinkTextView*)label_.get(); |
| [view setMessageAndLink:message |
| withLink:base::SysUTF16ToNSString(link) |
| atOffset:[message length] |
| font:font |
| messageColor:[NSColor blackColor] |
| linkColor:[NSColor blueColor]]; |
| } |
| |
| // Called when someone clicks on the link in the infobar. This method |
| // is called by the InfobarTextField on its delegate (the |
| // AlternateNavInfoBarController). |
| - (void)linkClicked { |
| if (![self isOwned]) |
| return; |
| WindowOpenDisposition disposition = |
| event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); |
| if (delegate_->AsConfirmInfoBarDelegate()->LinkClicked(disposition)) |
| [self removeSelf]; |
| } |
| |
| @end |
| |
| |
| ////////////////////////////////////////////////////////////////////////// |
| // CreateInfoBar() implementations |
| |
| InfoBar* AlternateNavInfoBarDelegate::CreateInfoBar(InfoBarService* owner) { |
| AlternateNavInfoBarController* controller = |
| [[AlternateNavInfoBarController alloc] initWithDelegate:this owner:owner]; |
| return new InfoBar(controller, this); |
| } |
| |
| InfoBar* ConfirmInfoBarDelegate::CreateInfoBar(InfoBarService* owner) { |
| ConfirmInfoBarController* controller = |
| [[ConfirmInfoBarController alloc] initWithDelegate:this owner:owner]; |
| return new InfoBar(controller, this); |
| } |