mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 04:57:03 +00:00
Before this commit, the tabViewItems and the segments were 2 different collections. Any indexed change in the tabViewItems asked to re-sync the segments making it difficult to maintain any persistent state. Now the tabView items are the segments content. Also fixed the selection update when items are removed. See CPTabViewTest.j and manual test Manual/CPTabViewNib/
598 lines
16 KiB
Plaintext
598 lines
16 KiB
Plaintext
/*
|
|
* CPTabView.j
|
|
* AppKit
|
|
*
|
|
* Created by Derek Hammer.
|
|
* Copyright 2010, Derek Hammer.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
@import "CPBox.j"
|
|
@import "CPSegmentedControl.j"
|
|
@import "CPTabViewItem.j"
|
|
@import "CPView.j"
|
|
|
|
@typedef CPTabViewType
|
|
CPTopTabsBezelBorder = 0;
|
|
//CPLeftTabsBezelBorder = 1;
|
|
CPBottomTabsBezelBorder = 2;
|
|
//CPRightTabsBezelBorder = 3;
|
|
CPNoTabsBezelBorder = 4; //Displays no tabs and has a bezeled border.
|
|
CPNoTabsLineBorder = 5; //Has no tabs and displays a line border.
|
|
CPNoTabsNoBorder = 6; //Displays no tabs and no border.
|
|
|
|
var CPTabViewDidSelectTabViewItemSelector = 1 << 1,
|
|
CPTabViewShouldSelectTabViewItemSelector = 1 << 2,
|
|
CPTabViewWillSelectTabViewItemSelector = 1 << 3,
|
|
CPTabViewDidChangeNumberOfTabViewItemsSelector = 1 << 4;
|
|
|
|
|
|
@protocol CPTabViewDelegate <CPObject>
|
|
|
|
@optional
|
|
- (BOOL)tabView:(CPTabView)tabView shouldSelectTabViewItem:(CPTabViewItem)tabViewItem;
|
|
- (void)tabView:(CPTabView)tabView didSelectTabViewItem:(CPTabViewItem)tabViewItem;
|
|
- (void)tabView:(CPTabView)tabView willSelectTabViewItem:(CPTabViewItem)tabViewItem;
|
|
- (void)tabViewDidChangeNumberOfTabViewItems:(CPTabView)tabView;
|
|
|
|
@end
|
|
|
|
|
|
/*!
|
|
@ingroup appkit
|
|
@class CPTabView
|
|
|
|
A CPTabView object presents a tabbed interface where each page is one a
|
|
complete view hiearchy of its own. The user can navigate between various
|
|
pages by clicking on the tab headers.
|
|
*/
|
|
@implementation CPTabView : CPView
|
|
{
|
|
CPArray _items;
|
|
|
|
CPSegmentedControl _tabs;
|
|
CPBox _box;
|
|
|
|
CPTabViewItem _selectedTabViewItem;
|
|
|
|
CPTabViewType _type;
|
|
CPFont _font;
|
|
|
|
id <CPTabViewDelegate> _delegate;
|
|
unsigned _delegateSelectors;
|
|
}
|
|
|
|
- (id)initWithFrame:(CGRect)aFrame
|
|
{
|
|
if (self = [super initWithFrame:aFrame])
|
|
{
|
|
[self _init];
|
|
_selectedTabViewItem = nil;
|
|
[self setTabViewType:CPTopTabsBezelBorder];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_init
|
|
{
|
|
_tabs = [[CPSegmentedControl alloc] initWithFrame:CGRectMakeZero()];
|
|
[_tabs setHitTests:NO];
|
|
[_tabs setSegments:[CPArray array]];
|
|
|
|
var height = [_tabs valueForThemeAttribute:@"min-size"].height;
|
|
[_tabs setFrameSize:CGSizeMake(0, height)];
|
|
|
|
_box = [[CPBox alloc] initWithFrame:[self bounds]];
|
|
[self setBackgroundColor:[CPColor colorWithCalibratedWhite:0.95 alpha:1.0]];
|
|
|
|
[self addSubview:_box];
|
|
[self addSubview:_tabs];
|
|
}
|
|
|
|
- (CPArray)items
|
|
{
|
|
return [_tabs segments];
|
|
}
|
|
|
|
// Adding and Removing Tabs
|
|
/*!
|
|
Adds a CPTabViewItem to the tab view.
|
|
@param aTabViewItem the item to add
|
|
*/
|
|
- (void)addTabViewItem:(CPTabViewItem)aTabViewItem
|
|
{
|
|
[self insertTabViewItem:aTabViewItem atIndex:[self numberOfTabViewItems]];
|
|
}
|
|
|
|
/*!
|
|
Inserts a CPTabViewItem into the tab view at the specified index.
|
|
@param aTabViewItem the item to insert
|
|
@param anIndex the index for the item
|
|
*/
|
|
- (void)insertTabViewItem:(CPTabViewItem)aTabViewItem atIndex:(CPUInteger)anIndex
|
|
{
|
|
[self _insertTabViewItems:[aTabViewItem] atIndexes:[CPIndexSet indexSetWithIndex:anIndex]];
|
|
}
|
|
|
|
- (void)_insertTabViewItems:(CPArray)tabViewItems atIndexes:(CPIndexSet)indexes
|
|
{
|
|
[_tabs insertSegments:tabViewItems atIndexes:indexes];
|
|
[tabViewItems makeObjectsPerformSelector:@selector(_setTabView:) withObject:self];
|
|
|
|
[self tileWithChangedItem:[tabViewItems firstObject]];
|
|
[self _reverseSetContent];
|
|
|
|
[self _sendDelegateTabViewDidChangeNumberOfTabViewItems];
|
|
}
|
|
|
|
/*!
|
|
Removes the specified tab view item from the tab view.
|
|
@param aTabViewItem the item to remove
|
|
*/
|
|
- (void)removeTabViewItem:(CPTabViewItem)aTabViewItem
|
|
{
|
|
var idx = [[self items] indexOfObjectIdenticalTo:aTabViewItem];
|
|
|
|
if (idx == CPNotFound)
|
|
return;
|
|
|
|
[_tabs removeSegmentsAtIndexes:[CPIndexSet indexSetWithIndex:idx]];
|
|
[aTabViewItem _setTabView:nil];
|
|
|
|
[self tileWithChangedItem:nil];
|
|
[self _didRemoveTabViewItem:aTabViewItem atIndex:idx];
|
|
[self _reverseSetContent];
|
|
|
|
[self _sendDelegateTabViewDidChangeNumberOfTabViewItems];
|
|
}
|
|
|
|
- (void)_didRemoveTabViewItem:(CPTabViewItem)aTabViewItem atIndex:(CPInteger)idx
|
|
{
|
|
if ([self selectedIndexBinder])
|
|
return;
|
|
|
|
if (_selectedTabViewItem == aTabViewItem)
|
|
{
|
|
var didSelect = NO;
|
|
|
|
if (idx > 0)
|
|
didSelect = [self selectTabViewItemAtIndex:idx - 1];
|
|
else if ([self numberOfTabViewItems] > 0)
|
|
didSelect = [self selectTabViewItemAtIndex:0];
|
|
|
|
if (didSelect == NO)
|
|
_selectedTabViewItem == nil;
|
|
}
|
|
}
|
|
|
|
// Accessing Tabs
|
|
/*!
|
|
Returns the index of the specified item
|
|
@param aTabViewItem the item to find the index for
|
|
@return the index of aTabViewItem or CPNotFound
|
|
*/
|
|
- (int)indexOfTabViewItem:(CPTabViewItem)aTabViewItem
|
|
{
|
|
return [[self items] indexOfObjectIdenticalTo:aTabViewItem];
|
|
}
|
|
|
|
/*!
|
|
Returns the index of the CPTabViewItem with the specified identifier.
|
|
@param anIdentifier the identifier of the item
|
|
@return the index of the tab view item identified by anIdentifier, or CPNotFound
|
|
*/
|
|
- (int)indexOfTabViewItemWithIdentifier:(CPString)anIdentifier
|
|
{
|
|
return [[self items] indexOfObjectPassingTest:function(item, idx, stop)
|
|
{
|
|
[[item identifier] isEqual:anIdentifier];
|
|
}];
|
|
}
|
|
|
|
/*!
|
|
Returns the number of items in the tab view.
|
|
@return the number of tab view items in the receiver
|
|
*/
|
|
- (unsigned)numberOfTabViewItems
|
|
{
|
|
return [[self items] count];
|
|
}
|
|
|
|
/*!
|
|
Returns the CPTabViewItem at the specified index.
|
|
@return a tab view item, or nil
|
|
*/
|
|
- (CPTabViewItem)tabViewItemAtIndex:(CPUInteger)anIndex
|
|
{
|
|
return [[self items] objectAtIndex:anIndex];
|
|
}
|
|
|
|
/*!
|
|
Returns the array of items that backs this tab view.
|
|
@return a copy of the array of items in the receiver
|
|
*/
|
|
- (CPArray)tabViewItems
|
|
{
|
|
return [[self items] copy]; // Copy?
|
|
}
|
|
|
|
// Selecting a Tab
|
|
/*!
|
|
Sets the first tab view item in the array to be displayed to the user.
|
|
@param aSender the object making this request
|
|
*/
|
|
- (void)selectFirstTabViewItem:(id)aSender
|
|
{
|
|
if ([self numberOfTabViewItems] === 0)
|
|
return; // throw?
|
|
|
|
[self selectTabViewItemAtIndex:0];
|
|
}
|
|
|
|
/*!
|
|
Sets the last tab view item in the array to be displayed to the user.
|
|
@param aSender the object making this request
|
|
*/
|
|
- (void)selectLastTabViewItem:(id)aSender
|
|
{
|
|
if ([self numberOfTabViewItems] === 0)
|
|
return; // throw?
|
|
|
|
[self selectTabViewItemAtIndex:[self numberOfTabViewItems] - 1];
|
|
}
|
|
|
|
/*!
|
|
Sets the next tab item in the array to be displayed.
|
|
@param aSender the object making this request
|
|
*/
|
|
- (void)selectNextTabViewItem:(id)aSender
|
|
{
|
|
if (_selectedTabViewItem === nil)
|
|
return;
|
|
|
|
var nextIndex = [self indexOfTabViewItem:_selectedTabViewItem] + 1;
|
|
|
|
if (nextIndex === [self numberOfTabViewItems])
|
|
// does nothing. According to spec at (http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Reference/ApplicationKit/Classes/NSTabView_Class/Reference/Reference.html#//apple_ref/occ/instm/NSTabView/selectNextTabViewItem:)
|
|
return;
|
|
|
|
[self selectTabViewItemAtIndex:nextIndex];
|
|
}
|
|
|
|
/*!
|
|
Selects the previous item in the array for display.
|
|
@param aSender the object making this request
|
|
*/
|
|
- (void)selectPreviousTabViewItem:(id)aSender
|
|
{
|
|
if (_selectedTabViewItem === nil)
|
|
return;
|
|
|
|
var previousIndex = [self indexOfTabViewItem:_selectedTabViewItem] - 1;
|
|
|
|
if (previousIndex < 0)
|
|
return; // does nothing. See above.
|
|
|
|
[self selectTabViewItemAtIndex:previousIndex];
|
|
}
|
|
|
|
/*!
|
|
Displays the specified item in the tab view.
|
|
@param aTabViewItem the item to display
|
|
*/
|
|
- (void)selectTabViewItem:(CPTabViewItem)aTabViewItem
|
|
{
|
|
[self selectTabViewItemAtIndex:[self indexOfTabViewItem:aTabViewItem]];
|
|
}
|
|
|
|
/*!
|
|
Selects the item at the specified index.
|
|
@param anIndex the index of the item to display.
|
|
*/
|
|
- (BOOL)selectTabViewItemAtIndex:(CPUInteger)anIndex
|
|
{
|
|
if (![self _selectTabViewItemAtIndex:anIndex])
|
|
return NO;
|
|
|
|
//[self _reverseSetSelectedIndex];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)_selectTabViewItemAtIndex:(CPUInteger)anIndex
|
|
{
|
|
var aTabViewItem = [self tabViewItemAtIndex:anIndex];
|
|
|
|
if (aTabViewItem == _selectedTabViewItem)
|
|
return NO;
|
|
|
|
if ((_delegateSelectors & CPTabViewShouldSelectTabViewItemSelector) && ![_delegate tabView:self shouldSelectTabViewItem:aTabViewItem])
|
|
return NO;
|
|
|
|
if (_delegateSelectors & CPTabViewWillSelectTabViewItemSelector)
|
|
[_delegate tabView:self willSelectTabViewItem:aTabViewItem];
|
|
|
|
[_tabs setSelectedSegment:anIndex];
|
|
_selectedTabViewItem = aTabViewItem;
|
|
|
|
[self _displayItemView:[aTabViewItem view]];
|
|
[self _sendDelegateDidSelectTabViewItem:aTabViewItem];
|
|
|
|
return YES;
|
|
}
|
|
|
|
/*!
|
|
Returns the current item being displayed.
|
|
@return the tab view item currenly being displayed by the receiver
|
|
*/
|
|
- (CPTabViewItem)selectedTabViewItem
|
|
{
|
|
return _selectedTabViewItem;
|
|
}
|
|
|
|
// Modifying the font
|
|
/*!
|
|
Returns the font for tab label text.
|
|
@return the font for tab label text
|
|
*/
|
|
- (CPFont)font
|
|
{
|
|
return _font;
|
|
}
|
|
|
|
/*!
|
|
Sets the font for tab label text to font.
|
|
@param font the font the receiver should use for tab label text
|
|
*/
|
|
- (void)setFont:(CPFont)font
|
|
{
|
|
if ([_font isEqual:font])
|
|
return;
|
|
|
|
_font = font;
|
|
[_tabs setFont:_font];
|
|
}
|
|
|
|
//
|
|
/*!
|
|
Sets the tab view type.
|
|
@param aTabViewType the view type
|
|
*/
|
|
- (void)setTabViewType:(CPTabViewType)aTabViewType
|
|
{
|
|
if (_type === aTabViewType)
|
|
return;
|
|
|
|
_type = aTabViewType;
|
|
|
|
if (_type !== CPTopTabsBezelBorder && _type !== CPBottomTabsBezelBorder)
|
|
[_tabs removeFromSuperview];
|
|
else
|
|
[self addSubview:_tabs];
|
|
|
|
switch (_type)
|
|
{
|
|
case CPTopTabsBezelBorder:
|
|
case CPBottomTabsBezelBorder:
|
|
case CPNoTabsBezelBorder:
|
|
[_box setBorderType:CPBezelBorder];
|
|
break;
|
|
case CPNoTabsLineBorder:
|
|
[_box setBorderType:CPLineBorder];
|
|
break;
|
|
case CPNoTabsNoBorder:
|
|
[_box setBorderType:CPNoBorder];
|
|
break;
|
|
}
|
|
|
|
[self setNeedsLayout];
|
|
}
|
|
|
|
- (void)tileWithChangedItem:(CPTabViewItem)aTabViewItem
|
|
{
|
|
var segment = aTabViewItem ? [self indexOfTabViewItem:aTabViewItem] : 0;
|
|
[_tabs tileWithChangedSegment:segment];
|
|
|
|
[self setNeedsLayout];
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
// Even if CPTabView's autoresizesSubviews is NO, _tabs and _box has to be laid out.
|
|
// This means we can't rely on autoresize masks.
|
|
if (_type !== CPTopTabsBezelBorder && _type !== CPBottomTabsBezelBorder)
|
|
{
|
|
[_box setFrame:[self bounds]];
|
|
}
|
|
else
|
|
{
|
|
var aFrame = [self frame],
|
|
segmentedHeight = CGRectGetHeight([_tabs frame]),
|
|
origin = _type === CPTopTabsBezelBorder ? segmentedHeight / 2 : 0;
|
|
|
|
[_box setFrame:CGRectMake(0, origin, CGRectGetWidth(aFrame),
|
|
CGRectGetHeight(aFrame) - segmentedHeight / 2)];
|
|
|
|
[self _repositionTabs];
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Returns the tab view type.
|
|
@return the tab view type of the receiver
|
|
*/
|
|
- (CPTabViewType)tabViewType
|
|
{
|
|
return _type;
|
|
}
|
|
|
|
/*!
|
|
Returns the receiver's delegate.
|
|
@return the receiver's delegate
|
|
*/
|
|
- (id)delegate
|
|
{
|
|
return _delegate;
|
|
}
|
|
|
|
/*!
|
|
Sets the delegate for this tab view.
|
|
@param aDelegate the tab view's delegate
|
|
*/
|
|
- (void)setDelegate:(id <CPTabViewDelegate>)aDelegate
|
|
{
|
|
if (_delegate == aDelegate)
|
|
return;
|
|
|
|
_delegate = aDelegate;
|
|
|
|
_delegateSelectors = 0;
|
|
|
|
if ([_delegate respondsToSelector:@selector(tabView:shouldSelectTabViewItem:)])
|
|
_delegateSelectors |= CPTabViewShouldSelectTabViewItemSelector;
|
|
|
|
if ([_delegate respondsToSelector:@selector(tabView:willSelectTabViewItem:)])
|
|
_delegateSelectors |= CPTabViewWillSelectTabViewItemSelector;
|
|
|
|
if ([_delegate respondsToSelector:@selector(tabView:didSelectTabViewItem:)])
|
|
_delegateSelectors |= CPTabViewDidSelectTabViewItemSelector;
|
|
|
|
if ([_delegate respondsToSelector:@selector(tabViewDidChangeNumberOfTabViewItems:)])
|
|
_delegateSelectors |= CPTabViewDidChangeNumberOfTabViewItemsSelector;
|
|
}
|
|
|
|
- (void)setBackgroundColor:(CPColor)aColor
|
|
{
|
|
[_box setBackgroundColor:aColor];
|
|
}
|
|
|
|
- (CPColor)backgroundColor
|
|
{
|
|
return [_box backgroundColor];
|
|
}
|
|
|
|
- (void)mouseDown:(CPEvent)anEvent
|
|
{
|
|
var segmentIndex = [_tabs testSegment:[_tabs convertPoint:[anEvent locationInWindow] fromView:nil]];
|
|
|
|
if (segmentIndex != CPNotFound && [self selectTabViewItemAtIndex:segmentIndex])
|
|
[_tabs trackSegment:anEvent];
|
|
}
|
|
|
|
- (void)_repositionTabs
|
|
{
|
|
var horizontalCenterOfSelf = CGRectGetWidth([self bounds]) / 2,
|
|
verticalCenterOfTabs = CGRectGetHeight([_tabs bounds]) / 2;
|
|
|
|
if (_type === CPBottomTabsBezelBorder)
|
|
[_tabs setCenter:CGPointMake(horizontalCenterOfSelf, CGRectGetHeight([self bounds]) - verticalCenterOfTabs)];
|
|
else
|
|
[_tabs setCenter:CGPointMake(horizontalCenterOfSelf, verticalCenterOfTabs)];
|
|
}
|
|
|
|
- (void)_displayItemView:(CPView)aView
|
|
{
|
|
[_box setContentView:aView];
|
|
}
|
|
|
|
// DELEGATE METHODS
|
|
|
|
- (void)_sendDelegateDidSelectTabViewItem:(CPTabViewItem)aTabViewItem
|
|
{
|
|
if (_delegateSelectors & CPTabViewDidSelectTabViewItemSelector)
|
|
[_delegate tabView:self didSelectTabViewItem:aTabViewItem];
|
|
}
|
|
|
|
- (void)_sendDelegateTabViewDidChangeNumberOfTabViewItems
|
|
{
|
|
if (_delegateSelectors & CPTabViewDidChangeNumberOfTabViewItemsSelector)
|
|
[_delegate tabViewDidChangeNumberOfTabViewItems:self];
|
|
}
|
|
|
|
@end
|
|
|
|
var CPTabViewItemsKey = "CPTabViewItemsKey",
|
|
CPTabViewSelectedItemKey = "CPTabViewSelectedItemKey",
|
|
CPTabViewTypeKey = "CPTabViewTypeKey",
|
|
CPTabViewFontKey = "CPTabViewFontKey",
|
|
CPTabViewDelegateKey = "CPTabViewDelegateKey";
|
|
|
|
@implementation CPTabView (CPCoding)
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
if (self = [super initWithCoder:aCoder])
|
|
{
|
|
[self _init];
|
|
|
|
_font = [aCoder decodeObjectForKey:CPTabViewFontKey];
|
|
[_tabs setFont:_font];
|
|
|
|
var items = [aCoder decodeObjectForKey:CPTabViewItemsKey] || [CPArray array];
|
|
[self _insertTabViewItems:items atIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [items count])]];
|
|
|
|
[self setDelegate:[aCoder decodeObjectForKey:CPTabViewDelegateKey]];
|
|
|
|
_selectedTabViewItem = [aCoder decodeObjectForKey:CPTabViewSelectedItemKey];
|
|
_type = [aCoder decodeIntForKey:CPTabViewTypeKey];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)awakeFromCib
|
|
{
|
|
// This cannot be run in initWithCoder because it might call selectTabViewItem:, which is
|
|
// not safe to call before the views of the tab views items are fully decoded.
|
|
|
|
if (_selectedTabViewItem)
|
|
{
|
|
var idx = [self indexOfTabViewItem:_selectedTabViewItem];
|
|
|
|
if (idx !== CPNotFound)
|
|
[self selectTabViewItemAtIndex:idx];
|
|
}
|
|
|
|
var type = _type;
|
|
_type = nil;
|
|
[self setTabViewType:type];
|
|
|
|
[self setNeedsLayout];
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
// Don't bother to encode the CPBox. We will recreate it on decode and its content view is already
|
|
// stored by the tab view item. Not encoding _box makes the resulting archive smaller and reduces
|
|
// the surface for decoding bugs (of which we've had many in tab view).
|
|
var subviews = [self subviews];
|
|
[_box removeFromSuperview];
|
|
[super encodeWithCoder:aCoder];
|
|
[self setSubviews:subviews];
|
|
|
|
[aCoder encodeObject:_items forKey:CPTabViewItemsKey];
|
|
|
|
[aCoder encodeConditionalObject:_selectedTabViewItem forKey:CPTabViewSelectedItemKey];
|
|
|
|
[aCoder encodeInt:_type forKey:CPTabViewTypeKey];
|
|
[aCoder encodeObject:_font forKey:CPTabViewFontKey];
|
|
|
|
[aCoder encodeConditionalObject:_delegate forKey:CPTabViewDelegateKey];
|
|
}
|
|
|
|
@end
|