// // PSMTabBarControl.m // PSMTabBarControl // // Created by John Pannell on 10/13/05. // Copyright 2005 Positive Spin Media. All rights reserved. // #import "PSMTabBarControl.h" #import "PSMTabBarCell.h" #import "PSMOverflowPopUpButton.h" #import "PSMRolloverButton.h" #import "PSMTabStyle.h" #import "PSMMetalTabStyle.h" #import "PSMAquaTabStyle.h" #import "PSMUnifiedTabStyle.h" #import "PSMCardTabStyle.h" #import "PSMAdiumTabStyle.h" #import "PSMSequelProTabStyle.h" #import "PSMTabDragAssistant.h" #import "PSMTabBarController.h" #include /* for GetKeys() and KeyMap */ #include @interface PSMTabBarControl (Private) // constructor/destructor - (void)initAddedProperties; // accessors - (NSEvent *)lastMouseDownEvent; - (void)setLastMouseDownEvent:(NSEvent *)event; // contents - (void)addTabViewItem:(NSTabViewItem *)item; - (void)removeTabForCell:(PSMTabBarCell *)cell; // draw - (void)_setupTrackingRectsForCell:(PSMTabBarCell *)cell; - (void)_positionOverflowMenu; - (void)_checkWindowFrame; // actions - (void)closeTabClick:(id)sender; - (void)tabNothing:(id)sender; // notification handlers - (void)frameDidChange:(NSNotification *)notification; - (void)windowDidMove:(NSNotification *)aNotification; - (void)windowDidUpdate:(NSNotification *)notification; - (void)windowStatusDidChange:(NSNotification *)notification; // NSTabView delegate - (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem; - (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem; - (void)tabView:(NSTabView *)tabView willSelectTabViewItem:(NSTabViewItem *)tabViewItem; - (void)tabViewDidChangeNumberOfTabViewItems:(NSTabView *)tabView; // archiving - (void)encodeWithCoder:(NSCoder *)aCoder; - (id)initWithCoder:(NSCoder *)aDecoder; // convenience - (void)_bindPropertiesForCell:(PSMTabBarCell *)cell andTabViewItem:(NSTabViewItem *)item; - (id)cellForPoint:(NSPoint)point cellFrame:(NSRectPointer)outFrame; - (void)fireSpring:(NSTimer *)timer; - (void)animateShowHide:(NSTimer *)timer; - (void)_animateCells:(NSTimer *)timer; @end @implementation PSMTabBarControl #pragma mark - #pragma mark Characteristics + (NSBundle *)bundle; { static NSBundle *bundle = nil; if (!bundle) bundle = [NSBundle bundleForClass:[PSMTabBarControl class]]; return bundle; } /*! @method availableCellWidth @abstract The number of pixels available for cells @discussion Calculates the number of pixels available for cells based on margins and the window resize badge. @returns Returns the amount of space for cells. */ - (CGFloat)availableCellWidth { return [self frame].size.width - [style leftMarginForTabBarControl] - [style rightMarginForTabBarControl] - _resizeAreaCompensation; } /*! @method genericCellRect @abstract The basic rect for a tab cell. @discussion Creates a generic frame for a tab cell based on the current control state. @returns Returns a basic rect for a tab cell. */ - (NSRect)genericCellRect { NSRect aRect=[self frame]; aRect.origin.x = [style leftMarginForTabBarControl]; aRect.origin.y = 0.0f; aRect.size.width = [self availableCellWidth]; aRect.size.height = [style tabCellHeight]; return aRect; } #pragma mark - #pragma mark Constructor/destructor - (void)initAddedProperties { _cells = [[NSMutableArray alloc] initWithCapacity:10]; _controller = [[PSMTabBarController alloc] initWithTabBarControl:self]; _animationTimer = nil; _lastWindowIsMainCheck = NO; _lastAttachedWindowIsMainCheck = NO; _lastAppIsActiveCheck = NO; _lastMouseDownEvent = nil; // default config _currentStep = kPSMIsNotBeingResized; _orientation = PSMTabBarHorizontalOrientation; _canCloseOnlyTab = NO; _disableTabClose = NO; _showAddTabButton = NO; _hideForSingleTab = NO; _sizeCellsToFit = NO; _isHidden = NO; _awakenedFromNib = NO; _automaticallyAnimates = NO; _useOverflowMenu = YES; _allowsBackgroundTabClosing = YES; _allowsResizing = NO; _selectsTabsOnMouseDown = NO; _alwaysShowActiveTab = NO; _allowsScrubbing = NO; _useSafariStyleDragging = NO; _cellMinWidth = 100; _cellMaxWidth = 280; _cellOptimumWidth = 130; _tearOffStyle = PSMTabBarTearOffAlphaWindow; style = [[PSMSequelProTabStyle alloc] init]; // the overflow button/menu NSRect overflowButtonRect = NSMakeRect([self frame].size.width - [style rightMarginForTabBarControl] + 1, 0, [style rightMarginForTabBarControl] - 1, [self frame].size.height); _overflowPopUpButton = [[PSMOverflowPopUpButton alloc] initWithFrame:overflowButtonRect pullsDown:YES]; [_overflowPopUpButton setAutoresizingMask:NSViewNotSizable | NSViewMinXMargin]; [_overflowPopUpButton setHidden:YES]; [self addSubview:_overflowPopUpButton]; [self _positionOverflowMenu]; // new tab button NSRect addTabButtonRect = NSMakeRect([self frame].size.width - [style rightMarginForTabBarControl] + 1, 3.0f, 16.0f, 16.0f); _addTabButton = [[PSMRolloverButton alloc] initWithFrame:addTabButtonRect]; if (_addTabButton) { NSImage *newButtonImage = [style addTabButtonImage]; if (newButtonImage) { [_addTabButton setUsualImage:newButtonImage]; } newButtonImage = [style addTabButtonPressedImage]; if (newButtonImage) { [_addTabButton setAlternateImage:newButtonImage]; } newButtonImage = [style addTabButtonRolloverImage]; if (newButtonImage) { [_addTabButton setRolloverImage:newButtonImage]; } [_addTabButton setTitle:@""]; [_addTabButton setImagePosition:NSImageOnly]; [_addTabButton setButtonType:NSMomentaryChangeButton]; [_addTabButton setBordered:NO]; [_addTabButton setBezelStyle:NSShadowlessSquareBezelStyle]; [self addSubview:_addTabButton]; if (_showAddTabButton) { [_addTabButton setHidden:NO]; } else { [_addTabButton setHidden:YES]; } [_addTabButton setNeedsDisplay:YES]; } } - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization [self initAddedProperties]; [self registerForDraggedTypes:[NSArray arrayWithObjects:@"PSMTabBarControlItemPBType", nil]]; // resize [self setPostsFrameChangedNotifications:YES]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameDidChange:) name:NSViewFrameDidChangeNotification object:self]; } [self setTarget:self]; return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [self destroyAnimations]; //unbind all the items to prevent crashing //not sure if this is necessary or not NSArray *cells = [NSArray arrayWithArray:_cells]; // create a copy as we will change the original array while being enumerated NSEnumerator *enumerator = [cells objectEnumerator]; PSMTabBarCell *nextCell; while ( (nextCell = [enumerator nextObject]) ) { [self removeTabForCell:nextCell]; } [_overflowPopUpButton release]; [_cells release]; [_controller release]; [tabView release]; [_addTabButton release]; [partnerView release]; [_lastMouseDownEvent release]; [style release]; [self unregisterDraggedTypes]; [super dealloc]; } - (void)awakeFromNib { // build cells from existing tab view items NSArray *existingItems = [tabView tabViewItems]; NSEnumerator *e = [existingItems objectEnumerator]; NSTabViewItem *item; while ( (item = [e nextObject]) ) { if (![[self representedTabViewItems] containsObject:item]) { [self addTabViewItem:item]; } } } - (void)viewWillMoveToWindow:(NSWindow *)aWindow { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver:self name:NSWindowDidBecomeMainNotification object:nil]; [center removeObserver:self name:NSWindowDidResignMainNotification object:nil]; [center removeObserver:self name:NSWindowDidUpdateNotification object:nil]; [center removeObserver:self name:NSWindowDidMoveNotification object:nil]; if (_showHideAnimationTimer) { [_showHideAnimationTimer invalidate]; [_showHideAnimationTimer release]; _showHideAnimationTimer = nil; } if (aWindow) { [center addObserver:self selector:@selector(windowStatusDidChange:) name:NSWindowDidBecomeMainNotification object:aWindow]; [center addObserver:self selector:@selector(windowStatusDidChange:) name:NSWindowDidResignMainNotification object:aWindow]; [center addObserver:self selector:@selector(windowDidUpdate:) name:NSWindowDidUpdateNotification object:aWindow]; [center addObserver:self selector:@selector(windowDidMove:) name:NSWindowDidMoveNotification object:aWindow]; } } /** * Allow a window to be redrawn in response to changes in position or focus level. */ - (void)windowStatusDidChange:(NSNotification *)notification { [self setNeedsDisplay:YES]; } #pragma mark - #pragma mark Accessors - (NSMutableArray *)cells { return _cells; } - (NSEvent *)lastMouseDownEvent { return _lastMouseDownEvent; } - (void)setLastMouseDownEvent:(NSEvent *)event { [event retain]; [_lastMouseDownEvent release]; _lastMouseDownEvent = event; } - (id)delegate { return delegate; } - (void)setDelegate:(id)object { delegate = object; NSMutableArray *types = [NSMutableArray arrayWithObjects:@"PSMTabBarControlItemPBType", NSStringPboardType,nil]; //Update the allowed drag types if ([self delegate] && [[self delegate] respondsToSelector:@selector(allowedDraggedTypesForTabView:)]) { [types addObjectsFromArray:[[self delegate] allowedDraggedTypesForTabView:tabView]]; } [self unregisterDraggedTypes]; [self registerForDraggedTypes:types]; } - (NSTabView *)tabView { return tabView; } - (void)setTabView:(NSTabView *)view { [view retain]; [tabView release]; tabView = view; } - (id)style { return style; } - (NSString *)styleName { return [style name]; } - (void)setStyle:(id )newStyle { if (style != newStyle) { [style autorelease]; style = [newStyle retain]; // restyle add tab button if (_addTabButton) { NSImage *newButtonImage = [style addTabButtonImage]; if (newButtonImage) { [_addTabButton setUsualImage:newButtonImage]; } newButtonImage = [style addTabButtonPressedImage]; if (newButtonImage) { [_addTabButton setAlternateImage:newButtonImage]; } newButtonImage = [style addTabButtonRolloverImage]; if (newButtonImage) { [_addTabButton setRolloverImage:newButtonImage]; } } [self update]; } } - (void)setStyleNamed:(NSString *)name { id newStyle; /* if ([name isEqualToString:@"Aqua"]) { newStyle = [[PSMAquaTabStyle alloc] init]; } else if ([name isEqualToString:@"Unified"]) { newStyle = [[PSMUnifiedTabStyle alloc] init]; } else if ([name isEqualToString:@"Adium"]) { newStyle = [[PSMAdiumTabStyle alloc] init]; } else if ([name isEqualToString:@"Card"]) { newStyle = [[PSMCardTabStyle alloc] init]; } else if ([name isEqualToString:@"Metal"]) { newStyle = [[PSMMetalTabStyle alloc] init]; } else */ if ([name isEqualToString:@"SequelPro"]) { newStyle = [[PSMSequelProTabStyle alloc] init]; } else { newStyle = [[PSMSequelProTabStyle alloc] init]; } [self setStyle:newStyle]; [newStyle release]; } - (PSMTabBarOrientation)orientation { return _orientation; } - (void)setOrientation:(PSMTabBarOrientation)value { PSMTabBarOrientation lastOrientation = _orientation; _orientation = value; if (_tabBarWidth < 10) { _tabBarWidth = 120; } if (lastOrientation != _orientation) { [[self style] setOrientation:_orientation]; [self _positionOverflowMenu]; //move the overflow popup button to the right place [self update:NO]; } } - (BOOL)canCloseOnlyTab { return _canCloseOnlyTab; } - (void)setCanCloseOnlyTab:(BOOL)value { _canCloseOnlyTab = value; if ([_cells count] == 1) { [self update]; } } - (BOOL)disableTabClose { return _disableTabClose; } - (void)setDisableTabClose:(BOOL)value { _disableTabClose = value; [self update]; } - (BOOL)hideForSingleTab { return _hideForSingleTab; } - (void)setHideForSingleTab:(BOOL)value { _hideForSingleTab = value; if ([_cells count] == 1) { [self update]; } } - (BOOL)showAddTabButton { return _showAddTabButton; } - (void)setShowAddTabButton:(BOOL)value { _showAddTabButton = value; if (!NSIsEmptyRect([_controller addButtonRect])) [_addTabButton setFrame:[_controller addButtonRect]]; [_addTabButton setHidden:!_showAddTabButton]; [_addTabButton setNeedsDisplay:YES]; [self update]; } - (id)createNewTabTarget { return _createNewTabTarget; } - (void)setCreateNewTabTarget:(id)object { _createNewTabTarget = object; [[self addTabButton] setTarget:object]; } - (SEL)createNewTabAction { return _createNewTabAction; } - (void)setCreateNewTabAction:(SEL)selector { _createNewTabAction = selector; [[self addTabButton] setAction:selector]; } - (id)doubleClickTarget { return _doubleClickTarget; } - (void)setDoubleClickTarget:(id)object { _doubleClickTarget = object; } - (SEL)doubleClickAction { return _doubleClickAction; } - (void)setDoubleClickAction:(SEL)selector { _doubleClickAction = selector; } - (NSInteger)cellMinWidth { return _cellMinWidth; } - (void)setCellMinWidth:(NSInteger)value { _cellMinWidth = value; [self update]; } - (NSInteger)cellMaxWidth { return _cellMaxWidth; } - (void)setCellMaxWidth:(NSInteger)value { _cellMaxWidth = value; [self update]; } - (NSInteger)cellOptimumWidth { return _cellOptimumWidth; } - (void)setCellOptimumWidth:(NSInteger)value { _cellOptimumWidth = value; [self update]; } - (BOOL)sizeCellsToFit { return _sizeCellsToFit; } - (void)setSizeCellsToFit:(BOOL)value { _sizeCellsToFit = value; [self update]; } - (BOOL)useOverflowMenu { return _useOverflowMenu; } - (void)setUseOverflowMenu:(BOOL)value { _useOverflowMenu = value; [self update]; } - (PSMRolloverButton *)addTabButton { return _addTabButton; } - (PSMOverflowPopUpButton *)overflowPopUpButton { return _overflowPopUpButton; } - (BOOL)allowsBackgroundTabClosing { return _allowsBackgroundTabClosing; } - (void)setAllowsBackgroundTabClosing:(BOOL)value { _allowsBackgroundTabClosing = value; } - (BOOL)allowsResizing { return _allowsResizing; } - (void)setAllowsResizing:(BOOL)value { _allowsResizing = value; } - (BOOL)selectsTabsOnMouseDown { return _selectsTabsOnMouseDown; } - (void)setSelectsTabsOnMouseDown:(BOOL)value { _selectsTabsOnMouseDown = value; } - (BOOL)createsTabOnDoubleClick; { return _createsTabOnDoubleClick; } - (void)setCreatesTabOnDoubleClick:(BOOL)value { _createsTabOnDoubleClick = value; } - (BOOL)automaticallyAnimates { return _automaticallyAnimates; } - (void)setAutomaticallyAnimates:(BOOL)value { _automaticallyAnimates = value; } - (BOOL)alwaysShowActiveTab { return _alwaysShowActiveTab; } - (void)setAlwaysShowActiveTab:(BOOL)value { _alwaysShowActiveTab = value; } - (BOOL)allowsScrubbing { return _allowsScrubbing; } - (void)setAllowsScrubbing:(BOOL)value { _allowsScrubbing = value; } - (BOOL)usesSafariStyleDragging { return _useSafariStyleDragging; } - (void)setUsesSafariStyleDragging:(BOOL)value { _useSafariStyleDragging = value; } - (PSMTabBarTearOffStyle)tearOffStyle { return _tearOffStyle; } - (void)setTearOffStyle:(PSMTabBarTearOffStyle)tearOffStyle { _tearOffStyle = tearOffStyle; } #pragma mark - #pragma mark Functionality - (void)addTabViewItem:(NSTabViewItem *)item { // create cell PSMTabBarCell *cell = [[PSMTabBarCell alloc] initWithControlView:self]; NSRect cellRect, lastCellFrame = [[_cells lastObject] frame]; if ([self orientation] == PSMTabBarHorizontalOrientation) { cellRect = [self genericCellRect]; cellRect.size.width = 30; cellRect.origin.x = lastCellFrame.origin.x + lastCellFrame.size.width; } else { cellRect = /*lastCellFrame*/[self genericCellRect]; cellRect.size.width = lastCellFrame.size.width; cellRect.size.height = 0; cellRect.origin.y = lastCellFrame.origin.y + lastCellFrame.size.height; } [cell setRepresentedObject:item]; [cell setFrame:cellRect]; // bind it up [self bindPropertiesForCell:cell andTabViewItem:item]; // add to collection [_cells addObject:cell]; [cell release]; if ((NSInteger)[_cells count] == [tabView numberOfTabViewItems]) { [self update]; // don't update unless all are accounted for! } } - (void)removeTabForCell:(PSMTabBarCell *)cell { NSTabViewItem *item = [cell representedObject]; // unbind [[cell indicator] unbind:@"animate"]; [[cell indicator] unbind:@"hidden"]; [cell unbind:@"hasIcon"]; [cell unbind:@"hasLargeImage"]; [cell unbind:@"title"]; [cell unbind:@"count"]; [cell unbind:@"countColor"]; [cell unbind:@"isEdited"]; if ([item identifier] != nil) { if ([[item identifier] respondsToSelector:@selector(isProcessing)]) { [[item identifier] removeObserver:cell forKeyPath:@"isProcessing"]; } } if ([item identifier] != nil) { if ([[item identifier] respondsToSelector:@selector(icon)]) { [[item identifier] removeObserver:cell forKeyPath:@"icon"]; } } if ([item identifier] != nil) { if ([[item identifier] respondsToSelector:@selector(count)]) { [[item identifier] removeObserver:cell forKeyPath:@"objectCount"]; } } if ([item identifier] != nil) { if ([[item identifier] respondsToSelector:@selector(countColor)]) { [[item identifier] removeObserver:cell forKeyPath:@"countColor"]; } } if ([item identifier] != nil) { if ([[item identifier] respondsToSelector:@selector(largeImage)]) { [[item identifier] removeObserver:cell forKeyPath:@"largeImage"]; } } if ([item identifier] != nil) { if ([[item identifier] respondsToSelector:@selector(isEdited)]) { [[item identifier] removeObserver:cell forKeyPath:@"isEdited"]; } } // stop watching identifier [item removeObserver:self forKeyPath:@"identifier"]; // remove indicator if ([[self subviews] containsObject:[cell indicator]]) { [[cell indicator] removeFromSuperview]; } // remove tracking [[NSNotificationCenter defaultCenter] removeObserver:cell]; if ([cell closeButtonTrackingTag] != 0) { [self removeTrackingRect:[cell closeButtonTrackingTag]]; [cell setCloseButtonTrackingTag:0]; } if ([cell cellTrackingTag] != 0) { [self removeTrackingRect:[cell cellTrackingTag]]; [cell setCellTrackingTag:0]; } // pull from collection [_cells removeObject:cell]; [self update]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // did the tab's identifier change? if ([keyPath isEqualToString:@"identifier"]) { NSEnumerator *e = [_cells objectEnumerator]; PSMTabBarCell *cell; while ( (cell = [e nextObject]) ) { if ([cell representedObject] == object) { [self _bindPropertiesForCell:cell andTabViewItem:object]; } } } } #pragma mark - #pragma mark Hide/Show - (void)hideTabBar:(BOOL)hide animate:(BOOL)animate { if (!_awakenedFromNib || (_isHidden && hide) || (!_isHidden && !hide)) { return; } [[self subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; _isHidden = hide; _currentStep = 0; if (!animate) { _currentStep = (NSInteger)kPSMHideAnimationSteps; } if (hide) { [_overflowPopUpButton removeFromSuperview]; [_addTabButton removeFromSuperview]; } else if (!animate) { [self addSubview:_overflowPopUpButton]; [self addSubview:_addTabButton]; } CGFloat partnerOriginalSize, partnerOriginalOrigin, myOriginalSize, myOriginalOrigin, partnerTargetSize, partnerTargetOrigin, myTargetSize, myTargetOrigin; // target values for partner if ([self orientation] == PSMTabBarHorizontalOrientation) { // current (original) values myOriginalSize = [self frame].size.height; myOriginalOrigin = [self frame].origin.y; if (partnerView) { partnerOriginalSize = [partnerView frame].size.height; partnerOriginalOrigin = [partnerView frame].origin.y; } else { partnerOriginalSize = [[self window] frame].size.height; partnerOriginalOrigin = [[self window] frame].origin.y; } // Determine the target sizes if (_isHidden) { myTargetSize = kPSMTabBarControlHeightCollapsed; } else { myTargetSize = kPSMTabBarControlHeight; } if (partnerView) { partnerTargetSize = partnerOriginalSize + myOriginalSize - myTargetSize; // above or below me? if ((myOriginalOrigin - kPSMTabBarControlHeight) > partnerOriginalOrigin) { // partner is below me, keeps its origin partnerTargetOrigin = partnerOriginalOrigin; myTargetOrigin = myOriginalOrigin + myOriginalSize - myTargetSize; } else { // partner is above me, I keep my origin myTargetOrigin = myOriginalOrigin; partnerTargetOrigin = partnerOriginalOrigin + myOriginalSize - myTargetSize; } } else { // for window movement myTargetOrigin = myOriginalOrigin; partnerTargetOrigin = partnerOriginalOrigin + myOriginalSize - myTargetSize; partnerTargetSize = partnerOriginalSize - myOriginalSize + myTargetSize; } } else /* vertical */ { // current (original) values myOriginalSize = [self frame].size.width; myOriginalOrigin = [self frame].origin.x; if (partnerView) { partnerOriginalSize = [partnerView frame].size.width; partnerOriginalOrigin = [partnerView frame].origin.x; } else { partnerOriginalSize = [[self window] frame].size.width; partnerOriginalOrigin = [[self window] frame].origin.x; } if (partnerView) { //to the left or right? if (myOriginalOrigin < partnerOriginalOrigin + partnerOriginalSize) { // partner is to the left if (_isHidden) { // I'm shrinking myTargetOrigin = myOriginalOrigin; myTargetSize = 1; partnerTargetOrigin = partnerOriginalOrigin - myOriginalSize + 1; partnerTargetSize = partnerOriginalSize + myOriginalSize - 1; _tabBarWidth = myOriginalSize; } else { // I'm growing myTargetOrigin = myOriginalOrigin; myTargetSize = myOriginalSize + _tabBarWidth; partnerTargetOrigin = partnerOriginalOrigin + _tabBarWidth; partnerTargetSize = partnerOriginalSize - _tabBarWidth; } } else { // partner is to the right if (_isHidden) { // I'm shrinking myTargetOrigin = myOriginalOrigin + myOriginalSize; myTargetSize = 1; partnerTargetOrigin = partnerOriginalOrigin; partnerTargetSize = partnerOriginalSize + myOriginalSize; _tabBarWidth = myOriginalSize; } else { // I'm growing myTargetOrigin = myOriginalOrigin - _tabBarWidth; myTargetSize = myOriginalSize + _tabBarWidth; partnerTargetOrigin = partnerOriginalOrigin; partnerTargetSize = partnerOriginalSize - _tabBarWidth; } } } else { // for window movement if (_isHidden) { // I'm shrinking myTargetOrigin = myOriginalOrigin; myTargetSize = 1; partnerTargetOrigin = partnerOriginalOrigin + myOriginalSize - 1; partnerTargetSize = partnerOriginalSize - myOriginalSize + 1; _tabBarWidth = myOriginalSize; } else { // I'm growing myTargetOrigin = myOriginalOrigin; myTargetSize = _tabBarWidth; partnerTargetOrigin = partnerOriginalOrigin - _tabBarWidth + 1; partnerTargetSize = partnerOriginalSize + _tabBarWidth - 1; } } if (!_isHidden && [[self delegate] respondsToSelector:@selector(desiredWidthForVerticalTabBar:)]) myTargetSize = [[self delegate] desiredWidthForVerticalTabBar:self]; } NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithDouble:myOriginalOrigin], @"myOriginalOrigin", [NSNumber numberWithDouble:partnerOriginalOrigin], @"partnerOriginalOrigin", [NSNumber numberWithDouble:myOriginalSize], @"myOriginalSize", [NSNumber numberWithDouble:partnerOriginalSize], @"partnerOriginalSize", [NSNumber numberWithDouble:myTargetOrigin], @"myTargetOrigin", [NSNumber numberWithDouble:partnerTargetOrigin], @"partnerTargetOrigin", [NSNumber numberWithDouble:myTargetSize], @"myTargetSize", [NSNumber numberWithDouble:partnerTargetSize], @"partnerTargetSize", nil]; if (_showHideAnimationTimer) { [_showHideAnimationTimer invalidate]; [_showHideAnimationTimer release]; } _showHideAnimationTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0 / 30.0) target:self selector:@selector(animateShowHide:) userInfo:userInfo repeats:YES] retain]; } - (void)animateShowHide:(NSTimer *)timer { // moves the frame of the tab bar and window (or partner view) linearly to hide or show the tab bar NSRect myFrame = [self frame]; NSDictionary *userInfo = [timer userInfo]; CGFloat myCurrentOrigin = ([[userInfo objectForKey:@"myOriginalOrigin"] floatValue] + (([[userInfo objectForKey:@"myTargetOrigin"] floatValue] - [[userInfo objectForKey:@"myOriginalOrigin"] floatValue]) * (_currentStep/kPSMHideAnimationSteps))); CGFloat myCurrentSize = ([[userInfo objectForKey:@"myOriginalSize"] floatValue] + (([[userInfo objectForKey:@"myTargetSize"] floatValue] - [[userInfo objectForKey:@"myOriginalSize"] floatValue]) * (_currentStep/kPSMHideAnimationSteps))); CGFloat partnerCurrentOrigin = ([[userInfo objectForKey:@"partnerOriginalOrigin"] floatValue] + (([[userInfo objectForKey:@"partnerTargetOrigin"] floatValue] - [[userInfo objectForKey:@"partnerOriginalOrigin"] floatValue]) * (_currentStep/kPSMHideAnimationSteps))); CGFloat partnerCurrentSize = ([[userInfo objectForKey:@"partnerOriginalSize"] floatValue] + (([[userInfo objectForKey:@"partnerTargetSize"] floatValue] - [[userInfo objectForKey:@"partnerOriginalSize"] floatValue]) * (_currentStep/kPSMHideAnimationSteps))); NSRect myNewFrame; if ([self orientation] == PSMTabBarHorizontalOrientation) { myNewFrame = NSMakeRect(myFrame.origin.x, myCurrentOrigin, myFrame.size.width, myCurrentSize); } else { myNewFrame = NSMakeRect(myCurrentOrigin, myFrame.origin.y, myCurrentSize, myFrame.size.height); } if (partnerView) { // resize self and view NSRect resizeRect; if ([self orientation] == PSMTabBarHorizontalOrientation) { resizeRect = NSMakeRect([partnerView frame].origin.x, partnerCurrentOrigin, [partnerView frame].size.width, partnerCurrentSize); } else { resizeRect = NSMakeRect(partnerCurrentOrigin, [partnerView frame].origin.y, partnerCurrentSize, [partnerView frame].size.height); } [partnerView setFrame:resizeRect]; [partnerView setNeedsDisplay:YES]; [self setFrame:myNewFrame]; } else { // resize self and window NSRect resizeRect; if ([self orientation] == PSMTabBarHorizontalOrientation) { resizeRect = NSMakeRect([[self window] frame].origin.x, partnerCurrentOrigin, [[self window] frame].size.width, partnerCurrentSize); } else { resizeRect = NSMakeRect(partnerCurrentOrigin, [[self window] frame].origin.y, partnerCurrentSize, [[self window] frame].size.height); } [[self window] setFrame:resizeRect display:YES]; [self setFrame:myNewFrame]; } // next _currentStep++; if (_currentStep == kPSMHideAnimationSteps + 1) { _currentStep = kPSMIsNotBeingResized; [self viewDidEndLiveResize]; [self update:NO]; //send the delegate messages if (_isHidden) { if ([self delegate] && [[self delegate] respondsToSelector:@selector(tabView:tabBarDidHide:)]) { [[self delegate] tabView:[self tabView] tabBarDidHide:self]; } } else { [self addSubview:_overflowPopUpButton]; [self addSubview:_addTabButton]; if ([self delegate] && [[self delegate] respondsToSelector:@selector(tabView:tabBarDidUnhide:)]) { [[self delegate] tabView:[self tabView] tabBarDidUnhide:self]; } } [_showHideAnimationTimer invalidate]; [_showHideAnimationTimer release]; _showHideAnimationTimer = nil; } [[self window] display]; } - (BOOL)isTabBarHidden { return _isHidden; } - (BOOL)isAnimating { return _animationTimer != nil; } - (id)partnerView { return partnerView; } - (void)setPartnerView:(id)view { [partnerView release]; [view retain]; partnerView = view; } - (void)destroyAnimations { // Stop any animations that may be running [_animationTimer invalidate]; [_animationTimer release]; _animationTimer = nil; [_showHideAnimationTimer invalidate]; [_showHideAnimationTimer release]; _showHideAnimationTimer = nil; // Also unwind the spring, if it's wound. [_springTimer invalidate]; [_springTimer release]; _springTimer = nil; } #pragma mark - #pragma mark Drawing - (BOOL)isFlipped { return YES; } - (void)drawRect:(NSRect)rect { [style drawTabBar:self inRect:rect]; } - (void)update { [self update:_automaticallyAnimates]; } - (void)update:(BOOL)animate { // make sure all of our tabs are accounted for before updating, // or only proceed if a drag is in progress (where counts may mismatch) if ([[self tabView] numberOfTabViewItems] != (NSInteger)[_cells count] && ![[PSMTabDragAssistant sharedDragAssistant] isDragging]) { return; } // hide/show? (these return if already in desired state) if ( (_hideForSingleTab) && ([_cells count] <= 1) ) { [self hideTabBar:YES animate:YES]; return; } else { [self hideTabBar:NO animate:YES]; } [self removeAllToolTips]; [_controller layoutCells]; //eventually we should only have to call this when we know something has changed PSMTabBarCell *currentCell; NSMenu *overflowMenu = [_controller overflowMenu]; [_overflowPopUpButton setHidden:(overflowMenu == nil)]; [_overflowPopUpButton setMenu:overflowMenu]; if (_animationTimer) { [_animationTimer invalidate]; [_animationTimer release]; _animationTimer = nil; } if (animate) { NSMutableArray *targetFrames = [NSMutableArray arrayWithCapacity:[_cells count]]; for (NSUInteger i = 0; i < [_cells count]; i++) { currentCell = [_cells objectAtIndex:i]; //we're going from NSRect -> NSValue -> NSRect -> NSValue here - oh well [targetFrames addObject:[NSValue valueWithRect:[_controller cellFrameAtIndex:i]]]; } [_addTabButton setHidden:!_showAddTabButton]; NSAnimation *animation = [[NSAnimation alloc] initWithDuration:0.50 animationCurve:NSAnimationEaseInOut]; [animation setAnimationBlockingMode:NSAnimationNonblocking]; [animation startAnimation]; _animationTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0 / 30.0 target:self selector:@selector(_animateCells:) userInfo:[NSArray arrayWithObjects:targetFrames, animation, nil] repeats:YES] retain]; [animation release]; [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSEventTrackingRunLoopMode]; [self _animateCells:_animationTimer]; } else { for (NSUInteger i = 0; i < [_cells count]; i++) { currentCell = [_cells objectAtIndex:i]; [currentCell setFrame:[_controller cellFrameAtIndex:i]]; if (![currentCell isInOverflowMenu]) { [self _setupTrackingRectsForCell:currentCell]; } } [_addTabButton setFrame:[_controller addButtonRect]]; [_addTabButton setHidden:!_showAddTabButton]; [self setNeedsDisplay:YES]; } } - (void)_animateCells:(NSTimer *)timer { NSAnimation *animation = [[timer userInfo] objectAtIndex:1]; NSArray *targetFrames = [[timer userInfo] objectAtIndex:0]; PSMTabBarCell *currentCell; NSUInteger cellCount = (NSUInteger)[_cells count]; if ((cellCount > 0) && [animation isAnimating]) { //compare our target position with the current position and move towards the target for (NSUInteger i = 0; i < [targetFrames count] && i < cellCount; i++) { currentCell = [_cells objectAtIndex:i]; NSRect cellFrame = [currentCell frame], targetFrame = [[targetFrames objectAtIndex:i] rectValue]; CGFloat sizeChange; CGFloat originChange; if ([self orientation] == PSMTabBarHorizontalOrientation) { sizeChange = (targetFrame.size.width - cellFrame.size.width) * [animation currentProgress]; originChange = (targetFrame.origin.x - cellFrame.origin.x) * [animation currentProgress]; cellFrame.size.width += sizeChange; cellFrame.origin.x += originChange; } else { sizeChange = (targetFrame.size.height - cellFrame.size.height) * [animation currentProgress]; originChange = (targetFrame.origin.y - cellFrame.origin.y) * [animation currentProgress]; cellFrame.size.height += sizeChange; cellFrame.origin.y += originChange; } [currentCell setFrame:cellFrame]; //highlight the cell if the mouse is over it NSPoint mousePoint = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:nil]; NSRect closeRect = [currentCell closeButtonRectForFrame:cellFrame]; [currentCell setHighlighted:NSMouseInRect(mousePoint, cellFrame, [self isFlipped])]; [currentCell setCloseButtonOver:NSMouseInRect(mousePoint, closeRect, [self isFlipped])]; } if (_showAddTabButton) { //animate the add tab button NSRect target = [_controller addButtonRect], frame = [_addTabButton frame]; frame.origin.x += (target.origin.x - frame.origin.x) * [animation currentProgress]; [_addTabButton setFrame:frame]; } } else { //put all the cells where they should be in their final position if (cellCount > 0) { for (NSUInteger i = 0; i < [targetFrames count] && i < cellCount; i++) { currentCell = [_cells objectAtIndex:i]; NSRect cellFrame = [currentCell frame], targetFrame = [[targetFrames objectAtIndex:i] rectValue]; if ([self orientation] == PSMTabBarHorizontalOrientation) { cellFrame.size.width = targetFrame.size.width; cellFrame.origin.x = targetFrame.origin.x; } else { cellFrame.size.height = targetFrame.size.height; cellFrame.origin.y = targetFrame.origin.y; } [currentCell setFrame:cellFrame]; //highlight the cell if the mouse is over it NSPoint mousePoint = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:nil]; NSRect closeRect = [currentCell closeButtonRectForFrame:cellFrame]; [currentCell setHighlighted:NSMouseInRect(mousePoint, cellFrame, [self isFlipped])]; [currentCell setCloseButtonOver:NSMouseInRect(mousePoint, closeRect, [self isFlipped])]; } } //set the frame for the add tab button if (_showAddTabButton) { NSRect frame = [_addTabButton frame]; frame.origin.x = [_controller addButtonRect].origin.x; [_addTabButton setFrame:frame]; } [_animationTimer invalidate]; [_animationTimer release]; _animationTimer = nil; for (NSUInteger i = 0; i < cellCount; i++) { currentCell = [_cells objectAtIndex:i]; //we've hit the cells that are in overflow, stop setting up tracking rects if ([currentCell isInOverflowMenu]) { break; } [self _setupTrackingRectsForCell:currentCell]; } } [self setNeedsDisplay:YES]; } - (void)_setupTrackingRectsForCell:(PSMTabBarCell *)cell { // Skip tracking rects for placeholders - not required. if ([cell isPlaceholder]) return; NSInteger tag; NSUInteger anIndex = [_cells indexOfObject:cell]; NSRect cellTrackingRect = [_controller cellTrackingRectAtIndex:anIndex]; NSPoint mousePoint = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:nil]; BOOL mouseInCell = NSMouseInRect(mousePoint, cellTrackingRect, [self isFlipped]); // If dragging, suppress mouse interaction if ([[PSMTabDragAssistant sharedDragAssistant] isDragging]) mouseInCell = NO; //set the cell tracking rect [self removeTrackingRect:[cell cellTrackingTag]]; tag = [self addTrackingRect:cellTrackingRect owner:cell userData:nil assumeInside:mouseInCell]; [cell setCellTrackingTag:tag]; [cell setHighlighted:mouseInCell]; if ([cell hasCloseButton] && ![cell isCloseButtonSuppressed]) { NSRect closeRect = [_controller closeButtonTrackingRectAtIndex:anIndex]; BOOL mouseInCloseRect = NSMouseInRect(mousePoint, closeRect, [self isFlipped]); //set the close button tracking rect [self removeTrackingRect:[cell closeButtonTrackingTag]]; tag = [self addTrackingRect:closeRect owner:cell userData:nil assumeInside:mouseInCloseRect]; [cell setCloseButtonTrackingTag:tag]; [cell setCloseButtonOver:mouseInCloseRect]; } //set the tooltip tracking rect [self addToolTipRect:[cell frame] owner:self userData:nil]; } - (void)_positionOverflowMenu { NSRect cellRect, frame = [self frame]; cellRect.size.height = [style tabCellHeight]; cellRect.size.width = [style rightMarginForTabBarControl]; if ([self orientation] == PSMTabBarHorizontalOrientation) { cellRect.origin.y = 0; cellRect.origin.x = frame.size.width - [style rightMarginForTabBarControl] + (_resizeAreaCompensation ? -(_resizeAreaCompensation - 1) : 1); [_overflowPopUpButton setAutoresizingMask:NSViewNotSizable | NSViewMinXMargin]; } else { cellRect.origin.x = 0; cellRect.origin.y = frame.size.height - [style tabCellHeight]; cellRect.size.width = frame.size.width; [_overflowPopUpButton setAutoresizingMask:NSViewNotSizable | NSViewMinXMargin | NSViewMinYMargin]; } [_overflowPopUpButton setFrame:cellRect]; } - (void)_checkWindowFrame { //figure out if the new frame puts the control in the way of the resize widget NSWindow *window = [self window]; if (window) { NSRect resizeWidgetFrame = [[window contentView] frame]; resizeWidgetFrame.origin.x += resizeWidgetFrame.size.width - 22; resizeWidgetFrame.size.width = 22; resizeWidgetFrame.size.height = 22; if ([window showsResizeIndicator] && NSIntersectsRect([self frame], resizeWidgetFrame)) { //the resize widgets are larger on metal windows _resizeAreaCompensation = [window styleMask] & NSTexturedBackgroundWindowMask ? 20 : 8; } else { _resizeAreaCompensation = 0; } [self _positionOverflowMenu]; } } #pragma mark - #pragma mark Mouse Tracking - (BOOL)mouseDownCanMoveWindow { return NO; } - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent { return YES; } - (void)mouseDown:(NSEvent *)theEvent { _didDrag = NO; // keep for dragging [self setLastMouseDownEvent:theEvent]; // what cell? NSPoint mousePt = [self convertPoint:[theEvent locationInWindow] fromView:nil]; NSRect frame = [self frame]; if ([self orientation] == PSMTabBarVerticalOrientation && [self allowsResizing] && partnerView && (mousePt.x > frame.size.width - 3)) { _resizing = YES; } NSRect cellFrame; PSMTabBarCell *cell = [self cellForPoint:mousePt cellFrame:&cellFrame]; if (cell) { BOOL overClose = NSMouseInRect(mousePt, [cell closeButtonRectForFrame:cellFrame], [self isFlipped]); if (overClose && ![self disableTabClose] && ![cell isCloseButtonSuppressed] && ([self allowsBackgroundTabClosing] || [[cell representedObject] isEqualTo:[tabView selectedTabViewItem]] || [theEvent modifierFlags] & NSCommandKeyMask)) { [cell setCloseButtonOver:NO]; [cell setCloseButtonPressed:YES]; _closeClicked = YES; } else if ([theEvent clickCount] == 2) { [cell setCloseButtonOver:NO]; [_doubleClickTarget performSelector:_doubleClickAction withObject:cell]; } else { [cell setCloseButtonPressed:NO]; if (_selectsTabsOnMouseDown) { [self performSelector:@selector(tabClick:) withObject:cell]; } } [self setNeedsDisplay:YES]; } else { if ([theEvent clickCount] == 2) { // fire create new tab if ([self createsTabOnDoubleClick] && [self createNewTabTarget] != nil && [self createNewTabAction] != nil) { [[self createNewTabTarget] performSelector:[self createNewTabAction]]; } return; } } } - (void)mouseDragged:(NSEvent *)theEvent { if (![self lastMouseDownEvent]) { return; } NSPoint currentPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil]; if (_resizing) { NSRect frame = [self frame]; CGFloat resizeAmount = [theEvent deltaX]; if ((currentPoint.x > frame.size.width && resizeAmount > 0) || (currentPoint.x < frame.size.width && resizeAmount < 0)) { [[NSCursor resizeLeftRightCursor] push]; NSRect partnerFrame = [partnerView frame]; //do some bounds checking if ((frame.size.width + resizeAmount > [self cellMinWidth]) && (frame.size.width + resizeAmount < [self cellMaxWidth])) { frame.size.width += resizeAmount; partnerFrame.size.width -= resizeAmount; partnerFrame.origin.x += resizeAmount; [self setFrame:frame]; [partnerView setFrame:partnerFrame]; [[self superview] setNeedsDisplay:YES]; } } return; } NSRect cellFrame; NSPoint trackingStartPoint = [self convertPoint:[[self lastMouseDownEvent] locationInWindow] fromView:nil]; PSMTabBarCell *cell = [self cellForPoint:trackingStartPoint cellFrame:&cellFrame]; if (cell) { //check to see if the close button was the target in the clicked cell //highlight/unhighlight the close button as necessary NSRect iconRect = [cell closeButtonRectForFrame:cellFrame]; if (_closeClicked && NSMouseInRect(trackingStartPoint, iconRect, [self isFlipped]) && ([self allowsBackgroundTabClosing] || [[cell representedObject] isEqualTo:[tabView selectedTabViewItem]])) { [cell setCloseButtonPressed:NSMouseInRect(currentPoint, iconRect, [self isFlipped])]; [self setNeedsDisplay:YES]; return; } CGFloat dx = fabsf(currentPoint.x - trackingStartPoint.x); CGFloat dy = fabsf(currentPoint.y - trackingStartPoint.y); CGFloat distance = sqrtf(dx * dx + dy * dy); if (distance >= 10 && !_didDrag && ![[PSMTabDragAssistant sharedDragAssistant] isDragging] && [self delegate] && [[self delegate] respondsToSelector:@selector(tabView:shouldDragTabViewItem:fromTabBar:)] && [[self delegate] tabView:tabView shouldDragTabViewItem:[cell representedObject] fromTabBar:self]) { _didDrag = YES; [[PSMTabDragAssistant sharedDragAssistant] startDraggingCell:cell fromTabBar:self withMouseDownEvent:[self lastMouseDownEvent]]; } } } - (void)mouseUp:(NSEvent *)theEvent { if (![self lastMouseDownEvent]) { return; } if (_resizing) { _resizing = NO; [[NSCursor arrowCursor] set]; } else { // what cell? NSPoint mousePt = [self convertPoint:[theEvent locationInWindow] fromView:nil]; NSRect cellFrame, mouseDownCellFrame; PSMTabBarCell *cell = [self cellForPoint:mousePt cellFrame:&cellFrame]; PSMTabBarCell *mouseDownCell = [self cellForPoint:[self convertPoint:[[self lastMouseDownEvent] locationInWindow] fromView:nil] cellFrame:&mouseDownCellFrame]; if (cell) { NSPoint trackingStartPoint = [self convertPoint:[[self lastMouseDownEvent] locationInWindow] fromView:nil]; NSRect iconRect = [mouseDownCell closeButtonRectForFrame:mouseDownCellFrame]; if ((NSMouseInRect(mousePt, iconRect,[self isFlipped])) && ![self disableTabClose] && ![cell isCloseButtonSuppressed] && [mouseDownCell closeButtonPressed]) { if (([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask) != 0) { //If the user is holding Option, close all other tabs NSEnumerator *enumerator = [[[[self cells] copy] autorelease] objectEnumerator]; PSMTabBarCell *otherCell; while ((otherCell = [enumerator nextObject])) { if (otherCell != cell) [self performSelector:@selector(closeTabClick:) withObject:otherCell]; } //Fix the close button for the clicked tab not to be pressed [cell setCloseButtonPressed:NO]; } else { //Otherwise, close this tab [self performSelector:@selector(closeTabClick:) withObject:cell]; } } else if (NSMouseInRect(mousePt, mouseDownCellFrame, [self isFlipped]) && (!NSMouseInRect(trackingStartPoint, [cell closeButtonRectForFrame:cellFrame], [self isFlipped]) || ![self allowsBackgroundTabClosing] || [self disableTabClose])) { [mouseDownCell setCloseButtonPressed:NO]; // If -[self selectsTabsOnMouseDown] is TRUE, we already performed tabClick: on mouseDown. if (![self selectsTabsOnMouseDown]) { [self performSelector:@selector(tabClick:) withObject:cell]; } } else { [mouseDownCell setCloseButtonPressed:NO]; [self performSelector:@selector(tabNothing:) withObject:cell]; } } _closeClicked = NO; } // Clear the last mouse down event to prevent drag issues [self setLastMouseDownEvent:nil]; } - (NSMenu *)menuForEvent:(NSEvent *)event { NSMenu *menu = nil; NSTabViewItem *item = [[self cellForPoint:[self convertPoint:[event locationInWindow] fromView:nil] cellFrame:nil] representedObject]; if (item && [[self delegate] respondsToSelector:@selector(tabView:menuForTabViewItem:)]) { menu = [[self delegate] tabView:tabView menuForTabViewItem:item]; } return menu; } #pragma mark - #pragma mark Drag and Drop - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)theEvent { return YES; } // NSDraggingSource - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { return (isLocal ? NSDragOperationMove : NSDragOperationNone); } - (BOOL)ignoreModifierKeysWhileDragging { return YES; } - (void)draggedImage:(NSImage *)anImage beganAt:(NSPoint)screenPoint { [[PSMTabDragAssistant sharedDragAssistant] draggingBeganAt:screenPoint]; } - (void)draggedImage:(NSImage *)image movedTo:(NSPoint)screenPoint { [[PSMTabDragAssistant sharedDragAssistant] draggingMovedTo:screenPoint]; } // NSDraggingDestination - (NSDragOperation)draggingEntered:(id )sender { if([[[sender draggingPasteboard] types] indexOfObject:@"PSMTabBarControlItemPBType"] != NSNotFound) { if ([self delegate] && [[self delegate] respondsToSelector:@selector(tabView:shouldDropTabViewItem:inTabBar:)] && ![[self delegate] tabView:[[sender draggingSource] tabView] shouldDropTabViewItem:[[[PSMTabDragAssistant sharedDragAssistant] draggedCell] representedObject] inTabBar:self]) { return NSDragOperationNone; } [[PSMTabDragAssistant sharedDragAssistant] draggingEnteredTabBar:self atPoint:[self convertPoint:[sender draggingLocation] fromView:nil]]; return NSDragOperationMove; } return NSDragOperationNone; } - (NSDragOperation)draggingUpdated:(id )sender { PSMTabBarCell *cell = [self cellForPoint:[self convertPoint:[sender draggingLocation] fromView:nil] cellFrame:nil]; if ([[[sender draggingPasteboard] types] indexOfObject:@"PSMTabBarControlItemPBType"] != NSNotFound) { if ([self delegate] && [[self delegate] respondsToSelector:@selector(tabView:shouldDropTabViewItem:inTabBar:)] && ![[self delegate] tabView:[[sender draggingSource] tabView] shouldDropTabViewItem:[[[PSMTabDragAssistant sharedDragAssistant] draggedCell] representedObject] inTabBar:self]) { return NSDragOperationNone; } [[PSMTabDragAssistant sharedDragAssistant] draggingUpdatedInTabBar:self atPoint:[self convertPoint:[sender draggingLocation] fromView:nil]]; return NSDragOperationMove; } else if (cell) { //something that was accepted by the delegate was dragged on //Test for the space bar (the skip-the-delay key). enum { virtualKeycodeForSpace = 49 }; //Source: IM:Tx (Fig. C-2) union { KeyMap keymap; char bits[16]; } keymap; GetKeys(keymap.keymap); if ((GetCurrentEventKeyModifiers() == 0) && bit_test(keymap.bits, virtualKeycodeForSpace)) { //The user pressed the space bar. This skips the delay; the user wants to pop the spring on this tab *now*. //For some reason, it crashes if I call -fire here. I don't know why. It doesn't crash if I simply set the fire date to now. [_springTimer setFireDate:[NSDate date]]; } else { //Wind the spring for a spring-loaded drop. //The delay time comes from Finder's defaults, which specifies it in milliseconds. //If the delegate can't handle our spring-loaded drop, we'll abort it when the timer fires. See fireSpring:. This is simpler than constantly (checking for spring-loaded awareness and tearing down/rebuilding the timer) at every delegate change. //If the user has dragged to a different tab, reset the timer. if (_tabViewItemWithSpring != [cell representedObject]) { [_springTimer invalidate]; [_springTimer release]; _springTimer = nil; _tabViewItemWithSpring = [cell representedObject]; } if (!_springTimer) { //Finder's default delay time, as of Tiger, is 668 ms. If the user has never changed it, there's no setting in its defaults, so we default to that amount. NSNumber *delayNumber = [(NSNumber *)CFPreferencesCopyAppValue((CFStringRef)@"SpringingDelayMilliseconds", (CFStringRef)@"com.apple.finder") autorelease]; NSTimeInterval delaySeconds = delayNumber ? [delayNumber doubleValue] / 1000.0 : 0.668; _springTimer = [[NSTimer scheduledTimerWithTimeInterval:delaySeconds target:self selector:@selector(fireSpring:) userInfo:sender repeats:NO] retain]; } // Notify the delegate to respond to drag events if supported. This allows custom // behaviour when dragging certain drag types onto the tab - for example changing the // view appropriately. if ([self delegate] && [[self delegate] respondsToSelector:@selector(draggingEvent:enteredTabBar:tabView:)]) { [[self delegate] draggingEvent:sender enteredTabBar:self tabView:[cell representedObject]]; } } return NSDragOperationCopy; } return NSDragOperationNone; } - (void)draggingExited:(id )sender { [_springTimer invalidate]; [_springTimer release]; _springTimer = nil; [[PSMTabDragAssistant sharedDragAssistant] draggingExitedTabBar:self]; } - (BOOL)prepareForDragOperation:(id )sender { //validate the drag operation only if there's a valid tab bar to drop into return [[[sender draggingPasteboard] types] indexOfObject:@"PSMTabBarControlItemPBType"] == NSNotFound || [[PSMTabDragAssistant sharedDragAssistant] destinationTabBar] != nil; } - (BOOL)performDragOperation:(id )sender { if ([[[sender draggingPasteboard] types] indexOfObject:@"PSMTabBarControlItemPBType"] != NSNotFound) { [[PSMTabDragAssistant sharedDragAssistant] performDragOperation]; } else if ([self delegate] && [[self delegate] respondsToSelector:@selector(tabView:acceptedDraggingInfo:onTabViewItem:)]) { //forward the drop to the delegate [[self delegate] tabView:tabView acceptedDraggingInfo:sender onTabViewItem:[[self cellForPoint:[self convertPoint:[sender draggingLocation] fromView:nil] cellFrame:nil] representedObject]]; } return YES; } - (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation { [[PSMTabDragAssistant sharedDragAssistant] draggedImageEndedAt:aPoint operation:operation]; } - (void)concludeDragOperation:(id )sender { } #pragma mark - #pragma mark Spring-loading - (void)fireSpring:(NSTimer *)timer { NSAssert1(timer == _springTimer, @"Spring fired by unrecognized timer %@", timer); id sender = [timer userInfo]; PSMTabBarCell *cell = [self cellForPoint:[self convertPoint:[sender draggingLocation] fromView:nil] cellFrame:nil]; [tabView selectTabViewItem:[cell representedObject]]; _tabViewItemWithSpring = nil; [_springTimer invalidate]; [_springTimer release]; _springTimer = nil; } #pragma mark - #pragma mark Actions - (void)overflowMenuAction:(id)sender { NSTabViewItem *tabViewItem = (NSTabViewItem *)[sender representedObject]; [tabView selectTabViewItem:tabViewItem]; } - (void)closeTabClick:(id)sender { NSTabViewItem *item = [sender representedObject]; [sender retain]; if(([_cells count] == 1) && (![self canCloseOnlyTab])) return; if ([[self delegate] respondsToSelector:@selector(tabView:shouldCloseTabViewItem:)]) { if (![[self delegate] tabView:tabView shouldCloseTabViewItem:item]) { // fix mouse downed close button [sender setCloseButtonPressed:NO]; [sender release]; return; } } [item retain]; [tabView removeTabViewItem:item]; [item release]; [sender release]; } - (void)tabClick:(id)sender { [tabView selectTabViewItem:[sender representedObject]]; } - (void)tabNothing:(id)sender { //[self update]; // takes care of highlighting based on state } - (void)frameDidChange:(NSNotification *)notification { [self _checkWindowFrame]; // trying to address the drawing artifacts for the progress indicators - hackery follows // this one fixes the "blanking" effect when the control hides and shows itself NSEnumerator *e = [_cells objectEnumerator]; PSMTabBarCell *cell; while ( (cell = [e nextObject]) ) { [[cell indicator] stopAnimation:self]; [[cell indicator] performSelector:@selector(startAnimation:) withObject:nil afterDelay:0]; } [self update:NO]; } - (void)viewDidMoveToWindow { [self _checkWindowFrame]; } - (void)viewWillStartLiveResize { NSEnumerator *e = [_cells objectEnumerator]; PSMTabBarCell *cell; while ( (cell = [e nextObject]) ) { [[cell indicator] stopAnimation:self]; } [self setNeedsDisplay:YES]; } -(void)viewDidEndLiveResize { NSEnumerator *e = [_cells objectEnumerator]; PSMTabBarCell *cell; while ( (cell = [e nextObject]) ) { [[cell indicator] startAnimation:self]; } [self _checkWindowFrame]; [self update:NO]; } - (void)resetCursorRects { [super resetCursorRects]; if ([self orientation] == PSMTabBarVerticalOrientation) { NSRect frame = [self frame]; [self addCursorRect:NSMakeRect(frame.size.width - 2, 0, 2, frame.size.height) cursor:[NSCursor resizeLeftRightCursor]]; } } - (void)windowDidMove:(NSNotification *)aNotification { [self setNeedsDisplay:YES]; } - (void)windowDidUpdate:(NSNotification *)notification { // hide? must readjust things if I'm not supposed to be showing // this block of code only runs when the app launches if (!_awakenedFromNib && [self hideForSingleTab] && ([_cells count] <= 1)) { // must adjust frames now before display NSRect myFrame = [self frame]; if ([self orientation] == PSMTabBarHorizontalOrientation) { if (partnerView) { NSRect partnerFrame = [partnerView frame]; // above or below me? if (myFrame.origin.y - 22 > [partnerView frame].origin.y) { // partner is below me [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y + 21, myFrame.size.width, myFrame.size.height - 21)]; [partnerView setFrame:NSMakeRect(partnerFrame.origin.x, partnerFrame.origin.y, partnerFrame.size.width, partnerFrame.size.height + 21)]; } else { // partner is above me [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y, myFrame.size.width, myFrame.size.height - 21)]; [partnerView setFrame:NSMakeRect(partnerFrame.origin.x, partnerFrame.origin.y - 21, partnerFrame.size.width, partnerFrame.size.height + 21)]; } [partnerView setNeedsDisplay:YES]; [self setNeedsDisplay:YES]; } else { // for window movement NSRect windowFrame = [[self window] frame]; [[self window] setFrame:NSMakeRect(windowFrame.origin.x, windowFrame.origin.y + 21, windowFrame.size.width, windowFrame.size.height - 21) display:YES]; [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y, myFrame.size.width, myFrame.size.height - 21)]; } } else { if (partnerView) { NSRect partnerFrame = [partnerView frame]; //to the left or right? if (myFrame.origin.x < [partnerView frame].origin.x) { // partner is to the left [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y, 1, myFrame.size.height)]; [partnerView setFrame:NSMakeRect(partnerFrame.origin.x - myFrame.size.width + 1, partnerFrame.origin.y, partnerFrame.size.width + myFrame.size.width - 1, partnerFrame.size.height)]; } else { // partner to the right [self setFrame:NSMakeRect(myFrame.origin.x + myFrame.size.width, myFrame.origin.y, 1, myFrame.size.height)]; [partnerView setFrame:NSMakeRect(partnerFrame.origin.x, partnerFrame.origin.y, partnerFrame.size.width + myFrame.size.width, partnerFrame.size.height)]; } _tabBarWidth = myFrame.size.width; [partnerView setNeedsDisplay:YES]; [self setNeedsDisplay:YES]; } else { // for window movement NSRect windowFrame = [[self window] frame]; [[self window] setFrame:NSMakeRect(windowFrame.origin.x + myFrame.size.width - 1, windowFrame.origin.y, windowFrame.size.width - myFrame.size.width + 1, windowFrame.size.height) display:YES]; [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y, 1, myFrame.size.height)]; } } _isHidden = YES; if ([[self delegate] respondsToSelector:@selector(tabView:tabBarDidHide:)]) { [[self delegate] tabView:[self tabView] tabBarDidHide:self]; } // The above tasks only needs to be run once, so set a flag to ensure that _awakenedFromNib = YES; } // Determine whether a draw update in response to window state change might be required BOOL isMainWindow = [[self window] isMainWindow]; BOOL attachedWindowIsMainWindow = [[[self window] attachedSheet] isMainWindow]; BOOL isActiveApplication = [NSApp isActive]; if (_lastWindowIsMainCheck != isMainWindow || _lastAttachedWindowIsMainCheck != attachedWindowIsMainWindow || _lastAppIsActiveCheck != isActiveApplication) { _lastWindowIsMainCheck = isMainWindow; _lastAttachedWindowIsMainCheck = attachedWindowIsMainWindow; _lastAppIsActiveCheck = isActiveApplication; // Allow the tab bar to redraw itself in result to window ordering/sheet/etc changes [self setNeedsDisplay:YES]; } } #pragma mark - #pragma mark Menu Validation - (BOOL)validateMenuItem:(NSMenuItem *)sender { [sender setState:([[sender representedObject] isEqualTo:[tabView selectedTabViewItem]]) ? NSOnState : NSOffState]; return [[self delegate] respondsToSelector:@selector(tabView:validateOverflowMenuItem:forTabViewItem:)] ? [[self delegate] tabView:[self tabView] validateOverflowMenuItem:sender forTabViewItem:[sender representedObject]] : YES; } #pragma mark - #pragma mark NSTabView Delegate - (void)tabView:(NSTabView *)aTabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem { // here's a weird one - this message is sent before the "tabViewDidChangeNumberOfTabViewItems" // message, thus I can end up updating when there are no cells, if no tabs were (yet) present NSInteger tabIndex = [aTabView indexOfTabViewItem:tabViewItem]; if ([_cells count] > 0 && tabIndex < (NSInteger)[_cells count]) { PSMTabBarCell *thisCell = [_cells objectAtIndex:tabIndex]; if (_alwaysShowActiveTab && [thisCell isInOverflowMenu]) { //temporarily disable the delegate in order to move the tab to a different index id tempDelegate = [aTabView delegate]; [aTabView setDelegate:nil]; // move it all around first [tabViewItem retain]; [thisCell retain]; [aTabView removeTabViewItem:tabViewItem]; [aTabView insertTabViewItem:tabViewItem atIndex:0]; [_cells removeObjectAtIndex:tabIndex]; [_cells insertObject:thisCell atIndex:0]; [thisCell setIsInOverflowMenu:NO]; //very important else we get a fun recursive loop going [[_cells objectAtIndex:[_cells count] - 1] setIsInOverflowMenu:YES]; //these 2 lines are pretty uncool and this logic needs to be updated [thisCell release]; [tabViewItem release]; [aTabView setDelegate:tempDelegate]; //reset the selection since removing it changed the selection [aTabView selectTabViewItem:tabViewItem]; [self update]; } else { [_controller setSelectedCell:thisCell]; [self setNeedsDisplay:YES]; } } if ([[self delegate] respondsToSelector:@selector(tabView:didSelectTabViewItem:)]) { [[self delegate] performSelector:@selector(tabView:didSelectTabViewItem:) withObject:aTabView withObject:tabViewItem]; } } - (BOOL)tabView:(NSTabView *)aTabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem { if ([[self delegate] respondsToSelector:@selector(tabView:shouldSelectTabViewItem:)]) { return [[self delegate] tabView:aTabView shouldSelectTabViewItem:tabViewItem]; } else { return YES; } } - (void)tabView:(NSTabView *)aTabView willSelectTabViewItem:(NSTabViewItem *)tabViewItem { if ([[self delegate] respondsToSelector:@selector(tabView:willSelectTabViewItem:)]) { [[self delegate] performSelector:@selector(tabView:willSelectTabViewItem:) withObject:aTabView withObject:tabViewItem]; } } - (void)tabViewDidChangeNumberOfTabViewItems:(NSTabView *)aTabView { NSArray *tabItems = [tabView tabViewItems]; // go through cells, remove any whose representedObjects are not in [tabView tabViewItems] NSEnumerator *e = [[[_cells copy] autorelease] objectEnumerator]; PSMTabBarCell *cell; while ( (cell = [e nextObject]) ) { //remove the observer binding if ([cell representedObject] && ![tabItems containsObject:[cell representedObject]]) { if ([[self delegate] respondsToSelector:@selector(tabView:didCloseTabViewItem:)]) { [[self delegate] tabView:aTabView didCloseTabViewItem:[cell representedObject]]; } [self removeTabForCell:cell]; } } // go through tab view items, add cell for any not present NSMutableArray *cellItems = [self representedTabViewItems]; NSEnumerator *ex = [tabItems objectEnumerator]; NSTabViewItem *item; while ( (item = [ex nextObject]) ) { if (![cellItems containsObject:item]) { [self addTabViewItem:item]; } } // pass along for other delegate responses if ([[self delegate] respondsToSelector:@selector(tabViewDidChangeNumberOfTabViewItems:)]) { [[self delegate] performSelector:@selector(tabViewDidChangeNumberOfTabViewItems:) withObject:aTabView]; } // reset cursor tracking for the add tab button if one exists if ([self addTabButton]) [[self addTabButton] resetCursorRects]; } #pragma mark - #pragma mark Tooltips - (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(NSPoint)point userData:(void *)userData { if ([[self delegate] respondsToSelector:@selector(tabView:toolTipForTabViewItem:)]) { return [[self delegate] tabView:[self tabView] toolTipForTabViewItem:[[self cellForPoint:point cellFrame:nil] representedObject]]; } return nil; } #pragma mark - #pragma mark Archiving - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; if ([aCoder allowsKeyedCoding]) { [aCoder encodeObject:_cells forKey:@"PSMcells"]; [aCoder encodeObject:tabView forKey:@"PSMtabView"]; [aCoder encodeObject:_overflowPopUpButton forKey:@"PSMoverflowPopUpButton"]; [aCoder encodeObject:_addTabButton forKey:@"PSMaddTabButton"]; [aCoder encodeObject:style forKey:@"PSMstyle"]; [aCoder encodeInteger:_orientation forKey:@"PSMorientation"]; [aCoder encodeBool:_canCloseOnlyTab forKey:@"PSMcanCloseOnlyTab"]; [aCoder encodeBool:_disableTabClose forKey:@"PSMdisableTabClose"]; [aCoder encodeBool:_hideForSingleTab forKey:@"PSMhideForSingleTab"]; [aCoder encodeBool:_allowsBackgroundTabClosing forKey:@"PSMallowsBackgroundTabClosing"]; [aCoder encodeBool:_allowsResizing forKey:@"PSMallowsResizing"]; [aCoder encodeBool:_selectsTabsOnMouseDown forKey:@"PSMselectsTabsOnMouseDown"]; [aCoder encodeBool:_showAddTabButton forKey:@"PSMshowAddTabButton"]; [aCoder encodeBool:_sizeCellsToFit forKey:@"PSMsizeCellsToFit"]; [aCoder encodeInteger:_cellMinWidth forKey:@"PSMcellMinWidth"]; [aCoder encodeInteger:_cellMaxWidth forKey:@"PSMcellMaxWidth"]; [aCoder encodeInteger:_cellOptimumWidth forKey:@"PSMcellOptimumWidth"]; [aCoder encodeInteger:_currentStep forKey:@"PSMcurrentStep"]; [aCoder encodeBool:_isHidden forKey:@"PSMisHidden"]; [aCoder encodeObject:partnerView forKey:@"PSMpartnerView"]; [aCoder encodeBool:_awakenedFromNib forKey:@"PSMawakenedFromNib"]; [aCoder encodeObject:_lastMouseDownEvent forKey:@"PSMlastMouseDownEvent"]; [aCoder encodeObject:delegate forKey:@"PSMdelegate"]; [aCoder encodeBool:_useOverflowMenu forKey:@"PSMuseOverflowMenu"]; [aCoder encodeBool:_automaticallyAnimates forKey:@"PSMautomaticallyAnimates"]; [aCoder encodeBool:_alwaysShowActiveTab forKey:@"PSMalwaysShowActiveTab"]; } } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { // Initialization [self initAddedProperties]; [self registerForDraggedTypes:[NSArray arrayWithObjects:@"PSMTabBarControlItemPBType", nil]]; if ([aDecoder allowsKeyedCoding]) { _cells = [[aDecoder decodeObjectForKey:@"PSMcells"] retain]; tabView = [[aDecoder decodeObjectForKey:@"PSMtabView"] retain]; _overflowPopUpButton = [[aDecoder decodeObjectForKey:@"PSMoverflowPopUpButton"] retain]; _addTabButton = [[aDecoder decodeObjectForKey:@"PSMaddTabButton"] retain]; style = [[aDecoder decodeObjectForKey:@"PSMstyle"] retain]; _orientation = (PSMTabBarOrientation)[aDecoder decodeIntegerForKey:@"PSMorientation"]; _canCloseOnlyTab = [aDecoder decodeBoolForKey:@"PSMcanCloseOnlyTab"]; _disableTabClose = [aDecoder decodeBoolForKey:@"PSMdisableTabClose"]; _hideForSingleTab = [aDecoder decodeBoolForKey:@"PSMhideForSingleTab"]; _allowsBackgroundTabClosing = [aDecoder decodeBoolForKey:@"PSMallowsBackgroundTabClosing"]; _allowsResizing = [aDecoder decodeBoolForKey:@"PSMallowsResizing"]; _selectsTabsOnMouseDown = [aDecoder decodeBoolForKey:@"PSMselectsTabsOnMouseDown"]; _showAddTabButton = [aDecoder decodeBoolForKey:@"PSMshowAddTabButton"]; _sizeCellsToFit = [aDecoder decodeBoolForKey:@"PSMsizeCellsToFit"]; _cellMinWidth = [aDecoder decodeIntegerForKey:@"PSMcellMinWidth"]; _cellMaxWidth = [aDecoder decodeIntegerForKey:@"PSMcellMaxWidth"]; _cellOptimumWidth = [aDecoder decodeIntegerForKey:@"PSMcellOptimumWidth"]; _currentStep = [aDecoder decodeIntegerForKey:@"PSMcurrentStep"]; _isHidden = [aDecoder decodeBoolForKey:@"PSMisHidden"]; partnerView = [[aDecoder decodeObjectForKey:@"PSMpartnerView"] retain]; _awakenedFromNib = [aDecoder decodeBoolForKey:@"PSMawakenedFromNib"]; _lastMouseDownEvent = [[aDecoder decodeObjectForKey:@"PSMlastMouseDownEvent"] retain]; _useOverflowMenu = [aDecoder decodeBoolForKey:@"PSMuseOverflowMenu"]; _automaticallyAnimates = [aDecoder decodeBoolForKey:@"PSMautomaticallyAnimates"]; _alwaysShowActiveTab = [aDecoder decodeBoolForKey:@"PSMalwaysShowActiveTab"]; delegate = [[aDecoder decodeObjectForKey:@"PSMdelegate"] retain]; } // resize [self setPostsFrameChangedNotifications:YES]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameDidChange:) name:NSViewFrameDidChangeNotification object:self]; } [self setTarget:self]; return self; } #pragma mark - #pragma mark IB Palette - (NSSize)minimumFrameSizeFromKnobPosition:(NSInteger)position { return NSMakeSize(100.0f, 22.0f); } - (NSSize)maximumFrameSizeFromKnobPosition:(NSInteger)knobPosition { return NSMakeSize(10000.0f, 22.0f); } - (void)placeView:(NSRect)newFrame { // this is called any time the view is resized in IB [self setFrame:newFrame]; [self update:NO]; } #pragma mark - #pragma mark Convenience - (void)bindPropertiesForCell:(PSMTabBarCell *)cell andTabViewItem:(NSTabViewItem *)item { [self _bindPropertiesForCell:cell andTabViewItem:item]; // watch for changes in the identifier [item addObserver:self forKeyPath:@"identifier" options:0 context:nil]; } - (void)_bindPropertiesForCell:(PSMTabBarCell *)cell andTabViewItem:(NSTabViewItem *)item { // bind the indicator to the represented object's status (if it exists) [[cell indicator] setHidden:YES]; if ([item identifier] != nil) { if ([[[cell representedObject] identifier] respondsToSelector:@selector(isProcessing)]) { NSMutableDictionary *bindingOptions = [NSMutableDictionary dictionary]; [bindingOptions setObject:NSNegateBooleanTransformerName forKey:@"NSValueTransformerName"]; [[cell indicator] bind:@"animate" toObject:[item identifier] withKeyPath:@"isProcessing" options:nil]; [[cell indicator] bind:@"hidden" toObject:[item identifier] withKeyPath:@"isProcessing" options:bindingOptions]; [[item identifier] addObserver:cell forKeyPath:@"isProcessing" options:0 context:nil]; } } // bind for the existence of an icon [cell setHasIcon:NO]; if ([item identifier] != nil) { if ([[[cell representedObject] identifier] respondsToSelector:@selector(icon)]) { NSMutableDictionary *bindingOptions = [NSMutableDictionary dictionary]; [bindingOptions setObject:NSIsNotNilTransformerName forKey:@"NSValueTransformerName"]; [cell bind:@"hasIcon" toObject:[item identifier] withKeyPath:@"icon" options:bindingOptions]; [[item identifier] addObserver:cell forKeyPath:@"icon" options:0 context:nil]; } } // bind for the existence of a counter [cell setCount:0]; if ([item identifier] != nil) { if ([[[cell representedObject] identifier] respondsToSelector:@selector(count)]) { [cell bind:@"count" toObject:[item identifier] withKeyPath:@"objectCount" options:nil]; [[item identifier] addObserver:cell forKeyPath:@"objectCount" options:0 context:nil]; } } // bind for the color of a counter [cell setCountColor:nil]; if ([item identifier] != nil) { if ([[[cell representedObject] identifier] respondsToSelector:@selector(countColor)]) { [cell bind:@"countColor" toObject:[item identifier] withKeyPath:@"countColor" options:nil]; [[item identifier] addObserver:cell forKeyPath:@"countColor" options:0 context:nil]; } } // bind for a large image [cell setHasLargeImage:NO]; if ([item identifier] != nil) { if ([[[cell representedObject] identifier] respondsToSelector:@selector(largeImage)]) { NSMutableDictionary *bindingOptions = [NSMutableDictionary dictionary]; [bindingOptions setObject:NSIsNotNilTransformerName forKey:@"NSValueTransformerName"]; [cell bind:@"hasLargeImage" toObject:[item identifier] withKeyPath:@"largeImage" options:bindingOptions]; [[item identifier] addObserver:cell forKeyPath:@"largeImage" options:0 context:nil]; } } [cell setIsEdited:NO]; if ([item identifier] != nil) { if ([[[cell representedObject] identifier] respondsToSelector:@selector(isEdited)]) { [cell bind:@"isEdited" toObject:[item identifier] withKeyPath:@"isEdited" options:nil]; [[item identifier] addObserver:cell forKeyPath:@"isEdited" options:0 context:nil]; } } // bind my string value to the label on the represented tab [cell bind:@"title" toObject:item withKeyPath:@"label" options:nil]; [cell bind:@"backgroundColor" toObject:item withKeyPath:@"color" options:nil]; } - (NSMutableArray *)representedTabViewItems { NSMutableArray *temp = [NSMutableArray arrayWithCapacity:[_cells count]]; NSEnumerator *e = [_cells objectEnumerator]; PSMTabBarCell *cell; while ( (cell = [e nextObject])) { if ([cell representedObject]) { [temp addObject:[cell representedObject]]; } } return temp; } - (id)cellForPoint:(NSPoint)point cellFrame:(NSRectPointer)outFrame { if ([self orientation] == PSMTabBarHorizontalOrientation && !NSPointInRect(point, [self genericCellRect])) { return nil; } NSInteger i, cnt = [_cells count]; for (i = 0; i < cnt; i++) { PSMTabBarCell *cell = [_cells objectAtIndex:i]; if (NSPointInRect(point, [cell frame])) { if (outFrame) { *outFrame = [cell frame]; } return cell; } } return nil; } - (PSMTabBarCell *)lastVisibleTab { NSInteger i, cellCount = [_cells count]; for (i = 0; i < cellCount; i++) { if ([[_cells objectAtIndex:i] isInOverflowMenu]) { return [_cells objectAtIndex:(i - 1)]; } } return [_cells objectAtIndex:(cellCount - 1)]; } - (NSUInteger)numberOfVisibleTabs { NSUInteger i, cellCount = 0; PSMTabBarCell *nextCell; for (i = 0; i < [_cells count]; i++) { nextCell = [_cells objectAtIndex:i]; if ([nextCell isInOverflowMenu]) { break; } if (![nextCell isPlaceholder]) { cellCount++; } } return cellCount; } #pragma mark - #pragma mark Accessibility -(BOOL)accessibilityIsIgnored { return NO; } - (id)accessibilityAttributeValue:(NSString *)attribute { id attributeValue = nil; if ([attribute isEqualToString: NSAccessibilityRoleAttribute]) { attributeValue = NSAccessibilityGroupRole; } else if ([attribute isEqualToString: NSAccessibilityChildrenAttribute]) { attributeValue = NSAccessibilityUnignoredChildren(_cells); } else { attributeValue = [super accessibilityAttributeValue:attribute]; } return attributeValue; } - (id)accessibilityHitTest:(NSPoint)point { id hitTestResult = self; NSEnumerator *enumerator = [_cells objectEnumerator]; PSMTabBarCell *cell = nil; PSMTabBarCell *highlightedCell = nil; while (!highlightedCell && (cell = [enumerator nextObject])) { if ([cell isHighlighted]) { highlightedCell = cell; } } if (highlightedCell) { hitTestResult = [highlightedCell accessibilityHitTest:point]; } return hitTestResult; } @end