blob: 30f77683dcfb696c5e1153997d9ec3c915168ab7 [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.
#import "chrome/browser/cocoa/tab_strip_controller.h"
#include "app/l10n_util.h"
#include "base/sys_string_conversions.h"
#include "chrome/app/chrome_dll_resource.h"
#include "chrome/browser/browser.h"
#include "chrome/browser/metrics/user_metrics.h"
#include "chrome/browser/profile.h"
#import "chrome/browser/cocoa/tab_strip_view.h"
#import "chrome/browser/cocoa/tab_cell.h"
#import "chrome/browser/cocoa/tab_contents_controller.h"
#import "chrome/browser/cocoa/tab_controller.h"
#import "chrome/browser/cocoa/tab_strip_model_observer_bridge.h"
#import "chrome/browser/cocoa/tab_view.h"
#import "chrome/browser/cocoa/throbber_view.h"
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/browser/tab_contents/tab_contents_view.h"
#include "chrome/browser/tabs/tab_strip_model.h"
#include "grit/generated_resources.h"
@implementation TabStripController
- (id)initWithView:(TabStripView*)view
switchView:(NSView*)switchView
model:(TabStripModel*)model {
DCHECK(view && switchView && model);
if ((self = [super init])) {
tabView_ = view;
switchView_ = switchView;
tabModel_ = model;
bridge_.reset(new TabStripModelObserverBridge(tabModel_, self));
tabContentsArray_.reset([[NSMutableArray alloc] init]);
tabArray_.reset([[NSMutableArray alloc] init]);
// Take the only child view present in the nib as the new tab button. For
// some reason, if the view is present in the nib apriori, it draws
// correctly. If we create it in code and add it to the tab view, it draws
// with all sorts of crazy artifacts.
newTabButton_ = [[tabView_ subviews] objectAtIndex:0];
DCHECK([newTabButton_ isKindOfClass:[NSButton class]]);
[newTabButton_ setTarget:nil];
[newTabButton_ setAction:@selector(commandDispatch:)];
[newTabButton_ setTag:IDC_NEW_TAB];
[tabView_ setWantsLayer:YES];
}
return self;
}
+ (CGFloat)defaultTabHeight {
return 24.0;
}
// Finds the associated TabContentsController at the given |index| and swaps
// out the sole child of the contentArea to display its contents.
- (void)swapInTabAtIndex:(NSInteger)index {
TabContentsController* controller = [tabContentsArray_ objectAtIndex:index];
// Resize the new view to fit the window
NSView* newView = [controller view];
NSRect frame = [switchView_ bounds];
[newView setFrame:frame];
// Remove the old view from the view hierarchy. We know there's only one
// child of |switchView_| because we're the one who put it there. There
// may not be any children in the case of a tab that's been closed, in
// which case there's no swapping going on.
NSArray* subviews = [switchView_ subviews];
if ([subviews count]) {
NSView* oldView = [subviews objectAtIndex:0];
[switchView_ replaceSubview:oldView with:newView];
} else {
[switchView_ addSubview:newView];
}
}
// Create a new tab view and set its cell correctly so it draws the way we want
// it to. It will be sized and positioned by |-layoutTabs| so there's no need to
// set the frame here. This also creates the view as hidden, it will be
// shown during layout.
- (TabController*)newTab {
TabController* controller = [[[TabController alloc] init] autorelease];
[controller setTarget:self];
[controller setAction:@selector(selectTab:)];
[[controller view] setHidden:YES];
return controller;
}
// Returns the number of tabs in the tab strip. This is just the number
// of TabControllers we know about as there's a 1-to-1 mapping from these
// controllers to a tab.
- (NSInteger)numberOfTabViews {
return [tabArray_ count];
}
// Returns the index of the subview |view|. Returns -1 if not present.
- (NSInteger)indexForTabView:(NSView*)view {
NSInteger index = 0;
for (TabController* current in tabArray_.get()) {
if ([current view] == view)
return index;
++index;
}
return -1;
}
// Returns the view at the given index, using the array of TabControllers to
// get the associated view. Returns nil if out of range.
- (NSView*)viewAtIndex:(NSUInteger)index {
if (index >= [tabArray_ count])
return NULL;
return [[tabArray_ objectAtIndex:index] view];
}
// Called when the user clicks a tab. Tell the model the selection has changed,
// which feeds back into us via a notification.
- (void)selectTab:(id)sender {
int index = [self indexForTabView:sender];
if (index >= 0 && tabModel_->ContainsIndex(index))
tabModel_->SelectTabContentsAt(index, true);
}
// Called when the user closes a tab. Asks the model to close the tab.
- (void)closeTab:(id)sender {
int index = [self indexForTabView:sender];
if (tabModel_->ContainsIndex(index)) {
TabContents* contents = tabModel_->GetTabContentsAt(index);
if (contents)
UserMetrics::RecordAction(L"CloseTab_Mouse", contents->profile());
if ([self numberOfTabViews] > 1) {
tabModel_->CloseTabContentsAt(index);
} else {
// Use the standard window close if this is the last tab
// this prevents the tab from being removed from the model until after
// the window dissapears
[[tabView_ window] performClose:nil];
}
}
}
- (void)insertPlaceholderForTab:(TabView*)tab
frame:(NSRect)frame
yStretchiness:(CGFloat)yStretchiness {
placeholderTab_ = tab;
placeholderFrame_ = frame;
placeholderStretchiness_ = yStretchiness;
[self layoutTabs];
}
// Lay out all tabs in the order of their TabContentsControllers, which matches
// the ordering in the TabStripModel. This call isn't that expensive, though
// it is O(n) in the number of tabs. Tabs will animate to their new position
// if the window is visible.
// TODO(pinkerton): Handle drag placeholders via proxy objects, perhaps a
// subclass of TabContentsController with everything stubbed out or by
// abstracting a base class interface.
// TODO(pinkerton): Note this doesn't do too well when the number of min-sized
// tabs would cause an overflow.
- (void)layoutTabs {
const float kIndentLeavingSpaceForControls = 64.0;
const float kTabOverlap = 20.0;
const float kNewTabButtonOffset = 8.0;
const float kMaxTabWidth = [TabController maxTabWidth];
const float kMinTabWidth = [TabController minTabWidth];
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.2];
BOOL visible = [[tabView_ window] isVisible];
float availableWidth =
NSWidth([tabView_ frame]) - NSWidth([newTabButton_ frame]);
float offset = kIndentLeavingSpaceForControls;
const float baseTabWidth =
MAX(MIN((availableWidth - offset) / [tabContentsArray_ count],
kMaxTabWidth),
kMinTabWidth);
CGFloat minX = NSMinX(placeholderFrame_);
NSUInteger i = 0;
NSInteger gap = -1;
for (TabController* tab in tabArray_.get()) {
BOOL isPlaceholder = [[tab view] isEqual:placeholderTab_];
NSRect tabFrame = [[tab view] frame];
tabFrame.size.height = [[self class] defaultTabHeight];
tabFrame.origin.y = 0;
tabFrame.origin.x = offset;
// If the tab is hidden, we consider it a new tab. We make it visible
// and animate it in.
BOOL newTab = [[tab view] isHidden];
if (newTab) {
[[tab view] setHidden:NO];
}
if (isPlaceholder) {
tabFrame.origin.x = placeholderFrame_.origin.x;
tabFrame.size.height += 10.0 * placeholderStretchiness_;
[[tab view] setFrame:tabFrame];
continue;
} else {
// If our left edge is to the left of the placeholder's left, but our mid
// is to the right of it we should slide over to make space for it.
if (placeholderTab_ && gap < 0 && NSMidX(tabFrame) > minX) {
gap = i;
offset += NSWidth(tabFrame);
offset -= kTabOverlap;
tabFrame.origin.x = offset;
}
#if 0
// Animate the tab in by putting it below the horizon.
// TODO(pinkerton/alcor): While this looks nice, it confuses the heck
// out of the window server and causes the window to think that there's
// no tab there. The net result is that dragging the tab also drags
// the window. We need to find another way to do this.
if (newTab && visible) {
[[tab view] setFrame:NSOffsetRect(tabFrame, 0, -NSHeight(tabFrame))];
}
#endif
id frameTarget = visible ? [[tab view] animator] : [tab view];
tabFrame.size.width = [tab selected] ? kMaxTabWidth : baseTabWidth;
[frameTarget setFrame:tabFrame];
}
if (offset < availableWidth) {
offset += NSWidth(tabFrame);
offset -= kTabOverlap;
}
i++;
}
// Move the new tab button into place
[[newTabButton_ animator] setFrameOrigin:
NSMakePoint(MIN(availableWidth, offset + kNewTabButtonOffset), 0)];
if (i > 0) [[newTabButton_ animator] setHidden:NO];
[NSAnimationContext endGrouping];
}
// Handles setting the title of the tab based on the given |contents|. Uses
// a canned string if |contents| is NULL.
- (void)setTabTitle:(NSViewController*)tab withContents:(TabContents*)contents {
NSString* titleString = nil;
if (contents)
titleString = base::SysUTF16ToNSString(contents->GetTitle());
if (![titleString length]) {
titleString =
base::SysWideToNSString(
l10n_util::GetString(IDS_BROWSER_WINDOW_MAC_TAB_UNTITLED));
}
[tab setTitle:titleString];
}
// Called when a notification is received from the model to insert a new tab
// at |index|.
- (void)insertTabWithContents:(TabContents*)contents
atIndex:(NSInteger)index
inForeground:(bool)inForeground {
DCHECK(contents);
DCHECK(index == TabStripModel::kNoTab || tabModel_->ContainsIndex(index));
// TODO(pinkerton): handle tab dragging in here
// Make a new tab. Load the contents of this tab from the nib and associate
// the new controller with |contents| so it can be looked up later.
TabContentsController* contentsController =
[[[TabContentsController alloc] initWithNibName:@"TabContents"
contents:contents]
autorelease];
[tabContentsArray_ insertObject:contentsController atIndex:index];
// Make a new tab and add it to the strip. Keep track of its controller.
TabController* newController = [self newTab];
[tabArray_ insertObject:newController atIndex:index];
NSView* newView = [newController view];
// Set the originating frame to just below the strip so that it animates
// upwards as it's being initially layed out. Oddly, this works while doing
// something similar in |-layoutTabs| confuses the window server.
// TODO(pinkerton): I'm not happy with this animiation either, but it's
// a little better that just sliding over (maybe?).
[newView setFrame:NSOffsetRect([newView frame],
0, -[[self class] defaultTabHeight])];
[tabView_ addSubview:newView
positioned:inForeground ? NSWindowAbove : NSWindowBelow
relativeTo:nil];
[self setTabTitle:newController withContents:contents];
// We don't need to call |-layoutTabs| if the tab will be in the foreground
// because it will get called when the new tab is selected by the tab model.
if (!inForeground) {
[self layoutTabs];
}
}
// Called when a notification is received from the model to select a particular
// tab. Swaps in the toolbar and content area associated with |newContents|.
- (void)selectTabWithContents:(TabContents*)newContents
previousContents:(TabContents*)oldContents
atIndex:(NSInteger)index
userGesture:(bool)wasUserGesture {
// De-select all other tabs and select the new tab.
int i = 0;
for (TabController* current in tabArray_.get()) {
[current setSelected:(i == index) ? YES : NO];
++i;
}
// Make this the top-most tab in the strips's z order.
NSView* selectedTab = [self viewAtIndex:index];
[tabView_ addSubview:selectedTab positioned:NSWindowAbove relativeTo:nil];
// Tell the new tab contents it is about to become the selected tab. Here it
// can do things like make sure the toolbar is up to date.
TabContentsController* newController =
[tabContentsArray_ objectAtIndex:index];
[newController willBecomeSelectedTab];
// Relayout for new tabs and to let the selected tab grow to be larger in
// size than surrounding tabs if the user has many.
[self layoutTabs];
if (oldContents)
oldContents->view()->StoreFocus();
// Swap in the contents for the new tab
[self swapInTabAtIndex:index];
if (newContents)
newContents->view()->RestoreFocus();
}
// Called when a notification is received from the model that the given tab
// has gone away. Remove all knowledge about this tab and it's associated
// controller and remove the view from the strip.
- (void)tabDetachedWithContents:(TabContents*)contents
atIndex:(NSInteger)index {
// Release the tab contents controller so those views get destroyed. This
// will remove all the tab content Cocoa views from the hierarchy. A
// subsequent "select tab" notification will follow from the model. To
// tell us what to swap in in its absence.
[tabContentsArray_ removeObjectAtIndex:index];
// Remove the |index|th view from the tab strip
NSView* tab = [self viewAtIndex:index];
[tab removeFromSuperview];
// Once we're totally done with the tab, delete its controller
[tabArray_ removeObjectAtIndex:index];
[self layoutTabs];
}
// A helper routine for creating an NSImageView to hold the fav icon for
// |contents|.
// TODO(pinkerton): fill in with code to use the real favicon, not the default
// for all cases.
- (NSImageView*)favIconImageViewForContents:(TabContents*)contents {
NSRect iconFrame = NSMakeRect(0, 0, 16, 16);
NSImageView* view = [[[NSImageView alloc] initWithFrame:iconFrame]
autorelease];
[view setImage:[NSImage imageNamed:@"nav"]];
return view;
}
// Called when a notification is received from the model that the given tab
// has been updated. |loading| will be YES when we only want to update the
// throbber state, not anything else about the (partially) loading tab.
- (void)tabChangedWithContents:(TabContents*)contents
atIndex:(NSInteger)index
loadingOnly:(BOOL)loading {
if (!loading)
[self setTabTitle:[tabArray_ objectAtIndex:index] withContents:contents];
// Update the current loading state, replacing the icon with a throbber, or
// vice versa. This will get called repeatedly with the same state during a
// load, so we need to make sure we're not creating the throbber view over and
// over.
if (contents) {
static NSImage* throbberImage = [[NSImage imageNamed:@"throbber"] retain];
static NSImage* throbberWaitingImage =
[[NSImage imageNamed:@"throbber_waiting"] retain];
TabController* tabController = [tabArray_ objectAtIndex:index];
NSImage* image = nil;
if (contents->waiting_for_response() && ![tabController waiting]) {
image = throbberWaitingImage;
[tabController setWaiting:YES];
} else if (contents->is_loading() && ![tabController loading]) {
image = throbberImage;
[tabController setLoading:YES];
}
if (image) {
NSRect frame = NSMakeRect(0, 0, 16, 16);
ThrobberView* throbber =
[[[ThrobberView alloc] initWithFrame:frame image:image] autorelease];
[tabController setIconView:throbber];
}
else if (!contents->is_loading()) {
// Set everything back to normal, we're done loading.
[tabController setIconView:[self favIconImageViewForContents:contents]];
[tabController setWaiting:NO];
[tabController setLoading:NO];
}
}
TabContentsController* updatedController =
[tabContentsArray_ objectAtIndex:index];
[updatedController tabDidChange:contents];
}
// Called when a tab is moved (usually by drag&drop). Keep our parallel arrays
// in sync with the tab strip model.
- (void)tabMovedWithContents:(TabContents*)contents
fromIndex:(NSInteger)from
toIndex:(NSInteger)to {
scoped_nsobject<TabContentsController> movedController(
[[tabContentsArray_ objectAtIndex:from] retain]);
[tabContentsArray_ removeObjectAtIndex:from];
[tabContentsArray_ insertObject:movedController.get() atIndex:to];
scoped_nsobject<TabView> movedView(
[[tabArray_ objectAtIndex:from] retain]);
[tabArray_ removeObjectAtIndex:from];
[tabArray_ insertObject:movedView.get() atIndex:to];
[self layoutTabs];
}
- (NSView *)selectedTabView {
int selectedIndex = tabModel_->selected_index();
return [self viewAtIndex:selectedIndex];
}
// Find the index based on the x coordinate of the placeholder. If there is
// no placeholder, this returns the end of the tab strip.
- (int)indexOfPlaceholder {
double placeholderX = placeholderFrame_.origin.x;
int index = 0;
int location = 0;
const int count = tabModel_->count();
while (index < count) {
NSView* curr = [self viewAtIndex:index];
// The placeholder tab works by changing the frame of the tab being dragged
// to be the bounds of the placeholder, so we need to skip it while we're
// iterating, otherwise we'll end up off by one. Note This only effects
// dragging to the right, not to the left.
if (curr == placeholderTab_) {
index++;
continue;
}
if (placeholderX <= NSMinX([curr frame]))
break;
index++;
location++;
}
return location;
}
// Move the given tab at index |from| in this window to the location of the
// current placeholder.
- (void)moveTabFromIndex:(NSInteger)from {
int toIndex = [self indexOfPlaceholder];
tabModel_->MoveTabContentsAt(from, toIndex, true);
}
// Drop a given TabContents at the location of the current placeholder. If there
// is no placeholder, it will go at the end. Used when dragging from another
// window when we don't have access to the TabContents as part of our strip.
- (void)dropTabContents:(TabContents*)contents {
int index = [self indexOfPlaceholder];
// Insert it into this tab strip. We want it in the foreground and to not
// inherit the current tab's group.
tabModel_->InsertTabContentsAt(index, contents, true, false);
}
// 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.
- (NSRect)selectedTabGrowBoxRect {
int selectedIndex = tabModel_->selected_index();
if (selectedIndex == TabStripModel::kNoTab) {
// When the window is initially being constructed, there may be no currently
// selected tab, so pick the first one. If there aren't any, just bail with
// an empty rect.
selectedIndex = 0;
}
TabContentsController* selectedController =
[tabContentsArray_ objectAtIndex:selectedIndex];
if (!selectedController)
return NSZeroRect;
return [selectedController growBoxRect];
}
@end