pjs/camino/src/browser/BrowserTabBarView.mm

597 строки
22 KiB
Plaintext

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is tab UI for Camino.
*
* The Initial Developer of the Original Code is
* Geoff Beier.
* Portions created by the Initial Developer are Copyright (C) 2004
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Geoff Beier <me@mollyandgeoff.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#import "BrowserTabBarView.h"
#import "BrowserTabViewItem.h"
#import "TabButtonCell.h"
#import "NSPasteboard+Utils.h"
@interface BrowserTabBarView (PRIVATE)
-(void)layoutButtons;
-(void)loadImages;
-(void)drawTabBarBackgroundInRect:(NSRect)rect withActiveTabRect:(NSRect)tabRect;
-(TabButtonCell*)buttonAtPoint:(NSPoint)clickPoint;
-(void)registerTabButtonsForTracking;
-(void)unregisterTabButtonsForTracking;
-(void)initOverflowMenu;
-(NSRect)tabsRect;
@end
static NSImage *gBackgroundImage = nil;
static NSImage *gTabButtonDividerImage = nil;
static NSImage *gTabOverflowImage = nil;
static const float kTabBarDefaultHeight = 22.0;
@implementation BrowserTabBarView
static const int kTabBarMargin = 5; // left/right margin for tab bar
static const float kMinTabWidth = 100.0; // the smallest tabs that will be drawn
static const float kMaxTabWidth = 175.0; // the widest tabs that will be drawn
static const int kTabDragThreshold = 3; // distance a drag must go before we start dnd
static const float kOverflowButtonWidth = 16;
static const float kOverflowButtonHeight = 16;
static const int kOverflowButtonMargin = 1;
-(id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
mActiveTabButton = nil;
mOverflowButton = nil;
mOverflowTabs = NO;
// initialize to YES so that awakeFromNib: will set the right size; awakeFromNib uses setVisible which
// will only be effective if visibility changes. initializing to YES causes the right thing to happen even
// if this view is visible in a nib file.
mVisible = YES;
// this will not likely have any result here
[self rebuildTabBar];
[self registerForDraggedTypes:[NSArray arrayWithObjects: kCaminoBookmarkListPBoardType,
kWebURLsWithTitlesPboardType,
NSStringPboardType,
NSFilenamesPboardType,
NSURLPboardType,
nil]];
}
return self;
}
-(void)awakeFromNib
{
// start off with the tabs hidden, and allow our controller to show or hide as appropriate.
[self setVisible:NO];
// this needs to be called again since our tabview should be non-nil now
[self rebuildTabBar];
}
-(void)dealloc
{
[mTrackingCells release];
[mActiveTabButton release];
[mOverflowButton release];
[mOverflowMenu release];
[super dealloc];
}
-(void)drawRect:(NSRect)rect
{
// this should only occur the first time any instance of this view is drawn
if (!gBackgroundImage || !gTabButtonDividerImage)
[self loadImages];
// determine the frame of the active tab button and fill the rest of the bar in with the background
NSRect activeTabButtonFrame = [mActiveTabButton frame];
NSRect tabsRect = [self tabsRect];
// if any of the active tab would be outside the drawable area, fill it in
if (NSMaxX(activeTabButtonFrame) > NSMaxX(tabsRect)) activeTabButtonFrame.size.width = 0.0;
[self drawTabBarBackgroundInRect:rect withActiveTabRect:activeTabButtonFrame];
NSArray *tabItems = [mTabView tabViewItems];
NSEnumerator *tabEnumerator = [tabItems objectEnumerator];
BrowserTabViewItem *tab = [tabEnumerator nextObject];
TabButtonCell *prevButton = nil;
while (tab != nil) {
TabButtonCell *tabButton = [tab tabButtonCell];
BrowserTabViewItem *nextTab = [tabEnumerator nextObject];
NSRect tabButtonFrame = [tabButton frame];
if (NSIntersectsRect(tabButtonFrame,rect) && NSMaxX(tabButtonFrame) <= NSMaxX(tabsRect))
[tabButton drawWithFrame:tabButtonFrame inView:self];
// draw the first divider.
if ((prevButton == nil) && ([tab tabState] != NSSelectedTab))
[gTabButtonDividerImage compositeToPoint:NSMakePoint(tabButtonFrame.origin.x - [gTabButtonDividerImage size].width, tabButtonFrame.origin.y)
operation:NSCompositeSourceOver];
prevButton = tabButton;
tab = nextTab;
}
}
-(void)setFrame:(NSRect)frameRect
{
[super setFrame:frameRect];
// tab buttons probably need to be resized if the frame changes
[self unregisterTabButtonsForTracking];
[self layoutButtons];
[self registerTabButtonsForTracking];
}
-(NSMenu*)menuForEvent:(NSEvent*)theEvent
{
NSPoint clickPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
TabButtonCell *clickedTabButton = [self buttonAtPoint:clickPoint];
// XXX should there be a menu if someone clicks in the tab bar where there is no tab?
return (clickedTabButton) ? [clickedTabButton menu] : nil;
}
-(void)mouseDown:(NSEvent*)theEvent
{
NSPoint clickPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
TabButtonCell *clickedTabButton = [self buttonAtPoint:clickPoint];
mLastClickPoint = clickPoint;
if (clickedTabButton && ![clickedTabButton willTrackMouse:theEvent inRect:[clickedTabButton frame] ofView:self])
[[[clickedTabButton tabViewItem] tabItemContentsView] mouseDown:theEvent];
}
-(void)mouseUp:(NSEvent*)theEvent
{
NSPoint clickPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
TabButtonCell * clickedTabButton = [self buttonAtPoint:clickPoint];
if (clickedTabButton && ![clickedTabButton willTrackMouse:theEvent inRect:[clickedTabButton frame] ofView:self])
[[[clickedTabButton tabViewItem] tabItemContentsView] mouseUp:theEvent];
}
-(void)mouseDragged:(NSEvent*)theEvent
{
NSPoint clickPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
TabButtonCell *clickedTabButton = [self buttonAtPoint:clickPoint];
if (clickedTabButton &&
![clickedTabButton willTrackMouse:theEvent inRect:[clickedTabButton frame] ofView:self])
[[[clickedTabButton tabViewItem] tabItemContentsView] mouseDragged:theEvent];
/* else if (!mDragStarted) {
// XXX TODO: Handle dnd of tabs here
if ((abs((int)(mLastClickPoint.x - clickPoint.x)) >= kTabDragThreshold) ||
(abs((int)(mLastClickPoint.y - clickPoint.y)) >= kTabDragThreshold)) {
NSLog(@"Here's where we'd handle the drag among friends rather than the drag manager");
}*/
}
// returns the tab at the specified point
-(TabButtonCell*)buttonAtPoint:(NSPoint)clickPoint
{
BrowserTabViewItem *tab = nil;
NSArray *tabItems = [mTabView tabViewItems];
NSEnumerator *tabEnumerator = [tabItems objectEnumerator];
while ((tab = [tabEnumerator nextObject])) {
TabButtonCell *button = [tab tabButtonCell];
if (NSPointInRect(clickPoint,[button frame]))
return button;
}
return nil;
}
-(void) drawTabBarBackgroundInRect:(NSRect)rect withActiveTabRect:(NSRect)tabRect
{
// draw tab bar background, omitting the selected Tab
NSRect barFrame = [self bounds];
NSRect fillRect;
// first, fill to the left of the active tab
fillRect = NSMakeRect(barFrame.origin.x, barFrame.origin.y,
(tabRect.origin.x - barFrame.origin.x), barFrame.size.height);
if (NSIntersectsRect(fillRect,rect)) {
// make sure we're not drawing to the left or right of the actual rectangle we were asked to draw
if (fillRect.origin.x < NSMinX(rect)) {
fillRect.size.width -= NSMinX(rect) - fillRect.origin.x;
fillRect.origin.x = NSMinX(rect);
}
if (NSMaxX(fillRect) > NSMaxX(rect))
fillRect.size.width -= NSMaxX(fillRect) - NSMaxX(rect);
[gBackgroundImage paintTiledInRect:fillRect];
}
// then fill to the right
fillRect = NSMakeRect(NSMaxX(tabRect), barFrame.origin.y,
(NSMaxX(barFrame) - NSMaxX(tabRect)), barFrame.size.height);
if (NSIntersectsRect(fillRect,rect)) {
// make sure we're not drawing to the left or right of the actual rectangle we were asked to draw
if (fillRect.origin.x < NSMinX(rect)) {
fillRect.size.width -= NSMinX(rect) - fillRect.origin.x;
fillRect.origin.x = NSMinX(rect);
}
if (NSMaxX(fillRect) > NSMaxX(rect))
fillRect.size.width -= NSMaxX(fillRect) - NSMaxX(rect);
[gBackgroundImage paintTiledInRect:fillRect];
}
}
-(void)loadImages
{
if (!gBackgroundImage)
gBackgroundImage = [[NSImage imageNamed:@"tab_bar_bg"] retain];
if (!gTabButtonDividerImage)
gTabButtonDividerImage = [[NSImage imageNamed:@"tab_button_divider"] retain];
if (!gTabOverflowImage)
gTabOverflowImage = [[NSImage imageNamed:@"tab_overflow"] retain];
}
// construct the tab bar based on the current state of mTabView;
// should be called when tabs are first shown.
-(void)rebuildTabBar
{
[self unregisterTabButtonsForTracking];
[mActiveTabButton release];
mActiveTabButton = [[(BrowserTabViewItem *)[mTabView selectedTabViewItem] tabButtonCell] retain];
[self layoutButtons];
[self registerTabButtonsForTracking];
}
- (void)windowClosed
{
// remove all tracking rects because this view is implicitly retained when they're registered
[self unregisterTabButtonsForTracking];
}
// allows tab button cells to react to mouse events
-(void)registerTabButtonsForTracking
{
if ([self window] && mVisible) {
NSArray * tabItems = [mTabView tabViewItems];
if(mTrackingCells) [self unregisterTabButtonsForTracking];
mTrackingCells = [NSMutableArray arrayWithCapacity:[tabItems count]];
[mTrackingCells retain];
NSEnumerator *tabEnumerator = [tabItems objectEnumerator];
NSPoint local = [[self window] convertScreenToBase:[NSEvent mouseLocation]];
local = [self convertPoint:local fromView:nil];
BrowserTabViewItem *tab = nil;
while ((tab = [tabEnumerator nextObject])) {
TabButtonCell * tabButton = [tab tabButtonCell];
if (tabButton) {
[mTrackingCells addObject:tabButton];
NSRect trackingRect = [tabButton frame];
// only track tabs that are onscreen
if (NSMaxX(trackingRect) <= NSMaxX([self tabsRect]))
[tabButton addTrackingRectInView: self withFrame:trackingRect cursorLocation:local];
}
}
}
}
// causes tab buttons to stop reacting to mouse events
-(void)unregisterTabButtonsForTracking
{
if (mTrackingCells) {
NSEnumerator *tabEnumerator = [mTrackingCells objectEnumerator];
TabButtonCell *tab = nil;
while ((tab = (TabButtonCell *)[tabEnumerator nextObject]))
[tab removeTrackingRectFromView: self];
[mTrackingCells release];
mTrackingCells = nil;
}
}
// returns the height the tab bar should be if drawn
-(float)tabBarHeight
{
// this will be constant for now
return kTabBarDefaultHeight;
}
-(BrowserTabViewItem *)tabViewItemAtPoint:(NSPoint)location
{
TabButtonCell *button = [self buttonAtPoint:[self convertPoint:location fromView:nil]];
return (button) ? [button tabViewItem] : nil;
}
// sets the tab buttons to the largest kMinTabWidth <= size <= kMaxTabWidth where they all fit
// and calculates the frame for each.
-(void)layoutButtons
{
const int numTabs = [mTabView numberOfTabViewItems];
float tabWidth = kMaxTabWidth;
// calculate the largest tabs that would fit... [self tabsRect] may not be correct until mOverflowTabs is set here.
float maxWidth = floor((NSWidth([self bounds]) - (2*kTabBarMargin))/numTabs);
// if tabs will overflow, leave space for the button
if (maxWidth < kMinTabWidth) {
mOverflowTabs = YES;
NSRect tabsRect = [self tabsRect];
for (int i = 1; i < numTabs; i++) {
maxWidth = floor(NSWidth(tabsRect)/(numTabs - i));
if (maxWidth >= kMinTabWidth) break;
}
// because the specific tabs which overflow may change, empty the menu and rebuild it as tabs are laid out
[self initOverflowMenu];
} else {
mOverflowTabs = NO;
}
// if our tabs are currently larger than that, shrink them to the larger of kMinTabWidth or maxWidth
if (tabWidth > maxWidth)
tabWidth = (maxWidth > kMinTabWidth ? maxWidth : kMinTabWidth);
// resize and position the tab buttons
int xCoord = kTabBarMargin;
NSArray *tabItems = [mTabView tabViewItems];
NSEnumerator *tabEnumerator = [tabItems objectEnumerator];
BrowserTabViewItem *tab = nil;
TabButtonCell *prevTabButton = nil;
while ((tab = [tabEnumerator nextObject])) {
TabButtonCell *tabButtonCell = [tab tabButtonCell];
NSSize buttonSize = [tabButtonCell size];
buttonSize.width = tabWidth;
buttonSize.height = kTabBarDefaultHeight;
NSPoint buttonOrigin = NSMakePoint(xCoord,0);
[tabButtonCell setFrame:NSMakeRect(buttonOrigin.x,buttonOrigin.y,buttonSize.width,buttonSize.height)];
// tell the button whether it needs to draw the right side dividing line
if ([tab tabState] == NSSelectedTab) {
[tabButtonCell setDrawDivider:NO];
[prevTabButton setDrawDivider:NO];
} else {
[tabButtonCell setDrawDivider:YES];
}
// If the tab ran off the edge, suppress its close button, make sure the divider is drawn, and add it to the menu
if (buttonOrigin.x + buttonSize.width > NSMaxX([self tabsRect])) {
[tabButtonCell hideCloseButton];
// push the tab off the edge of the view to keep it from grabbing clicks if there is an area
// between the overflow menu and the last tab which is within tabsRect due to rounding
[tabButtonCell setFrame:NSMakeRect(NSMaxX([self bounds]),buttonOrigin.y,buttonSize.width,buttonSize.height)];
// if the tab prior to the overflow is not selected, it must draw a divider
if([[prevTabButton tabViewItem] tabState] != NSSelectedTab) [prevTabButton setDrawDivider:YES];
[mOverflowMenu addItem:[tab menuItem]];
}
prevTabButton = tabButtonCell;
xCoord += (int)tabWidth;
}
// if tabs overflowed, position and display the overflow button
if (mOverflowTabs) {
[mOverflowButton setFrame:NSMakeRect(NSMaxX([self tabsRect]) + kOverflowButtonMargin,
([self tabBarHeight] - kOverflowButtonHeight)/2,kOverflowButtonWidth,kOverflowButtonHeight)];
[self addSubview:mOverflowButton];
} else {
[mOverflowButton removeFromSuperview];
}
[self setNeedsDisplay:YES];
}
-(void)initOverflowMenu
{
if (!mOverflowButton) {
// if it hasn't been created yet, create an NSPopUpButton and retain a strong reference
mOverflowButton = [[NSButton alloc] initWithFrame:NSMakeRect(0,0,kOverflowButtonWidth,kOverflowButtonHeight)];
if (!gTabOverflowImage) [self loadImages];
[mOverflowButton setImage:gTabOverflowImage];
[mOverflowButton setImagePosition:NSImageOnly];
[mOverflowButton setBezelStyle:NSShadowlessSquareBezelStyle];
[mOverflowButton setBordered:NO];
[[mOverflowButton cell] setHighlightsBy:NSNoCellMask];
[mOverflowButton setTarget:self];
[mOverflowButton setAction:@selector(overflowMenu:)];
[(NSButtonCell *)[mOverflowButton cell]sendActionOn:NSLeftMouseDownMask];
}
if (!mOverflowMenu) {
// create an empty NSMenu for later use and retain a strong reference
mOverflowMenu = [[NSMenu alloc] init];
[mOverflowMenu addItemWithTitle:@"" action:NULL keyEquivalent:@""];
}
// remove any items on the menu other than the dummy item
for (int i = [mOverflowMenu numberOfItems]; i > 1; i--)
[mOverflowMenu removeItemAtIndex:i-1];
}
- (IBAction)overflowMenu:(id)sender
{
NSPopUpButtonCell *popupCell = [[[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:YES] autorelease];
[popupCell setMenu:mOverflowMenu];
[popupCell trackMouse:[NSApp currentEvent] inRect:[sender bounds] ofView:sender untilMouseUp:YES];
}
// returns an NSRect of the area where tab widgets may be drawn
-(NSRect)tabsRect
{
NSRect rect = [self bounds];
rect.origin.x += kTabBarMargin;
rect.size.width -= 2 * kTabBarMargin + (mOverflowTabs ? kOverflowButtonWidth : 0.0);
return rect;
}
-(BOOL)isVisible
{
return mVisible;
}
// show or hide tabs- should be called if this view will be hidden, to give it a chance to register or
// unregister tracking rects as appropriate.
//
// Does not actually remove the view from the hierarchy; simply hides it.
-(void)setVisible:(BOOL)show
{
// only change anything if the new state is different from the current state
if (show && !mVisible) {
mVisible = show;
NSRect newFrame = [self frame];
newFrame.size.height = [self tabBarHeight];
[self setFrame:newFrame];
[self rebuildTabBar];
// set up tracking rects
[self registerTabButtonsForTracking];
} else if (!show && mVisible) { // being hidden
mVisible = show;
NSRect newFrame = [self frame];
newFrame.size.height = 0.0;
[self setFrame:newFrame];
// destroy tracking rects
[self unregisterTabButtonsForTracking];
}
}
#pragma mark -
// NSDraggingDestination destination methods
-(unsigned int)draggingEntered:(id <NSDraggingInfo>)sender
{
TabButtonCell * button = [self buttonAtPoint:[self convertPoint:[sender draggingLocation] fromView:nil]];
if (!button) {
// if the mouse isn't over a button, it'd be nice to give the user some indication that something will happen
// if the user releases the mouse here. Try to indicate copy.
if ([sender draggingSourceOperationMask] & NSDragOperationCopy)
return NSDragOperationCopy;
else
return NSDragOperationGeneric;
}
NSView * dragDest = [[button tabViewItem] tabItemContentsView];
mDragDestButton = button;
unsigned int rv = [ dragDest draggingEntered:sender];
if (NSDragOperationNone != rv) {
[button setDragTarget:YES];
[self setNeedsDisplay:YES];
}
[self unregisterTabButtonsForTracking];
return rv;
}
-(unsigned int)draggingUpdated:(id <NSDraggingInfo>)sender
{
TabButtonCell * button = [self buttonAtPoint:[self convertPoint:[sender draggingLocation] fromView:nil]];
if (!button) {
if (mDragDestButton) {
[mDragDestButton setDragTarget:NO];
[self setNeedsDisplay:YES];
mDragDestButton = nil;
}
if ([sender draggingSourceOperationMask] & NSDragOperationCopy)
return NSDragOperationCopy;
else
return NSDragOperationGeneric;
}
if (mDragDestButton != button) {
[mDragDestButton setDragTarget:NO];
[self setNeedsDisplay:YES];
mDragDestButton = button;
}
NSView * dragDest = [[button tabViewItem] tabItemContentsView];
unsigned int rv = [dragDest draggingUpdated:sender];
if (NSDragOperationNone != rv) {
[button setDragTarget:YES];
[self setNeedsDisplay:YES];
}
return rv;
}
-(void)draggingExited:(id <NSDraggingInfo>)sender
{
if (mDragDestButton) {
[mDragDestButton setDragTarget:NO];
mDragDestButton = nil;
}
[self setNeedsDisplay:YES];
[self registerTabButtonsForTracking];
}
-(BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
TabButtonCell * button = [self buttonAtPoint:[self convertPoint:[sender draggingLocation] fromView:nil]];
if (!button) {
if (mDragDestButton)
[mDragDestButton setDragTarget:NO];
return [mTabView prepareForDragOperation:sender];
}
NSView * dragDest = [[button tabViewItem] tabItemContentsView];
BOOL rv = [dragDest prepareForDragOperation: sender];
if (!rv) {
if (mDragDestButton)
[mDragDestButton setDragTarget:NO];
[self setNeedsDisplay:YES];
}
return rv;
}
-(BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
TabButtonCell * button = [self buttonAtPoint:[self convertPoint:[sender draggingLocation] fromView:nil]];
if (!button) {
if (mDragDestButton)
[mDragDestButton setDragTarget:NO];
mDragDestButton = nil;
return [mTabView performDragOperation:sender];
}
[mDragDestButton setDragTarget:NO];
[button setDragTarget:NO];
[self setNeedsDisplay:YES];
NSView * dragDest = [[button tabViewItem] tabItemContentsView];
[self registerTabButtonsForTracking];
mDragDestButton = nil;
return [dragDest performDragOperation:sender];
}
@end
#pragma mark -
@implementation NSImage (BrowserTabBarViewAdditions)
//an addition to NSImage used by both BrowserTabBarView and TabButtonCell
//tiles an image in the specified rect... even though it should work vertically, these images
//will only look right tiled horizontally.
-(void)paintTiledInRect:(NSRect)rect
{
NSSize imageSize = [self size];
NSRect currTile = NSMakeRect(rect.origin.x, rect.origin.y, imageSize.width, imageSize.height);
while (currTile.origin.y < NSMaxY(rect)) {
while (currTile.origin.x < NSMaxX(rect)) {
// clip the tile as needed
if (NSMaxX(currTile) > NSMaxX(rect)) {
currTile.size.width -= NSMaxX(currTile) - NSMaxX(rect);
}
if (NSMaxY(currTile) > NSMaxY(rect)) {
currTile.size.height -= NSMaxY(currTile) - NSMaxY(rect);
}
NSRect imageRect = NSMakeRect(0, 0, currTile.size.width, currTile.size.height);
[self compositeToPoint:currTile.origin fromRect:imageRect operation:NSCompositeSourceOver];
currTile.origin.x += currTile.size.width;
}
currTile.origin.y += currTile.size.height;
}
}
@end