diff options
author | rohitrao@chromium.org <rohitrao@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-17 03:16:58 +0000 |
---|---|---|
committer | rohitrao@chromium.org <rohitrao@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-17 03:16:58 +0000 |
commit | 91cea0e774541297a46197d87e486cf8a4199775 (patch) | |
tree | be5058bd3eec65a923abbaef68342249574b7ce2 /chrome/browser/cocoa | |
parent | 8a1d601fcd2f8bf457ea4e9592e918acc9835f87 (diff) | |
download | chromium_src-91cea0e774541297a46197d87e486cf8a4199775.zip chromium_src-91cea0e774541297a46197d87e486cf8a4199775.tar.gz chromium_src-91cea0e774541297a46197d87e486cf8a4199775.tar.bz2 |
First cut at infobars on Mac. These are not expected to be
pretty. Animations and aesthetic appeal will come in a
future CL.
BUG=http://crbug.com/14462
BUG=http://crbug.com/14937
BUG=http://crbug.com/15839
BUG=http://crbug.com/16487
TEST=Infobars should show up when expected.
Review URL: http://codereview.chromium.org/155494
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@20930 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/cocoa')
20 files changed, 1328 insertions, 11 deletions
diff --git a/chrome/browser/cocoa/bookmark_bar_bridge_unittest.mm b/chrome/browser/cocoa/bookmark_bar_bridge_unittest.mm index b32f165..158069b 100644 --- a/chrome/browser/cocoa/bookmark_bar_bridge_unittest.mm +++ b/chrome/browser/cocoa/bookmark_bar_bridge_unittest.mm @@ -34,10 +34,12 @@ typedef std::pair<GURL,WindowOpenDisposition> OpenInfo; - (id)initWithProfile:(Profile*)profile parentView:(NSView*)parentView - webContentView:(NSView*)webContentView { + webContentView:(NSView*)webContentView + infoBarsView:(NSView*)infoBarsView { if ((self = [super initWithProfile:profile parentView:parentView webContentView:webContentView + infoBarsView:infoBarsView delegate:self])) { callbacks_.reset([[NSMutableArray alloc] init]); } @@ -113,12 +115,15 @@ TEST_F(BookmarkBarBridgeTest, TestRedirect) { initWithFrame:NSMakeRect(0,0,100,100)]); scoped_nsobject<NSView> webView([[NSView alloc] initWithFrame:NSMakeRect(0,0,100,100)]); + scoped_nsobject<NSView> infoBarsView( + [[NSView alloc] initWithFrame:NSMakeRect(0,0,100,100)]); scoped_nsobject<FakeBookmarkBarController> controller([[FakeBookmarkBarController alloc] initWithProfile:profile parentView:parentView.get() - webContentView:webView.get()]); + webContentView:webView.get() + infoBarsView:infoBarsView.get()]); EXPECT_TRUE(controller.get()); scoped_ptr<BookmarkBarBridge> bridge(new BookmarkBarBridge(controller.get(), model)); diff --git a/chrome/browser/cocoa/bookmark_bar_controller.h b/chrome/browser/cocoa/bookmark_bar_controller.h index 5d401de..1e98b93 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller.h +++ b/chrome/browser/cocoa/bookmark_bar_controller.h @@ -46,6 +46,7 @@ class PrefService; NSView* parentView_; // weak; our parent view NSView* webContentView_; // weak; where the web goes + NSView* infoBarsView_; // weak; where the infobars go // Bridge from Chrome-style C++ notifications (e.g. derived from // BookmarkModelObserver) @@ -60,9 +61,12 @@ class PrefService; // Initializes the bookmark bar controller with the given browser // profile, parent view (the toolbar), web content view, and delegate. // |delegate| is used for opening URLs. +// TODO(rohitrao, jrg): The bookmark bar shouldn't know about the +// infoBarsView or the webContentView. - (id)initWithProfile:(Profile*)profile parentView:(NSView*)parentView webContentView:(NSView*)webContentView + infoBarsView:(NSView*)infoBarsView delegate:(id<BookmarkURLOpener>)delegate; // Returns whether or not the bookmark bar is visible. diff --git a/chrome/browser/cocoa/bookmark_bar_controller.mm b/chrome/browser/cocoa/bookmark_bar_controller.mm index 678d4ef..7d1ea55 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller.mm +++ b/chrome/browser/cocoa/bookmark_bar_controller.mm @@ -44,6 +44,7 @@ const CGFloat kBookmarkHorizontalPadding = 8.0; - (id)initWithProfile:(Profile*)profile parentView:(NSView*)parentView webContentView:(NSView*)webContentView + infoBarsView:(NSView*)infoBarsView delegate:(id<BookmarkURLOpener>)delegate { if ((self = [super initWithNibName:@"BookmarkBar" bundle:mac_util::MainAppBundle()])) { @@ -51,6 +52,7 @@ const CGFloat kBookmarkHorizontalPadding = 8.0; preferences_ = profile->GetPrefs(); parentView_ = parentView; webContentView_ = webContentView; + infoBarsView_ = infoBarsView; delegate_ = delegate; } return self; @@ -120,6 +122,7 @@ const CGFloat kBookmarkHorizontalPadding = 8.0; NSRect superframe = [parentView_ frame]; NSRect frame = [[self view] frame]; NSRect webframe = [webContentView_ frame]; + NSRect infoframe = [infoBarsView_ frame]; if (apply) { superframe.size.height += kBookmarkBarSuperviewHeightAdjustment; // TODO(jrg): y=0 if we add the bookmark bar before the parent @@ -131,11 +134,13 @@ const CGFloat kBookmarkHorizontalPadding = 8.0; webframe.size.height -= kBookmarkBarWebframeHeightAdjustment; } frame.size.height += kBookmarkBarHeight; + infoframe.origin.y -= kBookmarkBarWebframeHeightAdjustment; } else { superframe.size.height -= kBookmarkBarSuperviewHeightAdjustment; superframe.origin.y += kBookmarkBarSuperviewHeightAdjustment; frame.size.height -= kBookmarkBarHeight; webframe.size.height += kBookmarkBarWebframeHeightAdjustment; + infoframe.origin.y += kBookmarkBarWebframeHeightAdjustment; } // TODO(jrg): Animators can be a little fussy. Setting these three @@ -145,16 +150,19 @@ const CGFloat kBookmarkHorizontalPadding = 8.0; if (1 /* immediately */) { [parentView_ setFrame:superframe]; [webContentView_ setFrame:webframe]; + [infoBarsView_ setFrame:infoframe]; [[self view] setFrame:frame]; } else { [[parentView_ animator] setFrame:superframe]; [[webContentView_ animator] setFrame:webframe]; + [[infoBarsView_ animator] setFrame:infoframe]; [[[self view] animator] setFrame:frame]; } [[self view] setNeedsDisplay:YES]; [parentView_ setNeedsDisplay:YES]; [webContentView_ setNeedsDisplay:YES]; + [infoBarsView_ setNeedsDisplay:YES]; } - (BOOL)isBookmarkBarVisible { diff --git a/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm b/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm index 1103556..f08978e 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm +++ b/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm @@ -30,25 +30,32 @@ namespace { static const int kContentAreaHeight = 500; +static const int kInfoBarViewHeight = 30; class BookmarkBarControllerTest : public testing::Test { public: BookmarkBarControllerTest() { NSRect content_frame = NSMakeRect(0, 0, 800, kContentAreaHeight); + // |infobar_frame| is set to be directly above |content_frame|. + NSRect infobar_frame = NSMakeRect(0, kContentAreaHeight, + 800, kInfoBarViewHeight); NSRect parent_frame = NSMakeRect(0, 0, 800, 50); content_area_.reset([[NSView alloc] initWithFrame:content_frame]); + infobar_view_.reset([[NSView alloc] initWithFrame:infobar_frame]); parent_view_.reset([[NSView alloc] initWithFrame:parent_frame]); [parent_view_ setHidden:YES]; bar_.reset( [[BookmarkBarController alloc] initWithProfile:helper_.profile() parentView:parent_view_.get() webContentView:content_area_.get() + infoBarsView:infobar_view_.get() delegate:nil]); [bar_ view]; // force loading of the nib } CocoaTestHelper cocoa_helper_; // Inits Cocoa, creates window, etc... scoped_nsobject<NSView> content_area_; + scoped_nsobject<NSView> infobar_view_; scoped_nsobject<NSView> parent_view_; BrowserTestHelper helper_; scoped_nsobject<BookmarkBarController> bar_; @@ -70,14 +77,20 @@ TEST_F(BookmarkBarControllerTest, ShowHide) { EXPECT_TRUE([bar_ isBookmarkBarVisible]); EXPECT_FALSE([[bar_ view] isHidden]); NSRect content_frame = [content_area_ frame]; + NSRect infobar_frame = [infobar_view_ frame]; EXPECT_NE(content_frame.size.height, kContentAreaHeight); + EXPECT_EQ(NSMaxY(content_frame), NSMinY(infobar_frame)); + EXPECT_EQ(kInfoBarViewHeight, infobar_frame.size.height); EXPECT_GT([[bar_ view] frame].size.height, 0); [bar_ toggleBookmarkBar]; EXPECT_FALSE([bar_ isBookmarkBarVisible]); EXPECT_TRUE([[bar_ view] isHidden]); content_frame = [content_area_ frame]; + infobar_frame = [infobar_view_ frame]; EXPECT_EQ(content_frame.size.height, kContentAreaHeight); + EXPECT_EQ(NSMaxY(content_frame), NSMinY(infobar_frame)); + EXPECT_EQ(kInfoBarViewHeight, infobar_frame.size.height); EXPECT_EQ([[bar_ view] frame].size.height, 0); } diff --git a/chrome/browser/cocoa/browser_window_controller.h b/chrome/browser/cocoa/browser_window_controller.h index c2c47c1..104742c 100644 --- a/chrome/browser/cocoa/browser_window_controller.h +++ b/chrome/browser/cocoa/browser_window_controller.h @@ -23,6 +23,7 @@ class BrowserWindow; class BrowserWindowCocoa; @class DownloadShelfController; @class FindBarCocoaController; +@class InfoBarContainerController; class LocationBar; class StatusBubble; class TabContents; @@ -55,6 +56,7 @@ class TabStripModelObserverBridge; scoped_nsobject<TitlebarController> titlebarController_; scoped_nsobject<TabStripController> tabStripController_; scoped_nsobject<FindBarCocoaController> findBarCocoaController_; + scoped_nsobject<InfoBarContainerController> infoBarContainerController_; scoped_ptr<StatusBubble> statusBubble_; scoped_nsobject<DownloadShelfController> downloadShelfController_; scoped_nsobject<GTMTheme> theme_; @@ -122,6 +124,10 @@ class TabStripModelObserverBridge; // Returns fullscreen state. - (BOOL)isFullscreen; +// Sent when the infobar view has been resized and other content needs +// to be shifted around it. +- (void)infoBarResized:(float)newHeight; + // The user changed the theme. - (void)userChangedTheme; diff --git a/chrome/browser/cocoa/browser_window_controller.mm b/chrome/browser/cocoa/browser_window_controller.mm index f43724c..4c3f2be 100644 --- a/chrome/browser/cocoa/browser_window_controller.mm +++ b/chrome/browser/cocoa/browser_window_controller.mm @@ -24,6 +24,7 @@ #import "chrome/browser/cocoa/find_bar_cocoa_controller.h" #include "chrome/browser/cocoa/find_bar_bridge.h" #import "chrome/browser/cocoa/fullscreen_window.h" +#import "chrome/browser/cocoa/infobar_container_controller.h" #import "chrome/browser/cocoa/status_bubble_mac.h" #import "chrome/browser/cocoa/tab_strip_model_observer_bridge.h" #import "chrome/browser/cocoa/tab_strip_view.h" @@ -64,6 +65,7 @@ const int kWindowGradientHeight = 24; @interface BrowserWindowController(Private) +- (void)positionInfoBar; - (void)positionToolbar; - (void)removeToolbar; - (void)installIncognitoBadge; @@ -135,14 +137,6 @@ willPositionSheet:(NSWindow*)sheet [self setTheme]; - // Register ourselves for frame changed notifications from the - // tabContentArea. - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(tabContentAreaFrameChanged:) - name:nil - object:[self tabContentArea]]; - // Get the most appropriate size for the window, then enforce the // minimum width and height. The window shim will handle flipping // the coordinates for us so we can use it to save some code. @@ -169,6 +163,14 @@ willPositionSheet:(NSWindow*)sheet // Puts the incognito badge on the window frame, if necessary. [self installIncognitoBadge]; + // Create the infobar container view, so we can pass it to the + // ToolbarController, but do not position the view until after the + // toolbar is in place, as positionToolbar will move the tab content area. + infoBarContainerController_.reset( + [[InfoBarContainerController alloc] + initWithTabStripModel:(browser_->tabstrip_model()) + browserWindowController:self]); + // Create a controller for the toolbar, giving it the toolbar model object // and the toolbar view from the nib. The controller will handle // registering for the appropriate command state changes from the back-end. @@ -177,10 +179,25 @@ willPositionSheet:(NSWindow*)sheet commands:browser->command_updater() profile:browser->profile() webContentView:[self tabContentArea] + infoBarsView:[infoBarContainerController_ view] bookmarkDelegate:self]); [self positionToolbar]; [self fixWindowGradient]; + // Put the infobar container view into the window above the + // tabcontentarea. There are no infobars when starting up, so its + // initial height is 0. + [self positionInfoBar]; + + // Register ourselves for frame changed notifications from the + // tabContentArea. This has to come after all of the resizing and + // positioning above. + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(tabContentAreaFrameChanged:) + name:nil + object:[self tabContentArea]]; + // Create the bridge for the status bubble. statusBubble_.reset(new StatusBubbleMac([self window])); @@ -212,6 +229,8 @@ willPositionSheet:(NSWindow*)sheet // delegate so nothing tries to call us back in the meantime as part of // window destruction. [window_ setDelegate:nil]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } @@ -751,7 +770,6 @@ willPositionSheet:(NSWindow*)sheet // TabContents. #if 0 // TODO(pinkerton):Update as more things become window-specific - infobar_container_->ChangeTabContents(new_contents); contents_container_->SetTabContents(new_contents); #endif @@ -778,6 +796,22 @@ willPositionSheet:(NSWindow*)sheet [self applyTheme]; } +// TODO(rohitrao, jrg): Move this logic out of BrowserWindowController? +- (void)infoBarResized:(float)newHeight { + // The top edge of the infobar is fixed. + NSView* infoBarView = [infoBarContainerController_ view]; + NSRect infoBarFrame = [infoBarView frame]; + int maxY = NSMaxY(infoBarFrame); + int minY = maxY - newHeight; + + [infoBarView setFrame:NSMakeRect(infoBarFrame.origin.x, minY, + infoBarFrame.size.width, newHeight)]; + + NSRect contentFrame = [[self tabContentArea] frame]; + contentFrame.size.height = minY - contentFrame.origin.y; + [[self tabContentArea] setFrame:contentFrame]; +} + - (GTMTheme *)gtm_themeForWindow:(NSWindow*)window { return theme_ ? theme_ : [GTMTheme defaultTheme]; } @@ -786,6 +820,16 @@ willPositionSheet:(NSWindow*)sheet @implementation BrowserWindowController (Private) +// TODO(rohitrao, jrg): Move this logic out of BrowserWindowController? +- (void)positionInfoBar { + NSView* infoBarView = [infoBarContainerController_ view]; + NSRect infoBarFrame = [[self tabContentArea] frame]; + infoBarFrame.origin.y = NSMaxY(infoBarFrame); + infoBarFrame.size.height = 0; + [infoBarView setFrame:infoBarFrame]; + [[[self window] contentView] addSubview:infoBarView]; +} + // If |add| is YES: // Position |toolbarView_| below the tab strip, but not as a // sibling. The toolbar is part of the window's contentView, mainly diff --git a/chrome/browser/cocoa/infobar.h b/chrome/browser/cocoa/infobar.h new file mode 100644 index 0000000..0ebd2a9 --- /dev/null +++ b/chrome/browser/cocoa/infobar.h @@ -0,0 +1,47 @@ +// Copyright (c) 2009 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. + +#ifndef CHROME_BROWSER_COCOA_INFOBAR_H_ +#define CHROME_BROWSER_COCOA_INFOBAR_H_ + +#include "base/logging.h" // for DCHECK + +@class InfoBarController; + +// A C++ wrapper around an Objective-C InfoBarController. This class +// exists solely to be the return value for InfoBarDelegate::CreateInfoBar(), +// as defined in chrome/browser/tab_contents/infobar_delegate.h. This +// class would be analogous to the various bridge classes we already +// have, but since there is no pre-defined InfoBar interface, it is +// easier to simply throw away this object and deal with the +// controller directly rather than pass messages through a bridge. +// +// Callers should delete the returned InfoBar immediately after +// calling CreateInfoBar(), as the returned InfoBar* object is not +// pointed to by anyone. Expected usage: +// +// scoped_ptr<InfoBar> infobar(delegate->CreateInfoBar()); +// InfoBarController* controller = infobar->controller(); +// // Do something with the controller, and save a pointer so it can be +// // deleted later. |infobar| will be deleted automatically. + +class InfoBar { + public: + InfoBar(InfoBarController* controller) { + DCHECK(controller); + controller_ = controller; + } + + InfoBarController* controller() { + return controller_; + } + + private: + // Pointer to the infobar controller. Is never null. + InfoBarController* controller_; // weak + + DISALLOW_COPY_AND_ASSIGN(InfoBar); +}; + +#endif // CHROME_BROWSER_COCOA_INFOBAR_H_ diff --git a/chrome/browser/cocoa/infobar_container_controller.h b/chrome/browser/cocoa/infobar_container_controller.h new file mode 100644 index 0000000..a565a10 --- /dev/null +++ b/chrome/browser/cocoa/infobar_container_controller.h @@ -0,0 +1,80 @@ +// Copyright (c) 2009 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/scoped_nsobject.h" +#include "base/scoped_ptr.h" +#include "chrome/common/notification_registrar.h" + +@class BrowserWindowController; +class InfoBarDelegate; +class InfoBarNotificationObserver; +class TabContents; +class TabStripModel; +class TabStripModelObserverBridge; + +// Controller for the infobar container view, which is the superview +// of all the infobar views. This class owns zero or more +// InfoBarControllers, which manage the infobar views. This class +// also receives tab strip model notifications and handles +// adding/removing infobars when needed. +@interface InfoBarContainerController : NSViewController { + @private + // Needed to send infoBarResized: messages when infobars are added or removed. + BrowserWindowController* browserController_; // weak, owns us. + + // The TabContents we are currently showing infobars for. + TabContents* currentTabContents_; // weak + + // Holds the InfoBarControllers currently owned by this container. + scoped_nsobject<NSMutableArray> infobarControllers_; + + // Lets us get TabChanged/TabDetachedAt notifications. + scoped_ptr<TabStripModelObserverBridge> tabObserver_; + + // Lets us registers for INFOBAR_ADDED/INFOBAR_REMOVED + // notifications. The actual notifications are sent to the + // InfoBarNotificationObserver object, which proxies them back to us. + NotificationRegistrar registrar_; + scoped_ptr<InfoBarNotificationObserver> infoBarObserver_; +} + +- (id)initWithTabStripModel:(TabStripModel*)model + browserWindowController:(BrowserWindowController*)controller; + +// Informs the selected TabContents that the infobars for the given +// |delegate| need to be removed. Does not remove any infobar views +// directly, as they will be removed when handling the subsequent +// INFOBAR_REMOVED notification. Does not notify |delegate| that the +// infobar was closed. +- (void)removeDelegate:(InfoBarDelegate*)delegate; + +@end + + +@interface InfoBarContainerController (ForTheObserverAndTesting) + +// Adds an infobar view for the given delegate. Callers must call +// positionInfoBarsAndRedraw after calling this method. +- (void)addInfoBar:(InfoBarDelegate*)delegate; + +// Removes all the infobar views for a given delegate. Callers must +// call positionInfoBarsAndRedraw after calling this method. +- (void)removeInfoBarsForDelegate:(InfoBarDelegate*)delegate; + +// Positions the infobar views in the container view and notifies +// |browser_controller_| that it needs to resize the container view. +- (void)positionInfoBarsAndRedraw; + +@end + + +@interface InfoBarContainerController (JustForTesting) + +// Removes all infobar views. Callers must call +// positionInfoBarsAndRedraw() after calling this method. +- (void)removeAllInfoBars; + +@end diff --git a/chrome/browser/cocoa/infobar_container_controller.mm b/chrome/browser/cocoa/infobar_container_controller.mm new file mode 100644 index 0000000..06eaf67 --- /dev/null +++ b/chrome/browser/cocoa/infobar_container_controller.mm @@ -0,0 +1,191 @@ +// Copyright (c) 2009 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. + +#include "base/logging.h" +#include "base/mac_util.h" +#import "chrome/browser/cocoa/browser_window_controller.h" +#include "chrome/browser/cocoa/infobar.h" +#import "chrome/browser/cocoa/infobar_container_controller.h" +#import "chrome/browser/cocoa/infobar_controller.h" +#include "chrome/browser/cocoa/tab_strip_model_observer_bridge.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/common/notification_service.h" +#include "skia/ext/skia_utils_mac.h" + +// C++ class that receives INFOBAR_ADDED and INFOBAR_REMOVED +// notifications and proxies them back to |controller|. +class InfoBarNotificationObserver : public NotificationObserver { + public: + InfoBarNotificationObserver(InfoBarContainerController* controller) + : controller_(controller) { + } + + private: + // NotificationObserver implementation + void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + switch (type.value) { + case NotificationType::TAB_CONTENTS_INFOBAR_ADDED: + [controller_ addInfoBar:Details<InfoBarDelegate>(details).ptr()]; + break; + case NotificationType::TAB_CONTENTS_INFOBAR_REMOVED: + [controller_ + removeInfoBarsForDelegate:Details<InfoBarDelegate>(details).ptr()]; + break; + default: + NOTREACHED(); // we don't ask for anything else! + break; + } + + [controller_ positionInfoBarsAndRedraw]; + } + + InfoBarContainerController* controller_; // weak, owns us. +}; + + +@interface InfoBarContainerController (PrivateMethods) +// Returns the desired height of the container view, computed by +// adding together the heights of all its subviews. +- (float)desiredHeight; + +// Modifies this container to display infobars for the given +// |contents|. Registers for INFOBAR_ADDED and INFOBAR_REMOVED +// notifications for |contents|. If we are currently showing any +// infobars, removes them first and deregisters for any +// notifications. |contents| can be NULL, in which case no infobars +// are shown and no notifications are registered for. +- (void)changeTabContents:(TabContents*)contents; + +@end + + +@implementation InfoBarContainerController +- (id)initWithTabStripModel:(TabStripModel*)model + browserWindowController:(BrowserWindowController*)controller { + DCHECK(controller); + if ((self = [super initWithNibName:@"InfoBarContainer" + bundle:mac_util::MainAppBundle()])) { + browserController_ = controller; + tabObserver_.reset(new TabStripModelObserverBridge(model, self)); + infoBarObserver_.reset(new InfoBarNotificationObserver(self)); + + // NSMutableArray needs an initial capacity, and we rarely ever see + // more than two infobars at a time, so that seems like a good choice. + infobarControllers_.reset([[NSMutableArray alloc] initWithCapacity:2]); + } + return self; +} + +- (void)dealloc { + DCHECK([infobarControllers_ count] == 0); + [super dealloc]; +} + +- (void)removeDelegate:(InfoBarDelegate*)delegate { + DCHECK(currentTabContents_); + currentTabContents_->RemoveInfoBar(delegate); +} + +// TabStripModelObserverBridge notifications +- (void)selectTabWithContents:(TabContents*)newContents + previousContents:(TabContents*)oldContents + atIndex:(NSInteger)index + userGesture:(bool)wasUserGesture { + [self changeTabContents:newContents]; +} + +- (void)tabDetachedWithContents:(TabContents*)contents + atIndex:(NSInteger)index { + [self changeTabContents:NULL]; +} + +@end + +@implementation InfoBarContainerController (PrivateMethods) + +- (float)desiredHeight { + float height = 0; + + for (InfoBarController* controller in infobarControllers_.get()) { + height += [[controller view] frame].size.height; + } + + return height; +} + +- (void)changeTabContents:(TabContents*)contents { + registrar_.RemoveAll(); + [self removeAllInfoBars]; + + currentTabContents_ = contents; + if (currentTabContents_) { + for (int i = 0; i < currentTabContents_->infobar_delegate_count(); ++i) { + [self addInfoBar:currentTabContents_->GetInfoBarDelegateAt(i)]; + } + + Source<TabContents> source(currentTabContents_); + registrar_.Add(infoBarObserver_.get(), + NotificationType::TAB_CONTENTS_INFOBAR_ADDED, source); + registrar_.Add(infoBarObserver_.get(), + NotificationType::TAB_CONTENTS_INFOBAR_REMOVED, source); + } + + [self positionInfoBarsAndRedraw]; +} + +- (void)addInfoBar:(InfoBarDelegate*)delegate { + scoped_ptr<InfoBar> infobar(delegate->CreateInfoBar()); + InfoBarController* controller = infobar->controller(); + [controller setContainerController:self]; + [[self view] addSubview:[controller view]]; + [infobarControllers_ addObject:[controller autorelease]]; +} + +- (void)removeInfoBarsForDelegate:(InfoBarDelegate*)delegate { + for (InfoBarController* controller in + [NSArray arrayWithArray:infobarControllers_.get()]) { + if ([controller delegate] == delegate) { + // This code can be executed while -[InfoBarController closeInfoBar] is + // still on the stack, so we retain and autorelease the controller to + // prevent it from being dealloc'ed too early. + [[controller retain] autorelease]; + [[controller view] removeFromSuperview]; + [infobarControllers_ removeObject:controller]; + } + } +} + +- (void)removeAllInfoBars { + for (InfoBarController* controller in infobarControllers_.get()) { + [[controller view] removeFromSuperview]; + } + [infobarControllers_ removeAllObjects]; +} + +- (void)positionInfoBarsAndRedraw { + NSRect containerBounds = [[self view] bounds]; + int minY = 0; + + // Stack the infobars at the bottom of the view, starting with the + // last infobar and working our way to the front of the array. This + // way we ensure that the first infobar added shows up on top, with + // the others below. + for (InfoBarController* controller in + [infobarControllers_ reverseObjectEnumerator]) { + NSView* view = [controller view]; + NSRect frame = [view frame]; + frame.origin.x = NSMinX(containerBounds); + frame.size.width = NSWidth(containerBounds); + frame.origin.y = minY; + minY += frame.size.height; + // TODO(rohitrao, jrg): Replace with an animator. + [view setFrame:frame]; + } + + [browserController_ infoBarResized:[self desiredHeight]]; +} + +@end diff --git a/chrome/browser/cocoa/infobar_container_controller_unittest.mm b/chrome/browser/cocoa/infobar_container_controller_unittest.mm new file mode 100644 index 0000000..77e8e82 --- /dev/null +++ b/chrome/browser/cocoa/infobar_container_controller_unittest.mm @@ -0,0 +1,123 @@ +// Copyright (c) 2009 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/scoped_nsautorelease_pool.h" +#include "base/scoped_nsobject.h" +#include "chrome/browser/cocoa/browser_test_helper.h" +#import "chrome/browser/cocoa/browser_window_controller.h" +#import "chrome/browser/cocoa/cocoa_test_helper.h" +#import "chrome/browser/cocoa/infobar_container_controller.h" +#include "chrome/browser/cocoa/infobar_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" + +// Objective-C classes must be defined outside the namespace. +@interface BrowserWindowControllerPong : BrowserWindowController { + BOOL pong_; +} +@property(readonly) BOOL pong; +@end + +@implementation BrowserWindowControllerPong +@synthesize pong = pong_; + +- (id)initWithBrowser:(Browser*)browser { + if ((self = [super initWithBrowser:browser takeOwnership:NO])) { + pong_ = NO; + } + return self; +} + +- (void)infoBarResized:(float)newHeight { + pong_ = TRUE; +} +@end + +namespace { + +class InfoBarContainerControllerTest : public testing::Test { + virtual void SetUp() { + browserController_.reset([[BrowserWindowControllerPong alloc] + initWithBrowser:browser_helper_.browser()]); + TabStripModel* model = browser_helper_.browser()->tabstrip_model(); + controller_.reset([[InfoBarContainerController alloc] + initWithTabStripModel:model + browserWindowController:browserController_]); + } + + public: + // Order is very important here. We want the controller deleted + // before the pool, and want the pool deleted before + // BrowserTestHelper. + CocoaTestHelper cocoa_helper_; + BrowserTestHelper browser_helper_; + base::ScopedNSAutoreleasePool pool_; + scoped_nsobject<BrowserWindowControllerPong> browserController_; + scoped_nsobject<InfoBarContainerController> controller_; +}; + +TEST_F(InfoBarContainerControllerTest, Show) { + // Make sure the container's view is non-nil and draws without crashing. + NSView* view = [controller_ view]; + EXPECT_TRUE(view != nil); + + [cocoa_helper_.contentView() addSubview:view]; +} + +TEST_F(InfoBarContainerControllerTest, BWCPong) { + // Call positionInfoBarsAndResize and check that the BWC got a resize message. + [controller_ positionInfoBarsAndRedraw]; + EXPECT_TRUE([browserController_ pong]); +} + +TEST_F(InfoBarContainerControllerTest, AddAndRemoveInfoBars) { + NSView* view = [controller_ view]; + [cocoa_helper_.contentView() addSubview:view]; + + // Add three infobars, one of each type, and then remove them. + // After each step check to make sure we have the correct number of + // infobar subviews. + MockAlertInfoBarDelegate alertDelegate; + MockLinkInfoBarDelegate linkDelegate; + MockConfirmInfoBarDelegate confirmDelegate; + + [controller_ addInfoBar:&alertDelegate]; + EXPECT_EQ(1U, [[view subviews] count]); + + [controller_ addInfoBar:&linkDelegate]; + EXPECT_EQ(2U, [[view subviews] count]); + + [controller_ addInfoBar:&confirmDelegate]; + EXPECT_EQ(3U, [[view subviews] count]); + + // Just to mix things up, remove them in a different order. + [controller_ removeInfoBarsForDelegate:&linkDelegate]; + EXPECT_EQ(2U, [[view subviews] count]); + + [controller_ removeInfoBarsForDelegate:&confirmDelegate]; + EXPECT_EQ(1U, [[view subviews] count]); + + [controller_ removeInfoBarsForDelegate:&alertDelegate]; + EXPECT_EQ(0U, [[view subviews] count]); +} + +TEST_F(InfoBarContainerControllerTest, RemoveAllInfoBars) { + NSView* view = [controller_ view]; + [cocoa_helper_.contentView() addSubview:view]; + + // Add three infobars and then remove them all. + MockAlertInfoBarDelegate alertDelegate; + MockLinkInfoBarDelegate linkDelegate; + MockConfirmInfoBarDelegate confirmDelegate; + + [controller_ addInfoBar:&alertDelegate]; + [controller_ addInfoBar:&linkDelegate]; + [controller_ addInfoBar:&confirmDelegate]; + EXPECT_EQ(3U, [[view subviews] count]); + + [controller_ removeAllInfoBars]; + EXPECT_EQ(0U, [[view subviews] count]); +} +} // namespace diff --git a/chrome/browser/cocoa/infobar_controller.h b/chrome/browser/cocoa/infobar_controller.h new file mode 100644 index 0000000..b8d7231 --- /dev/null +++ b/chrome/browser/cocoa/infobar_controller.h @@ -0,0 +1,63 @@ +// Copyright (c) 2009 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> + +@class InfoBarContainerController; +class InfoBarDelegate; + +// A controller for an infobar in the browser window. There is one +// controller per infobar view. The base InfoBarController is able to +// draw an icon, a text message, and a close button. Subclasses can +// override addAdditionalControls to customize the UI. +@interface InfoBarController : NSViewController { + @private + InfoBarContainerController* containerController_; // weak, owns us + + @protected + InfoBarDelegate* delegate_; // weak + IBOutlet NSImageView* image_; + IBOutlet NSTextField* label_; + IBOutlet NSButton* closeButton_; +}; + +// Initializes a new InfoBarController. +- (id)initWithDelegate:(InfoBarDelegate*)delegate; + +// Dismisses the infobar without taking any action. +- (IBAction)dismiss:(id)sender; + +// Subclasses can override this method to add additional controls to +// the infobar view. This method is called by awakeFromNib. The +// default implementation does nothing. +- (void)addAdditionalControls; + +@property(assign, nonatomic) InfoBarContainerController* containerController; +@property(readonly) InfoBarDelegate* delegate; + +@end + +///////////////////////////////////////////////////////////////////////// +// InfoBarController subclasses, one for each InfoBarDelegate +// subclass. Each of these subclasses overrides addAdditionalControls to +// configure its view as necessary. + +@interface AlertInfoBarController : InfoBarController { +} +@end + + +@interface LinkInfoBarController : InfoBarController { +} +// Called when there is a click on the link in the infobar. +- (void)linkClicked; +@end + + +@interface ConfirmInfoBarController : InfoBarController { +} +// Called when the ok and cancel buttons are clicked. +- (IBAction)ok:(id)sender; +- (IBAction)cancel:(id)sender; +@end diff --git a/chrome/browser/cocoa/infobar_controller.mm b/chrome/browser/cocoa/infobar_controller.mm new file mode 100644 index 0000000..5f8083e --- /dev/null +++ b/chrome/browser/cocoa/infobar_controller.mm @@ -0,0 +1,283 @@ +// Copyright (c) 2009 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/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/cocoa/infobar.h" +#import "chrome/browser/cocoa/infobar_container_controller.h" +#import "chrome/browser/cocoa/infobar_controller.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "skia/ext/skia_utils_mac.h" +#include "webkit/glue/window_open_disposition.h" + + +@interface InfoBarController (PrivateMethods) +// Closes the infobar by calling RemoveDelegate on the container. +// This will remove the infobar from its associated TabContents as +// well as trigger the deletion of this InfoBarController. Once the +// delegate is removed from the container, it is no longer needed, so +// we ask it to delete itself. +- (void)closeInfoBar; +@end + +@implementation InfoBarController + +@synthesize containerController = containerController_; +@synthesize delegate = delegate_; + +- (id)initWithDelegate:(InfoBarDelegate*)delegate { + DCHECK(delegate); + if ((self = [super initWithNibName:@"InfoBar" + bundle:mac_util::MainAppBundle()])) { + delegate_ = delegate; + } + return self; +} + +// All infobars have an icon, so we set up the icon in the base class +// awakeFromNib. +- (void)awakeFromNib { + if (delegate_->GetIcon()) { + [image_ setImage:gfx::SkBitmapToNSImage(*(delegate_->GetIcon()))]; + } + + [self addAdditionalControls]; +} + +// Called when someone clicks on the close button. +- (void)dismiss:(id)sender { + [self closeInfoBar]; +} + +- (void)addAdditionalControls { + // Default implementation does nothing. +} + +@end + +@implementation InfoBarController (PrivateMethods) +- (void)closeInfoBar { + // Calling RemoveDelegate() triggers notifications which will remove + // the infobar view from the infobar container. At that point it is + // safe to ask the delegate to delete itself. + DCHECK(delegate_); + [containerController_ removeDelegate:delegate_]; + delegate_->InfoBarClosed(); + delegate_ = NULL; +} +@end + + +///////////////////////////////////////////////////////////////////////// +// AlertInfoBarController implementation + +@implementation AlertInfoBarController + +// Alert infobars have a text message. +- (void)addAdditionalControls { + AlertInfoBarDelegate* delegate = delegate_->AsAlertInfoBarDelegate(); + [label_ setStringValue:base::SysWideToNSString( + delegate->GetMessageText())]; +} + +@end + + +///////////////////////////////////////////////////////////////////////// +// LinkInfoBarController implementation + +@implementation LinkInfoBarController + +// 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. +// +// TODO(rohitrao): Using an NSTextField here has some weird UI side +// effects, such as showing the wrong cursor at times. Explore other +// solutions. +- (void)addAdditionalControls { + LinkInfoBarDelegate* delegate = delegate_->AsLinkInfoBarDelegate(); + size_t offset = std::wstring::npos; + std::wstring message = delegate->GetMessageTextWithOffset(&offset); + + // Create an attributes dictionary for the entire message. We have + // to expicitly set the font to the system font, because + // NSAttributedString defaults to Helvetica 12. We also override + // the cursor to give us the normal cursor rather than the text + // insertion cursor. + NSMutableDictionary* linkAttributes = + [NSMutableDictionary dictionaryWithObject:[NSCursor arrowCursor] + forKey:NSCursorAttributeName]; + [linkAttributes setObject:[NSFont systemFontOfSize:[NSFont systemFontSize]] + forKey:NSFontAttributeName]; + + // Create the attributed string for the main message text. + NSMutableAttributedString* infoText = + [[NSMutableAttributedString alloc] + initWithString:base::SysWideToNSString(message)]; + [infoText addAttributes:linkAttributes + range:NSMakeRange(0, [infoText length])]; + + // Add additional attributes to style the link text appropriately as + // well as linkify it. We use an empty string for the NSLink + // attribute because the actual object we pass doesn't matter, but + // it cannot be nil. + [linkAttributes setObject:[NSColor blueColor] + forKey:NSForegroundColorAttributeName]; + [linkAttributes setObject:[NSNumber numberWithBool:YES] + forKey:NSUnderlineStyleAttributeName]; + [linkAttributes setObject:[NSCursor pointingHandCursor] + forKey:NSCursorAttributeName]; + [linkAttributes setObject:[NSString string] // dummy value + forKey:NSLinkAttributeName]; + + // Insert the link text into the string at the appropriate offset. + [infoText insertAttributedString: + [[[NSAttributedString alloc] + initWithString:base::SysWideToNSString(delegate->GetLinkText()) + attributes:linkAttributes] autorelease] + atIndex:offset]; + + // Update the label view with the new text. The view must be + // selectable and allow editing text attributes for the + // linkification to work correctly. + [label_ setAllowsEditingTextAttributes: YES]; + [label_ setSelectable: YES]; + [label_ setAttributedStringValue:infoText]; +} + +// Called when someone clicks on the link in the infobar. This method +// is called by the InfobarTextField on its delegate (the +// LinkInfoBarController). +- (void)linkClicked { + // TODO(rohitrao): Set the disposition correctly based on modifier keys. + WindowOpenDisposition disposition = CURRENT_TAB; + if (delegate_->AsLinkInfoBarDelegate()->LinkClicked(disposition)) + [self closeInfoBar]; +} + +@end + + +///////////////////////////////////////////////////////////////////////// +// ConfirmInfoBarController implementation + +@implementation ConfirmInfoBarController + +// Called when someone clicks on the "OK" button. +- (IBAction)ok:(id)sender { + if (delegate_->AsConfirmInfoBarDelegate()->Accept()) + [self closeInfoBar]; +} + +// Called when someone clicks on the "Cancel" button. +- (IBAction)cancel:(id)sender { + if (delegate_->AsConfirmInfoBarDelegate()->Cancel()) + [self closeInfoBar]; +} + +// 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(); + [label_ setStringValue:base::SysWideToNSString(delegate->GetMessageText())]; + + int visibleButtons = delegate->GetButtons(); + NSButton *okButton = nil; + NSButton *cancelButton = nil; + + // Create the OK button if needed. + if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_OK) { + okButton = [[[NSButton alloc] initWithFrame:NSZeroRect] autorelease]; + [okButton setBezelStyle:NSRoundedBezelStyle]; + [okButton setTitle:base::SysWideToNSString( + delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_OK))]; + [okButton sizeToFit]; + [okButton setAutoresizingMask:NSViewMinXMargin]; + [okButton setTarget:self]; + [okButton setAction:@selector(ok:)]; + } + + // Create the cancel button if needed. + if (visibleButtons & ConfirmInfoBarDelegate::BUTTON_CANCEL) { + cancelButton = [[[NSButton alloc] initWithFrame:NSZeroRect] autorelease]; + [cancelButton setBezelStyle:NSRoundedBezelStyle]; + [cancelButton setTitle:base::SysWideToNSString( + delegate->GetButtonLabel(ConfirmInfoBarDelegate::BUTTON_CANCEL))]; + [cancelButton sizeToFit]; + [cancelButton setAutoresizingMask:NSViewMinXMargin]; + [cancelButton setTarget:self]; + [cancelButton setAction:@selector(cancel:)]; + } + + // Position the cancel button, if it exists. + int cancelWidth = 0; + if (cancelButton) { + NSRect cancelFrame = [cancelButton frame]; + cancelWidth = cancelFrame.size.width + 10; + + // Position the cancel button to the left of the close button. A 10px + // margin is already built into cancelWidth. + cancelFrame.origin.x = NSMinX([closeButton_ frame]) - cancelWidth; + cancelFrame.origin.y = 0; + [cancelButton setFrame:cancelFrame]; + [[self view] addSubview:cancelButton]; + + // Resize the label box to extend all the way to the cancel button, + // minus a 10px argin. + NSRect labelFrame = [label_ frame]; + labelFrame.size.width = NSMinX(cancelFrame) - 10 - NSMinX(labelFrame); + [label_ setFrame:labelFrame]; + } + + // Position the OK button, if it exists. + if (okButton) { + NSRect okFrame = [okButton frame]; + int okWidth = okFrame.size.width + 10; + + // Position the OK button to the left of the close button as + // well. If a cancel button is present, |cancelWidth| will be positive. + // In either case, a 10px margin is built into okWidth. + okFrame.origin.x = + NSMinX([closeButton_ frame]) - cancelWidth - okWidth; + okFrame.origin.y = 0; + [okButton setFrame:okFrame]; + [[self view] addSubview:okButton]; + + // Resize the label box to extend all the way to the OK button, + // minus a 10px argin. + NSRect labelFrame = [label_ frame]; + labelFrame.size.width = NSMinX(okFrame) - 10 - NSMinX(labelFrame); + [label_ setFrame:labelFrame]; + } +} + +@end + + +////////////////////////////////////////////////////////////////////////// +// CreateInfoBar() implementations + +InfoBar* AlertInfoBarDelegate::CreateInfoBar() { + AlertInfoBarController* controller = + [[AlertInfoBarController alloc] initWithDelegate:this]; + return new InfoBar(controller); +} + +InfoBar* LinkInfoBarDelegate::CreateInfoBar() { + LinkInfoBarController* controller = + [[LinkInfoBarController alloc] initWithDelegate:this]; + return new InfoBar(controller); +} + +InfoBar* ConfirmInfoBarDelegate::CreateInfoBar() { + ConfirmInfoBarController* controller = + [[ConfirmInfoBarController alloc] initWithDelegate:this]; + return new InfoBar(controller); +} diff --git a/chrome/browser/cocoa/infobar_controller_unittest.mm b/chrome/browser/cocoa/infobar_controller_unittest.mm new file mode 100644 index 0000000..c6c45c9 --- /dev/null +++ b/chrome/browser/cocoa/infobar_controller_unittest.mm @@ -0,0 +1,191 @@ +// Copyright (c) 2009 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/scoped_nsobject.h" +#include "base/string_util.h" +#include "base/sys_string_conversions.h" +#import "chrome/browser/cocoa/cocoa_test_helper.h" +#import "chrome/browser/cocoa/infobar_controller.h" +#include "chrome/browser/cocoa/infobar_test_helper.h" +#include "chrome/browser/tab_contents/infobar_delegate.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface InfoBarController (ExposedForTesting) +- (NSTextField*)label; +@end + +@implementation InfoBarController (ExposedForTesting) +- (NSTextField*)label { + return label_; +} +@end + +namespace { + +/////////////////////////////////////////////////////////////////////////// +// Test fixtures + +class AlertInfoBarControllerTest : public PlatformTest { + public: + virtual void SetUp() { + PlatformTest::SetUp(); + + controller_.reset( + [[AlertInfoBarController alloc] initWithDelegate:&delegate_]); + [helper_.contentView() addSubview:[controller_ view]]; + } + + protected: + CocoaTestHelper helper_; + MockAlertInfoBarDelegate delegate_; + scoped_nsobject<AlertInfoBarController> controller_; +}; + +class LinkInfoBarControllerTest : public PlatformTest { + public: + virtual void SetUp() { + PlatformTest::SetUp(); + + controller_.reset( + [[LinkInfoBarController alloc] initWithDelegate:&delegate_]); + [helper_.contentView() addSubview:[controller_ view]]; + } + + protected: + CocoaTestHelper helper_; + MockLinkInfoBarDelegate delegate_; + scoped_nsobject<LinkInfoBarController> controller_; +}; + +class ConfirmInfoBarControllerTest : public PlatformTest { + public: + virtual void SetUp() { + PlatformTest::SetUp(); + + controller_.reset( + [[ConfirmInfoBarController alloc] initWithDelegate:&delegate_]); + [helper_.contentView() addSubview:[controller_ view]]; + } + + protected: + CocoaTestHelper helper_; + MockConfirmInfoBarDelegate delegate_; + scoped_nsobject<ConfirmInfoBarController> controller_; +}; + + +//////////////////////////////////////////////////////////////////////////// +// Tests + +TEST_F(AlertInfoBarControllerTest, ShowAndDismiss) { + // Make sure someone looked at the message and icon. + EXPECT_TRUE(delegate_.message_text_accessed); + EXPECT_TRUE(delegate_.icon_accessed); + + // Check to make sure the infobar message was set properly. + EXPECT_EQ(std::wstring(kMockAlertInfoBarMessage), + base::SysNSStringToWide([[controller_.get() label] stringValue])); + + // Check that dismissing the infobar calls InfoBarClosed() on the delegate. + [controller_ dismiss:nil]; + EXPECT_TRUE(delegate_.closed); +} + +TEST_F(AlertInfoBarControllerTest, DeallocController) { + // Test that dealloc'ing the controller does not send an + // InfoBarClosed() message to the delegate. + controller_.reset(nil); + EXPECT_FALSE(delegate_.closed); +} + +TEST_F(LinkInfoBarControllerTest, ShowAndDismiss) { + // Make sure someone looked at the message, link, and icon. + EXPECT_TRUE(delegate_.message_text_accessed); + EXPECT_TRUE(delegate_.link_text_accessed); + EXPECT_TRUE(delegate_.icon_accessed); + + // Check that dismissing the infobar calls InfoBarClosed() on the delegate. + [controller_ dismiss:nil]; + EXPECT_FALSE(delegate_.link_clicked); + EXPECT_TRUE(delegate_.closed); +} + +TEST_F(LinkInfoBarControllerTest, ShowAndClickLink) { + // Check that clicking on the link calls LinkClicked() on the + // delegate. It should also close the infobar. + [controller_ linkClicked]; + EXPECT_TRUE(delegate_.link_clicked); + EXPECT_TRUE(delegate_.closed); +} + +TEST_F(LinkInfoBarControllerTest, ShowAndClickLinkWithoutClosing) { + delegate_.closes_on_action = false; + + // Check that clicking on the link calls LinkClicked() on the + // delegate. It should not close the infobar. + [controller_ linkClicked]; + EXPECT_TRUE(delegate_.link_clicked); + EXPECT_FALSE(delegate_.closed); +} + +TEST_F(ConfirmInfoBarControllerTest, ShowAndDismiss) { + // Make sure someone looked at the message and icon. + EXPECT_TRUE(delegate_.message_text_accessed); + EXPECT_TRUE(delegate_.icon_accessed); + + // Check to make sure the infobar message was set properly. + EXPECT_EQ(std::wstring(kMockConfirmInfoBarMessage), + base::SysNSStringToWide([[controller_.get() label] stringValue])); + + // Check that dismissing the infobar calls InfoBarClosed() on the delegate. + [controller_ dismiss:nil]; + EXPECT_FALSE(delegate_.ok_clicked); + EXPECT_FALSE(delegate_.cancel_clicked); + EXPECT_TRUE(delegate_.closed); +} + +TEST_F(ConfirmInfoBarControllerTest, ShowAndClickOK) { + // Check that clicking the OK button calls Accept() and then closes + // the infobar. + [controller_ ok:nil]; + EXPECT_TRUE(delegate_.ok_clicked); + EXPECT_FALSE(delegate_.cancel_clicked); + EXPECT_TRUE(delegate_.closed); +} + +TEST_F(ConfirmInfoBarControllerTest, ShowAndClickOKWithoutClosing) { + delegate_.closes_on_action = false; + + // Check that clicking the OK button calls Accept() but does not close + // the infobar. + [controller_ ok:nil]; + EXPECT_TRUE(delegate_.ok_clicked); + EXPECT_FALSE(delegate_.cancel_clicked); + EXPECT_FALSE(delegate_.closed); +} + +TEST_F(ConfirmInfoBarControllerTest, ShowAndClickCancel) { + // Check that clicking the cancel button calls Cancel() and closes + // the infobar. + [controller_ cancel:nil]; + EXPECT_FALSE(delegate_.ok_clicked); + EXPECT_TRUE(delegate_.cancel_clicked); + EXPECT_TRUE(delegate_.closed); +} + +TEST_F(ConfirmInfoBarControllerTest, ShowAndClickCancelWithoutClosing) { + delegate_.closes_on_action = false; + + // Check that clicking the cancel button calls Cancel() but does not close + // the infobar. + [controller_ cancel:nil]; + EXPECT_FALSE(delegate_.ok_clicked); + EXPECT_TRUE(delegate_.cancel_clicked); + EXPECT_FALSE(delegate_.closed); +} + +} // namespace diff --git a/chrome/browser/cocoa/infobar_test_helper.h b/chrome/browser/cocoa/infobar_test_helper.h new file mode 100644 index 0000000..e8f5876 --- /dev/null +++ b/chrome/browser/cocoa/infobar_test_helper.h @@ -0,0 +1,152 @@ +// Copyright (c) 2009 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. + +#include "chrome/browser/tab_contents/infobar_delegate.h" + +namespace { +const wchar_t* kMockAlertInfoBarMessage = L"MockAlertInfoBarMessage"; +const wchar_t* kMockLinkInfoBarMessage = L"MockLinkInfoBarMessage"; +const wchar_t* kMockLinkInfoBarLink = L"http://dev.chromium.org"; +const wchar_t* kMockConfirmInfoBarMessage = L"MockConfirmInfoBarMessage"; +} + +////////////////////////////////////////////////////////////////////////// +// Mock InfoBarDelgates + +class MockAlertInfoBarDelegate : public AlertInfoBarDelegate { + public: + explicit MockAlertInfoBarDelegate() + : AlertInfoBarDelegate(NULL), + message_text_accessed(false), + icon_accessed(false), + closed(false) { + } + + virtual std::wstring GetMessageText() const { + message_text_accessed = true; + return kMockAlertInfoBarMessage; + } + + virtual SkBitmap* GetIcon() const { + icon_accessed = true; + return NULL; + } + + virtual void InfoBarClosed() { + closed = true; + } + + // These are declared mutable to get around const-ness issues. + mutable bool message_text_accessed; + mutable bool icon_accessed; + bool closed; +}; + +class MockLinkInfoBarDelegate : public LinkInfoBarDelegate { + public: + explicit MockLinkInfoBarDelegate() + : LinkInfoBarDelegate(NULL), + message_text_accessed(false), + link_text_accessed(false), + icon_accessed(false), + link_clicked(false), + closed(false), + closes_on_action(true) { + } + + virtual std::wstring GetMessageTextWithOffset(size_t* link_offset) const { + message_text_accessed = true; + *link_offset = 1; + return kMockLinkInfoBarMessage; + } + + virtual std::wstring GetLinkText() const { + link_text_accessed = true; + return kMockLinkInfoBarLink; + } + + virtual SkBitmap* GetIcon() const { + icon_accessed = true; + return NULL; + } + + virtual bool LinkClicked(WindowOpenDisposition disposition) { + link_clicked = true; + return closes_on_action; + } + + virtual void InfoBarClosed() { + closed = true; + } + + // These are declared mutable to get around const-ness issues. + mutable bool message_text_accessed; + mutable bool link_text_accessed; + mutable bool icon_accessed; + bool link_clicked; + bool closed; + + // Determines whether the infobar closes when an action is taken or not. + bool closes_on_action; +}; + +class MockConfirmInfoBarDelegate : public ConfirmInfoBarDelegate { + public: + explicit MockConfirmInfoBarDelegate() + : ConfirmInfoBarDelegate(NULL), + message_text_accessed(false), + link_text_accessed(false), + icon_accessed(false), + ok_clicked(false), + cancel_clicked(false), + closed(false), + closes_on_action(true) { + } + + virtual int GetButtons() const { + return (BUTTON_OK | BUTTON_CANCEL); + } + + virtual std::wstring GetButtonLabel(InfoBarButton button) const { + if (button == BUTTON_OK) + return L"OK"; + else + return L"Cancel"; + } + + virtual bool Accept() { + ok_clicked = true; + return closes_on_action; + } + + virtual bool Cancel() { + cancel_clicked = true; + return closes_on_action; + } + + virtual std::wstring GetMessageText() const { + message_text_accessed = true; + return kMockConfirmInfoBarMessage; + } + + virtual SkBitmap* GetIcon() const { + icon_accessed = true; + return NULL; + } + + virtual void InfoBarClosed() { + closed = true; + } + + // These are declared mutable to get around const-ness issues. + mutable bool message_text_accessed; + mutable bool link_text_accessed; + mutable bool icon_accessed; + bool ok_clicked; + bool cancel_clicked; + bool closed; + + // Determines whether the infobar closes when an action is taken or not. + bool closes_on_action; +}; diff --git a/chrome/browser/cocoa/infobar_text_field.h b/chrome/browser/cocoa/infobar_text_field.h new file mode 100644 index 0000000..6f7d482 --- /dev/null +++ b/chrome/browser/cocoa/infobar_text_field.h @@ -0,0 +1,14 @@ +// Copyright (c) 2009 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> + +// A subclass of NSTextField that allows us to customize NSTextView +// delegate method implementations. We override +// textView:clickedOnLink:atIndex: to handle users clicking on URLs +// embedded in LinkInfoBars. +@interface InfoBarTextField : NSTextField { +} +@end + diff --git a/chrome/browser/cocoa/infobar_text_field.mm b/chrome/browser/cocoa/infobar_text_field.mm new file mode 100644 index 0000000..accea6a --- /dev/null +++ b/chrome/browser/cocoa/infobar_text_field.mm @@ -0,0 +1,20 @@ +// Copyright (c) 2009 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> + +#import "chrome/browser/cocoa/infobar_text_field.h" + +@implementation InfoBarTextField + +- (BOOL)textView:(NSTextView*)aTextView + clickedOnLink:(id)link + atIndex:(NSUInteger)charIndex { + if ([[self delegate] respondsToSelector:@selector(linkClicked)]) + [[self delegate] performSelector:@selector(linkClicked)]; + + return YES; // We handled the click, so Cocoa does not need to do anything. +} + +@end diff --git a/chrome/browser/cocoa/infobar_text_field_unittest.mm b/chrome/browser/cocoa/infobar_text_field_unittest.mm new file mode 100644 index 0000000..5df9e3f --- /dev/null +++ b/chrome/browser/cocoa/infobar_text_field_unittest.mm @@ -0,0 +1,67 @@ +// Copyright (c) 2009 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 "chrome/browser/cocoa/cocoa_test_helper.h" +#import "chrome/browser/cocoa/infobar_text_field.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface TextFieldDelegatePong: NSObject { + @private + BOOL pong_; +} +@property(readonly) BOOL pong; +@end + +@implementation TextFieldDelegatePong +@synthesize pong = pong_; + +- (id)init { + if ((self == [super init])) { + pong_ = NO; + } + return self; +} + +- (void)linkClicked { + pong_ = YES; +} +@end + +namespace { + +/////////////////////////////////////////////////////////////////////////// +// Test fixtures + +class InfoBarTextFieldTest : public PlatformTest { + protected: + CocoaTestHelper helper_; +}; + +//////////////////////////////////////////////////////////////////////////// +// Tests + +TEST_F(InfoBarTextFieldTest, Show) { + // Test basic drawing. + scoped_nsobject<InfoBarTextField> field( + [[InfoBarTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)]); + [helper_.contentView() addSubview:field]; +} + +TEST_F(InfoBarTextFieldTest, LinkClicked) { + scoped_nsobject<InfoBarTextField> field( + [[InfoBarTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)]); + scoped_nsobject<TextFieldDelegatePong> delegate( + [[TextFieldDelegatePong alloc] init]); + [field setDelegate:delegate]; + + // Our implementation doesn't look at any of these fields, so they + // can all be nil. + [field textView:nil clickedOnLink:nil atIndex:0]; + EXPECT_TRUE([delegate pong]); +} + +} // namespace diff --git a/chrome/browser/cocoa/toolbar_controller.h b/chrome/browser/cocoa/toolbar_controller.h index fc93070..73891ec 100644 --- a/chrome/browser/cocoa/toolbar_controller.h +++ b/chrome/browser/cocoa/toolbar_controller.h @@ -42,6 +42,7 @@ class ToolbarView; scoped_nsobject<BookmarkBarController> bookmarkBarController_; id<BookmarkURLOpener> bookmarkBarDelegate_; // weak NSView* webContentView_; // weak; where the web goes + NSView* infoBarsView_; // weak; where the infobars go // Used for monitoring the optional toolbar button prefs. scoped_ptr<ToolbarControllerInternal::PrefObserverBridge> prefObserver_; @@ -72,6 +73,7 @@ class ToolbarView; commands:(CommandUpdater*)commands profile:(Profile*)profile webContentView:(NSView*)webContentView + infoBarsView:(NSView*)infoBarsView bookmarkDelegate:(id<BookmarkURLOpener>)delegate; // Get the C++ bridge object representing the location bar for this tab. diff --git a/chrome/browser/cocoa/toolbar_controller.mm b/chrome/browser/cocoa/toolbar_controller.mm index bb28cfa..681795b 100644 --- a/chrome/browser/cocoa/toolbar_controller.mm +++ b/chrome/browser/cocoa/toolbar_controller.mm @@ -55,6 +55,7 @@ class PrefObserverBridge : public NotificationObserver { commands:(CommandUpdater*)commands profile:(Profile*)profile webContentView:(NSView*)webContentView + infoBarsView:(NSView*)infoBarsView bookmarkDelegate:(id<BookmarkURLOpener>)delegate { DCHECK(model && commands && profile); if ((self = [super initWithNibName:@"Toolbar" @@ -64,6 +65,7 @@ class PrefObserverBridge : public NotificationObserver { profile_ = profile; bookmarkBarDelegate_ = delegate; webContentView_ = webContentView; + infoBarsView_ = infoBarsView; hasToolbar_ = YES; // Register for notifications about state changes for the toolbar buttons @@ -109,6 +111,7 @@ class PrefObserverBridge : public NotificationObserver { initWithProfile:profile_ parentView:[self view] webContentView:webContentView_ + infoBarsView:infoBarsView_ delegate:bookmarkBarDelegate_]); // Add bookmark bar to the view hierarchy. This also triggers the diff --git a/chrome/browser/cocoa/toolbar_controller_unittest.mm b/chrome/browser/cocoa/toolbar_controller_unittest.mm index 58493a2..a3a7649 100644 --- a/chrome/browser/cocoa/toolbar_controller_unittest.mm +++ b/chrome/browser/cocoa/toolbar_controller_unittest.mm @@ -37,6 +37,7 @@ class ToolbarControllerTest : public testing::Test { commands:browser->command_updater() profile:helper_.profile() webContentView:nil + infoBarsView:nil bookmarkDelegate:nil]); EXPECT_TRUE([bar_ view]); NSView* parent = [cocoa_helper_.window() contentView]; |