blob: 6df869f64d493445850cd42fd29d80ca1790a210 [file] [log] [blame]
// 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/mac_util.h"
#include "base/sys_string_conversions.h"
#include "chrome/app/chrome_dll_resource.h" // IDC_*
#include "chrome/browser/browser.h"
#include "chrome/browser/browser_list.h"
#include "chrome/browser/tab_contents/web_contents.h"
#include "chrome/browser/tab_contents/tab_contents_view.h"
#include "chrome/browser/tabs/tab_strip_model.h"
#import "chrome/browser/cocoa/bookmark_bar_controller.h"
#import "chrome/browser/cocoa/browser_window_cocoa.h"
#import "chrome/browser/cocoa/browser_window_controller.h"
#import "chrome/browser/cocoa/find_bar_cocoa_controller.h"
#include "chrome/browser/cocoa/find_bar_bridge.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"
#import "chrome/browser/cocoa/tab_strip_controller.h"
#import "chrome/browser/cocoa/tab_view.h"
#import "chrome/browser/cocoa/toolbar_controller.h"
namespace {
// Size of the gradient. Empirically determined so that the gradient looks
// like what the heuristic does when there are just a few tabs.
const int kWindowGradientHeight = 24;
}
@interface BrowserWindowController(Private)
- (void)positionToolbar;
// Leopard's gradient heuristic gets confused by our tabs and makes the title
// gradient jump when creating a tab that is less than a tab width from the
// right side of the screen. This function disables Leopard's gradient
// heuristic.
- (void)fixWindowGradient;
// Called by the Notification Center whenever the tabContentArea's
// frame changes. Re-positions the bookmark bar and the find bar.
- (void)tabContentAreaFrameChanged:(id)sender;
// We need to adjust where sheets come out of the window, as by default they
// erupt from the omnibox, which is rather weird.
- (NSRect)window:(NSWindow *)window
willPositionSheet:(NSWindow *)sheet
usingRect:(NSRect)defaultSheetRect;
@end
@implementation BrowserWindowController
// Load the browser window nib and do any Cocoa-specific initialization.
// Takes ownership of |browser|. Note that the nib also sets this controller
// up as the window's delegate.
- (id)initWithBrowser:(Browser*)browser {
return [self initWithBrowser:browser takeOwnership:YES];
}
// Private (TestingAPI) init routine with testing options.
- (id)initWithBrowser:(Browser*)browser takeOwnership:(BOOL)ownIt {
// Use initWithWindowNibPath:: instead of initWithWindowNibName: so we
// can override it in a unit test.
NSString *nibpath = [mac_util::MainAppBundle()
pathForResource:@"BrowserWindow"
ofType:@"nib"];
if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
DCHECK(browser);
browser_.reset(browser);
ownsBrowser_ = ownIt;
tabObserver_.reset(
new TabStripModelObserverBridge(browser->tabstrip_model(), self));
windowShim_.reset(new BrowserWindowCocoa(browser, self, [self window]));
// The window is now fully realized and |-windowDidLoad:| has been
// called. We shouldn't do much in wDL because |windowShim_| won't yet
// be initialized (as it's called in response to |[self window]| above).
// Retain it per the comment in the header.
window_.reset([[self window] retain]);
// 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. The window shim will handle
// flipping the coordinates for us so we can use it to save some code.
gfx::Rect windowRect = browser_->GetSavedWindowBounds();
windowShim_->SetBounds(windowRect);
// Create a controller for the tab strip, giving it the model object for
// this window's Browser and the tab strip view. The controller will handle
// registering for the appropriate tab notifications from the back-end and
// managing the creation of new tabs.
tabStripController_.reset([[TabStripController alloc]
initWithView:[self tabStripView]
switchView:[self tabContentArea]
model:browser_->tabstrip_model()]);
// 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.
toolbarController_.reset([[ToolbarController alloc]
initWithModel:browser->toolbar_model()
commands:browser->command_updater()
profile:browser->profile()]);
[self positionToolbar];
// After we've adjusted the toolbar, create a controller for the bookmark
// bar. It will show/hide itself based on the global preference and handle
// positioning itself (if visible) above the content area, which is why
// we need to do it after we've placed the toolbar.
bookmarkController_.reset([[BookmarkBarController alloc]
initWithProfile:browser_->profile()
contentArea:[self tabContentArea]]);
[self fixWindowGradient];
// Create the bridge for the status bubble.
statusBubble_.reset(new StatusBubbleMac([self window]));
}
return self;
}
- (void)dealloc {
browser_->CloseAllTabs();
// Under certain testing configurations we may not actually own the browser.
if (ownsBrowser_ == NO)
browser_.release();
[super dealloc];
}
// Access the C++ bridge between the NSWindow and the rest of Chromium
- (BrowserWindow*)browserWindow {
return windowShim_.get();
}
- (void)destroyBrowser {
[NSApp removeWindowsItem:[self window]];
// We need the window to go away now.
[self autorelease];
}
// Called when the window meets the criteria to be closed (ie,
// |-windowShoudlClose:| returns YES). We must be careful to preserve the
// semantics of BrowserWindow::Close() and not call the Browser's dtor directly
// from this method.
- (void)windowWillClose:(NSNotification *)notification {
DCHECK(!browser_->tabstrip_model()->count());
// We can't acutally use |-autorelease| here because there's an embedded
// run loop in the |-performClose:| which contains its own autorelease pool.
// Instead we use call it after a zero-length delay, which gets us back
// to the main event loop.
[self performSelector:@selector(autorelease)
withObject:nil
afterDelay:0];
}
// Called when the user wants to close a window or from the shutdown process.
// The Browser object is in control of whether or not we're allowed to close. It
// may defer closing due to several states, such as onUnload handlers needing to
// be fired. If closing is deferred, the Browser will handle the processing
// required to get us to the closing state and (by watching for all the tabs
// going away) will again call to close the window when it's finally ready.
- (BOOL)windowShouldClose:(id)sender {
// Give beforeunload handlers the chance to cancel the close before we hide
// the window below.
if (!browser_->ShouldCloseWindow())
return NO;
if (!browser_->tabstrip_model()->empty()) {
// Tab strip isn't empty. Hide the frame (so it appears to have closed
// immediately) and close all the tabs, allowing the renderers to shut
// down. When the tab strip is empty we'll be called back again.
[[self window] orderOut:self];
browser_->OnWindowClosing();
return NO;
}
// the tab strip is empty, it's ok to close the window
return YES;
}
// Called right after our window became the main window.
- (void)windowDidBecomeMain:(NSNotification *)notification {
BrowserList::SetLastActive(browser_.get());
}
// Update a toggle state for an NSMenuItem if modified.
// Take care to insure |item| looks like a NSMenuItem.
// Called by validateUserInterfaceItem:.
- (void)updateToggleStateWithTag:(NSInteger)tag forItem:(id)item {
if (![item respondsToSelector:@selector(state)] ||
![item respondsToSelector:@selector(setState:)])
return;
// On Windows this logic happens in bookmark_bar_view.cc. On the
// Mac we're a lot more MVC happy so we've moved it into a
// controller. To be clear, this simply updates the menu item; it
// does not display the bookmark bar itself.
if (tag == IDC_SHOW_BOOKMARK_BAR) {
bool toggled = windowShim_->IsBookmarkBarVisible();
NSInteger oldState = [item state];
NSInteger newState = toggled ? NSOnState : NSOffState;
if (oldState != newState)
[item setState:newState];
}
}
// Called to validate menu and toolbar items when this window is key. All the
// items we care about have been set with the |commandDispatch:| action and
// a target of FirstResponder in IB. If it's not one of those, let it
// continue up the responder chain to be handled elsewhere. We pull out the
// tag as the cross-platform constant to differentiate and dispatch the
// various commands.
// NOTE: we might have to handle state for app-wide menu items,
// although we could cheat and directly ask the app controller if our
// command_updater doesn't support the command. This may or may not be an issue,
// too early to tell.
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
SEL action = [item action];
BOOL enable = NO;
if (action == @selector(commandDispatch:)) {
NSInteger tag = [item tag];
if (browser_->command_updater()->SupportsCommand(tag)) {
// Generate return value (enabled state)
enable = browser_->command_updater()->IsCommandEnabled(tag) ? YES : NO;
// If the item is toggleable, find it's toggle state and
// try to update it. This is a little awkward, but the alternative is
// to check after a commandDispatch, which seems worse.
[self updateToggleStateWithTag:tag forItem:item];
}
}
return enable;
}
// Called when the user picks a menu or toolbar item when this window is key.
// Calls through to the browser object to execute the command. This assumes that
// the command is supported and doesn't check, otherwise it would have been
// disabled in the UI in validateUserInterfaceItem:.
- (void)commandDispatch:(id)sender {
NSInteger tag = [sender tag];
browser_->ExecuteCommand(tag);
}
- (LocationBar*)locationBar {
return [toolbarController_ locationBar];
}
- (StatusBubble*)statusBubble {
return statusBubble_.get();
}
- (void)updateToolbarWithContents:(TabContents*)tab
shouldRestoreState:(BOOL)shouldRestore {
[toolbarController_ updateToolbarWithContents:shouldRestore ? tab : NULL];
}
- (void)setStarredState:(BOOL)isStarred {
[toolbarController_ setStarredState:isStarred];
}
// Return the rect, in WebKit coordinates (flipped), of the window's grow box
// in the coordinate system of the content area of the currently selected tab.
// |windowGrowBox| needs to be in the window's coordinate system.
- (NSRect)selectedTabGrowBoxRect {
return [tabStripController_ selectedTabGrowBoxRect];
}
// Drop a given tab view at the location of the current placeholder. If there
// is no placeholder, it will go at the end. |dragController| is the window
// controller of a tab being dropped from a different window. It will be nil
// if the drag is within the window. The implementation will call
// |-removePlaceholder| since the drag is now complete. This also calls
// |-layoutTabs| internally so clients do not need to call it again. When
// dragging tabs between windows, this should be called *before*
// |-detachTabView| on the source window since it needs to still be in the
// source window's tab model for this method to find the information it needs
// to complete the drop.
- (void)dropTabView:(NSView*)view
fromController:(TabWindowController*)dragController {
if (dragController) {
// Moving between windows. Figure out the TabContents to drop into our tab
// model from the source window's model.
BOOL isBrowser =
[dragController isKindOfClass:[BrowserWindowController class]];
DCHECK(isBrowser);
if (!isBrowser) return;
BrowserWindowController* dragBWC = (BrowserWindowController*)dragController;
int index = [dragBWC->tabStripController_ indexForTabView:view];
TabContents* contents =
dragBWC->browser_->tabstrip_model()->GetTabContentsAt(index);
// Deposit it into our model at the appropriate location (it already knows
// where it should go from tracking the drag).
[tabStripController_ dropTabContents:contents];
} else {
// Moving within a window.
int index = [tabStripController_ indexForTabView:view];
[tabStripController_ moveTabFromIndex:index];
}
// Remove the placeholder since the drag is now complete.
[self removePlaceholder];
}
// Tells the tab strip to forget about this tab in preparation for it being
// put into a different tab strip, such as during a drop on another window.
- (void)detachTabView:(NSView*)view {
int index = [tabStripController_ indexForTabView:view];
browser_->tabstrip_model()->DetachTabContentsAt(index);
}
- (NSView *)selectedTabView {
return [tabStripController_ selectedTabView];
}
- (void)setIsLoading:(BOOL)isLoading {
[toolbarController_ setIsLoading:isLoading];
}
// Called to start/stop the loading animations.
- (void)updateLoadingAnimations:(BOOL)animate {
if (animate) {
// TODO(pinkerton): determine what throbber animation is necessary and
// start a timer to periodically update. Windows tells the tab strip to
// do this. It uses a single timer to coalesce the multiple things that
// could be updating. http://crbug.com/8281
} else {
// TODO(pinkerton): stop the timer.
}
}
// Make the location bar the first responder, if possible.
- (void)focusLocationBar {
[toolbarController_ focusLocationBar];
}
- (void)layoutTabs {
[tabStripController_ layoutTabs];
}
- (TabWindowController*)detachTabToNewWindow:(TabView*)tabView {
// Fetch the tab contents for the tab being dragged
int index = [tabStripController_ indexForTabView:tabView];
TabContents* contents = browser_->tabstrip_model()->GetTabContentsAt(index);
// Set the window size. Need to do this before we detach the tab so it's
// still in the window. We have to flip the coordinates as that's what
// is expected by the Browser code.
NSWindow* sourceWindow = [tabView window];
NSRect windowRect = [sourceWindow frame];
NSScreen* screen = [sourceWindow screen];
windowRect.origin.y =
[screen frame].size.height - windowRect.size.height -
windowRect.origin.y;
gfx::Rect browserRect(windowRect.origin.x, windowRect.origin.y,
windowRect.size.width, windowRect.size.height);
NSRect tabRect = [tabView frame];
// Detach it from the source window, which just updates the model without
// deleting the tab contents. This needs to come before creating the new
// Browser because it clears the TabContents' delegate, which gets hooked
// up during creation of the new window.
browser_->tabstrip_model()->DetachTabContentsAt(index);
// Create the new window with a single tab in its model, the one being
// dragged.
DockInfo dockInfo;
Browser* newBrowser =
browser_->tabstrip_model()->TearOffTabContents(contents,
browserRect,
dockInfo);
// Get the new controller by asking the new window for its delegate.
TabWindowController* controller =
[newBrowser->window()->GetNativeHandle() delegate];
DCHECK(controller && [controller isKindOfClass:[TabWindowController class]]);
// Force the added tab to the right size (remove stretching)
tabRect.size.height = [TabStripController defaultTabHeight];
NSView *newTabView = [controller selectedTabView];
[newTabView setFrame:tabRect];
return controller;
}
- (void)insertPlaceholderForTab:(TabView*)tab
frame:(NSRect)frame
yStretchiness:(CGFloat)yStretchiness {
[tabStripController_ insertPlaceholderForTab:tab
frame:frame
yStretchiness:yStretchiness];
}
- (void)removePlaceholder {
[tabStripController_ insertPlaceholderForTab:nil
frame:NSZeroRect
yStretchiness:0];
}
- (BOOL)isBookmarkBarVisible {
return [bookmarkController_ isBookmarkBarVisible];
}
- (void)toggleBookmarkBar {
[bookmarkController_ toggleBookmarkBar];
}
- (void)addFindBar:(FindBarCocoaController*)findBarCocoaController {
// Shouldn't call addFindBar twice.
DCHECK(!findBarCocoaController_.get());
// Create a controller for the findbar.
findBarCocoaController_.reset([findBarCocoaController retain]);
[[[self window] contentView] addSubview:[findBarCocoaController_ view]];
[findBarCocoaController_ positionFindBarView:[self tabContentArea]];
}
- (NSInteger)numberOfTabs {
return browser_->tabstrip_model()->count();
}
- (NSString*)selectedTabTitle {
TabContents* contents = browser_->tabstrip_model()->GetSelectedTabContents();
return base::SysUTF16ToNSString(contents->GetTitle());
}
- (void)selectTabWithContents:(TabContents*)newContents
previousContents:(TabContents*)oldContents
atIndex:(NSInteger)index
userGesture:(bool)wasUserGesture {
DCHECK(oldContents != newContents);
// We do not store the focus when closing the tab to work-around bug 4633.
// Some reports seem to show that the focus manager and/or focused view can
// be garbage at that point, it is not clear why.
if (oldContents && !oldContents->is_being_destroyed() &&
oldContents->AsWebContents())
oldContents->AsWebContents()->view()->StoreFocus();
// Update various elements that are interested in knowing the current
// TabContents.
#if 0
// TODO(pinkerton):Update as more things become window-specific
infobar_container_->ChangeTabContents(new_contents);
contents_container_->SetTabContents(new_contents);
#endif
newContents->DidBecomeSelected();
// Change the entry in the Window menu to match the title of the
// currently selected tab. This will create an entry if one does
// not already exist.
[NSApp changeWindowsItem:[self window]
title:base::SysUTF16ToNSString(newContents->GetTitle())
filename:NO];
if (BrowserList::GetLastActive() == browser_ &&
!browser_->tabstrip_model()->closing_all() &&
newContents->AsWebContents()) {
newContents->AsWebContents()->view()->RestoreFocus();
}
#if 0
// TODO(pinkerton):Update as more things become window-specific
// Update all the UI bits.
UpdateTitleBar();
toolbar_->SetProfile(new_contents->profile());
UpdateToolbar(new_contents, true);
UpdateUIForContents(new_contents);
#endif
}
- (void)tabChangedWithContents:(TabContents*)contents
atIndex:(NSInteger)index
loadingOnly:(BOOL)loading {
// Change the entry in the Window menu to match the new title of the tab,
// but only if this is the currently selected tab.
if (index == browser_->tabstrip_model()->selected_index()) {
[NSApp changeWindowsItem:[self window]
title:base::SysUTF16ToNSString(contents->GetTitle())
filename:NO];
}
}
@end
@interface NSWindow (NSPrivateApis)
// Note: These functions are private, use -[NSObject respondsToSelector:]
// before calling them.
- (void)setAutorecalculatesContentBorderThickness:(BOOL)b
forEdge:(NSRectEdge)e;
- (void)setContentBorderThickness:(CGFloat)b forEdge:(NSRectEdge)e;
@end
@implementation BrowserWindowController (Private)
// Position |toolbarView_| below the tab strip, but not as a sibling. The
// toolbar is part of the window's contentView, mainly because we want the
// opacity during drags to be the same as the web content.
- (void)positionToolbar {
NSView* contentView = [self tabContentArea];
NSRect contentFrame = [contentView frame];
NSView* toolbarView = [toolbarController_ view];
NSRect toolbarFrame = [toolbarView frame];
// Shrink the content area by the height of the toolbar.
contentFrame.size.height -= toolbarFrame.size.height;
[contentView setFrame:contentFrame];
// Move the toolbar above the content area, within the window's content view
// (as opposed to the tab strip, which is a sibling).
toolbarFrame.origin.y = NSMaxY(contentFrame);
toolbarFrame.origin.x = 0;
toolbarFrame.size.width = contentFrame.size.width;
[toolbarView setFrame:toolbarFrame];
[[[self window] contentView] addSubview:toolbarView];
}
- (void)fixWindowGradient {
NSWindow* win = [self window];
if ([win respondsToSelector:@selector(
setAutorecalculatesContentBorderThickness:forEdge:)] &&
[win respondsToSelector:@selector(
setContentBorderThickness:forEdge:)]) {
[win setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
[win setContentBorderThickness:kWindowGradientHeight forEdge:NSMaxYEdge];
}
}
- (void)tabContentAreaFrameChanged:(id)sender {
// TODO(rohitrao): This is triggered by window resizes also. Make
// sure we aren't doing anything wasteful in those cases.
[bookmarkController_ resizeBookmarkBar];
if (findBarCocoaController_.get()) {
[findBarCocoaController_ positionFindBarView:[self tabContentArea]];
}
}
- (NSRect)window:(NSWindow *)window
willPositionSheet:(NSWindow *)sheet
usingRect:(NSRect)defaultSheetRect {
NSRect windowFrame = [window frame];
defaultSheetRect.origin.y = windowFrame.size.height - 10;
return defaultSheetRect;
}
// In addition to the tab strip and content area, which the superview's impl
// takes care of, we need to add the toolbar and bookmark bar to the
// overlay so they draw correctly when dragging out a new window.
- (NSArray*)viewsToMoveToOverlay {
NSArray* views = [super viewsToMoveToOverlay];
NSArray* browserViews =
[NSArray arrayWithObjects:[toolbarController_ view],
[bookmarkController_ view],
nil];
return [views arrayByAddingObjectsFromArray:browserViews];
}
@end