blob: ff675925afab4f86d93175cfe945ab537107183a [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
#include <algorithm>
#include <cmath>
#include "base/mac/bundle_locations.h"
#include "base/mac/mac_util.h"
#import "chrome/browser/themes/theme_properties.h"
#import "chrome/browser/themes/theme_service.h"
#import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h"
#import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h"
#import "chrome/browser/ui/cocoa/tabs/tab_view.h"
#import "chrome/browser/ui/cocoa/themed_window.h"
#import "chrome/common/extensions/extension.h"
#include "grit/generated_resources.h"
#import "third_party/GTM/AppKit/GTMFadeTruncatingTextFieldCell.h"
#import "ui/base/cocoa/menu_controller.h"
#include "ui/base/l10n/l10n_util_mac.h"
@implementation TabController
@synthesize action = action_;
@synthesize app = app_;
@synthesize loadingState = loadingState_;
@synthesize mini = mini_;
@synthesize pinned = pinned_;
@synthesize target = target_;
@synthesize url = url_;
namespace TabControllerInternal {
// A C++ delegate that handles enabling/disabling menu items and handling when
// a menu command is chosen. Also fixes up the menu item label for "pin/unpin
// tab".
class MenuDelegate : public ui::SimpleMenuModel::Delegate {
public:
explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
: target_(target),
owner_(owner) {}
// Overridden from ui::SimpleMenuModel::Delegate
virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
return false;
}
virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
TabStripModel::ContextMenuCommand command =
static_cast<TabStripModel::ContextMenuCommand>(command_id);
return [target_ isCommandEnabled:command forController:owner_];
}
virtual bool GetAcceleratorForCommandId(
int command_id,
ui::Accelerator* accelerator) OVERRIDE { return false; }
virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
TabStripModel::ContextMenuCommand command =
static_cast<TabStripModel::ContextMenuCommand>(command_id);
[target_ commandDispatch:command forController:owner_];
}
private:
id<TabControllerTarget> target_; // weak
TabController* owner_; // weak, owns me
};
} // TabControllerInternal namespace
// The min widths is the smallest number at which the right edge of the right
// tab border image is not visibly clipped. It is a bit smaller than the sum
// of the two tab edge bitmaps because these bitmaps have a few transparent
// pixels on the side. The selected tab width includes the close button width.
+ (CGFloat)minTabWidth { return 36; }
+ (CGFloat)minSelectedTabWidth { return 52; }
+ (CGFloat)maxTabWidth { return 214; }
+ (CGFloat)miniTabWidth { return 58; }
+ (CGFloat)appTabWidth { return 66; }
- (TabView*)tabView {
DCHECK([[self view] isKindOfClass:[TabView class]]);
return static_cast<TabView*>([self view]);
}
- (id)init {
if ((self = [super init])) {
// Icon.
// Remember the icon's frame, so that if the icon is ever removed, a new
// one can later replace it in the proper location.
originalIconFrame_ = NSMakeRect(19, 5, 16, 16);
iconView_.reset([[NSImageView alloc] initWithFrame:originalIconFrame_]);
[iconView_ setAutoresizingMask:NSViewMaxXMargin];
// When the icon is removed, the title expands to the left to fill the
// space left by the icon. When the close button is removed, the title
// expands to the right to fill its space. These are the amounts to expand
// and contract titleView_ under those conditions. We don't have to
// explicilty save the offset between the title and the close button since
// we can just get that value for the close button's frame.
NSRect titleFrame = NSMakeRect(35, 6, 92, 14);
// Label.
titleView_.reset([[NSTextField alloc] initWithFrame:titleFrame]);
[titleView_ setAutoresizingMask:NSViewWidthSizable];
base::scoped_nsobject<GTMFadeTruncatingTextFieldCell> labelCell(
[[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@"Label"]);
[labelCell setControlSize:NSSmallControlSize];
CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSSmallControlSize];
NSFont* font = [NSFont fontWithName:
[[labelCell font] fontName] size:fontSize];
[labelCell setFont:font];
[titleView_ setCell:labelCell];
// Close button.
closeButton_.reset([[HoverCloseButton alloc] initWithFrame:
NSMakeRect(127, 4, 18, 18)]);
[closeButton_ setAutoresizingMask:NSViewMinXMargin];
[closeButton_ setTarget:self];
[closeButton_ setAction:@selector(closeTab:)];
base::scoped_nsobject<TabView> view(
[[TabView alloc] initWithFrame:NSMakeRect(0, 0, 160, 25)
controller:self
closeButton:closeButton_]);
[view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
[view addSubview:iconView_];
[view addSubview:titleView_];
[view addSubview:closeButton_];
[super setView:view];
isIconShowing_ = YES;
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(themeChangedNotification:)
name:kBrowserThemeDidChangeNotification
object:nil];
[self internalSetSelected:selected_];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[self tabView] setController:nil];
[super dealloc];
}
// The internals of |-setSelected:| and |-setActive:| but doesn't set the
// backing variables. This updates the drawing state and marks self as needing
// a re-draw.
- (void)internalSetSelected:(BOOL)selected {
TabView* tabView = [self tabView];
DCHECK([tabView isKindOfClass:[TabView class]]);
[tabView setState:selected];
if ([self active])
[tabView cancelAlert];
[self updateVisibility];
[self updateTitleColor];
}
// Called when Cocoa wants to display the context menu. Lazily instantiate
// the menu based off of the cross-platform model. Re-create the menu and
// model every time to get the correct labels and enabling.
- (NSMenu*)menu {
contextMenuDelegate_.reset(
new TabControllerInternal::MenuDelegate(target_, self));
contextMenuModel_.reset(
[target_ contextMenuModelForController:self
menuDelegate:contextMenuDelegate_.get()]);
contextMenuController_.reset(
[[MenuController alloc] initWithModel:contextMenuModel_.get()
useWithPopUpButtonCell:NO]);
return [contextMenuController_ menu];
}
- (void)closeTab:(id)sender {
if ([[self target] respondsToSelector:@selector(closeTab:)]) {
[[self target] performSelector:@selector(closeTab:)
withObject:[self view]];
}
}
- (void)selectTab:(id)sender {
if ([[self tabView] isClosing])
return;
if ([[self target] respondsToSelector:[self action]]) {
[[self target] performSelector:[self action]
withObject:[self view]];
}
}
- (void)setTitle:(NSString*)title {
[titleView_ setStringValue:title];
[[self view] setToolTip:title];
if ([self mini] && ![self selected]) {
TabView* tabView = static_cast<TabView*>([self view]);
DCHECK([tabView isKindOfClass:[TabView class]]);
[tabView startAlert];
}
[super setTitle:title];
}
- (void)setActive:(BOOL)active {
if (active != active_) {
active_ = active;
[self internalSetSelected:[self selected]];
}
}
- (BOOL)active {
return active_;
}
- (void)setSelected:(BOOL)selected {
if (selected_ != selected) {
selected_ = selected;
[self internalSetSelected:[self selected]];
}
}
- (BOOL)selected {
return selected_ || active_;
}
- (NSView*)iconView {
return iconView_;
}
- (void)setIconView:(NSView*)iconView {
[iconView_ removeFromSuperview];
iconView_.reset([iconView retain]);
if ([self app] || [self mini]) {
NSRect appIconFrame = [iconView frame];
appIconFrame.origin = originalIconFrame_.origin;
const CGFloat tabWidth = [self app] ? [TabController appTabWidth]
: [TabController miniTabWidth];
// Center the icon.
appIconFrame.origin.x =
std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0);
[iconView_ setFrame:appIconFrame];
} else {
[iconView_ setFrame:originalIconFrame_];
}
// Ensure that the icon is suppressed if no icon is set or if the tab is too
// narrow to display one.
[self updateVisibility];
if (iconView_)
[[self view] addSubview:iconView_];
}
- (NSTextField*)titleView {
return titleView_;
}
- (MediaIndicatorView*)mediaIndicatorView {
return mediaIndicatorView_;
}
- (void)setMediaIndicatorView:(MediaIndicatorView*)mediaIndicatorView {
[mediaIndicatorView_ removeFromSuperview];
mediaIndicatorView_.reset([mediaIndicatorView retain]);
[self updateVisibility];
if (mediaIndicatorView_)
[[self view] addSubview:mediaIndicatorView_];
}
- (HoverCloseButton*)closeButton {
return closeButton_;
}
- (NSString*)toolTip {
return [[self tabView] toolTipText];
}
// Return a rough approximation of the number of icons we could fit in the
// tab. We never actually do this, but it's a helpful guide for determining
// how much space we have available.
- (int)iconCapacity {
const CGFloat availableWidth = std::max<CGFloat>(
0, NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_));
const CGFloat widthPerIcon = NSWidth(originalIconFrame_);
const int kPaddingBetweenIcons = 2;
if (availableWidth >= widthPerIcon &&
availableWidth < (widthPerIcon + kPaddingBetweenIcons)) {
return 1;
}
return availableWidth / (widthPerIcon + kPaddingBetweenIcons);
}
- (BOOL)shouldShowIcon {
return chrome::ShouldTabShowFavicon(
[self iconCapacity], [self mini], [self selected], iconView_ != nil,
!mediaIndicatorView_ ? TAB_MEDIA_STATE_NONE :
[mediaIndicatorView_ animatingMediaState]);
}
- (BOOL)shouldShowMediaIndicator {
if (!mediaIndicatorView_)
return NO;
return chrome::ShouldTabShowMediaIndicator(
[self iconCapacity], [self mini], [self selected], iconView_ != nil,
[mediaIndicatorView_ animatingMediaState]);
}
- (BOOL)shouldShowCloseButton {
return chrome::ShouldTabShowCloseButton(
[self iconCapacity], [self mini], [self selected]);
}
- (void)updateVisibility {
// iconView_ may have been replaced or it may be nil, so [iconView_ isHidden]
// won't work. Instead, the state of the icon is tracked separately in
// isIconShowing_.
BOOL newShowIcon = [self shouldShowIcon];
[iconView_ setHidden:!newShowIcon];
isIconShowing_ = newShowIcon;
// If the tab is a mini-tab, hide the title.
[titleView_ setHidden:[self mini]];
BOOL newShowCloseButton = [self shouldShowCloseButton];
[closeButton_ setHidden:!newShowCloseButton];
BOOL newShowMediaIndicator = [self shouldShowMediaIndicator];
[mediaIndicatorView_ setHidden:!newShowMediaIndicator];
if (newShowMediaIndicator) {
NSRect newFrame = [mediaIndicatorView_ frame];
if ([self app] || [self mini]) {
// Tab is pinned: Position the media indicator in the center.
const CGFloat tabWidth = [self app] ?
[TabController appTabWidth] : [TabController miniTabWidth];
newFrame.origin.x = std::floor((tabWidth - NSWidth(newFrame)) / 2);
newFrame.origin.y = NSMinY(originalIconFrame_) -
std::floor((NSHeight(newFrame) - NSHeight(originalIconFrame_)) / 2);
} else {
// The Frame for the mediaIndicatorView_ depends on whether iconView_
// and/or closeButton_ are visible, and where they have been positioned.
const NSRect closeButtonFrame = [closeButton_ frame];
newFrame.origin.x = NSMinX(closeButtonFrame);
// Position to the left of the close button when it is showing.
if (newShowCloseButton)
newFrame.origin.x -= NSWidth(newFrame);
// Media indicator is centered vertically, with respect to closeButton_.
newFrame.origin.y = NSMinY(closeButtonFrame) -
std::floor((NSHeight(newFrame) - NSHeight(closeButtonFrame)) / 2);
}
[mediaIndicatorView_ setFrame:newFrame];
}
// Adjust the title view based on changes to the icon's and close button's
// visibility.
NSRect oldTitleFrame = [titleView_ frame];
NSRect newTitleFrame;
newTitleFrame.size.height = oldTitleFrame.size.height;
newTitleFrame.origin.y = oldTitleFrame.origin.y;
if (newShowIcon) {
newTitleFrame.origin.x = NSMaxX([iconView_ frame]);
} else {
newTitleFrame.origin.x = originalIconFrame_.origin.x;
}
if (newShowMediaIndicator) {
newTitleFrame.size.width = NSMinX([mediaIndicatorView_ frame]) -
newTitleFrame.origin.x;
} else if (newShowCloseButton) {
newTitleFrame.size.width = NSMinX([closeButton_ frame]) -
newTitleFrame.origin.x;
} else {
newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
newTitleFrame.origin.x;
}
[titleView_ setFrame:newTitleFrame];
}
- (void)updateTitleColor {
NSColor* titleColor = nil;
ui::ThemeProvider* theme = [[[self view] window] themeProvider];
if (theme && ![self selected])
titleColor = theme->GetNSColor(ThemeProperties::COLOR_BACKGROUND_TAB_TEXT);
// Default to the selected text color unless told otherwise.
if (theme && !titleColor)
titleColor = theme->GetNSColor(ThemeProperties::COLOR_TAB_TEXT);
[titleView_ setTextColor:titleColor ? titleColor : [NSColor textColor]];
}
- (void)themeChangedNotification:(NSNotification*)notification {
[self updateTitleColor];
}
// Called by the tabs to determine whether we are in rapid (tab) closure mode.
- (BOOL)inRapidClosureMode {
if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) {
return [[self target] performSelector:@selector(inRapidClosureMode)] ?
YES : NO;
}
return NO;
}
// The following methods are invoked from the TabView and are forwarded to the
// TabStripDragController.
- (BOOL)tabCanBeDragged:(TabController*)controller {
return [[target_ dragController] tabCanBeDragged:controller];
}
- (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab {
[[target_ dragController] maybeStartDrag:event forTab:tab];
}
@end