5 // Created by John Pannell on 10/13/05.
6 // Copyright 2005 Positive Spin Media. All rights reserved.
9 #import "PSMTabBarControl.h"
10 #import "PSMTabBarCell.h"
11 #import "PSMOverflowPopUpButton.h"
12 #import "PSMRolloverButton.h"
13 #import "PSMTabStyle.h"
14 #import "PSMMetalTabStyle.h"
15 #import "PSMAquaTabStyle.h"
16 #import "PSMUnifiedTabStyle.h"
17 #import "PSMTabDragAssistant.h"
19 @interface PSMTabBarControl (Private)
21 - (float)availableCellWidth;
22 - (NSRect)genericCellRect;
24 // constructor/destructor
25 - (void)initAddedProperties;
29 - (NSEvent *)lastMouseDownEvent;
30 - (void)setLastMouseDownEvent:(NSEvent *)event;
33 - (void)addTabViewItem:(NSTabViewItem *)item;
34 - (void)removeTabForCell:(PSMTabBarCell *)cell;
40 - (void)overflowMenuAction:(id)sender;
41 - (void)closeTabClick:(id)sender;
42 - (void)tabClick:(id)sender;
43 - (void)tabNothing:(id)sender;
44 - (void)frameDidChange:(NSNotification *)notification;
45 - (void)windowDidMove:(NSNotification *)aNotification;
46 - (void)windowStatusDidChange:(NSNotification *)notification;
49 - (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem;
50 - (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem;
51 - (void)tabView:(NSTabView *)tabView willSelectTabViewItem:(NSTabViewItem *)tabViewItem;
52 - (void)tabViewDidChangeNumberOfTabViewItems:(NSTabView *)tabView;
55 - (void)encodeWithCoder:(NSCoder *)aCoder;
56 - (id)initWithCoder:(NSCoder *)aDecoder;
59 - (id)cellForPoint:(NSPoint)point cellFrame:(NSRectPointer)outFrame;
60 - (PSMTabBarCell *)lastVisibleTab;
61 - (int)numberOfVisibleTabs;
65 @implementation PSMTabBarControl
67 #pragma mark Characteristics
70 static NSBundle *bundle = nil;
71 if (!bundle) bundle = [NSBundle bundleForClass:[PSMTabBarControl class]];
75 - (float)availableCellWidth
77 float width = [self frame].size.width;
78 width = width - [style leftMarginForTabBarControl] - [style rightMarginForTabBarControl];
82 - (NSRect)genericCellRect
84 NSRect aRect=[self frame];
85 aRect.origin.x = [style leftMarginForTabBarControl];
87 aRect.size.width = [self availableCellWidth];
88 aRect.size.height = kPSMTabBarControlHeight;
93 #pragma mark Constructor/destructor
95 - (void)initAddedProperties
97 _cells = [[NSMutableArray alloc] initWithCapacity:10];
100 _allowsDragBetweenWindows = YES;
101 _canCloseOnlyTab = NO;
102 _showAddTabButton = NO;
103 _hideForSingleTab = NO;
104 _sizeCellsToFit = NO;
106 _hideIndicators = NO;
107 _awakenedFromNib = NO;
110 _cellOptimumWidth = 130;
111 style = [[PSMMetalTabStyle alloc] init];
113 // the overflow button/menu
114 NSRect overflowButtonRect = NSMakeRect([self frame].size.width - [style rightMarginForTabBarControl] + 1, 0, [style rightMarginForTabBarControl] - 1, [self frame].size.height);
115 _overflowPopUpButton = [[PSMOverflowPopUpButton alloc] initWithFrame:overflowButtonRect pullsDown:YES];
116 if(_overflowPopUpButton){
118 [_overflowPopUpButton setAutoresizingMask:NSViewNotSizable|NSViewMinXMargin];
122 NSRect addTabButtonRect = NSMakeRect([self frame].size.width - [style rightMarginForTabBarControl] + 1, 3.0, 16.0, 16.0);
123 _addTabButton = [[PSMRolloverButton alloc] initWithFrame:addTabButtonRect];
125 NSImage *newButtonImage = [style addTabButtonImage];
127 [_addTabButton setUsualImage:newButtonImage];
128 newButtonImage = [style addTabButtonPressedImage];
130 [_addTabButton setAlternateImage:newButtonImage];
131 newButtonImage = [style addTabButtonRolloverImage];
133 [_addTabButton setRolloverImage:newButtonImage];
134 [_addTabButton setTitle:@""];
135 [_addTabButton setImagePosition:NSImageOnly];
136 [_addTabButton setButtonType:NSMomentaryChangeButton];
137 [_addTabButton setBordered:NO];
138 [_addTabButton setBezelStyle:NSShadowlessSquareBezelStyle];
139 if(_showAddTabButton){
140 [_addTabButton setHidden:NO];
142 [_addTabButton setHidden:YES];
144 [_addTabButton setNeedsDisplay:YES];
148 - (id)initWithFrame:(NSRect)frame
150 self = [super initWithFrame:frame];
153 [self initAddedProperties];
154 [self registerForDraggedTypes:[NSArray arrayWithObjects: @"PSMTabBarControlItemPBType", nil]];
156 [self setTarget:self];
162 [_overflowPopUpButton release];
165 [_addTabButton release];
166 [partnerView release];
167 [_lastMouseDownEvent release];
171 [self unregisterDraggedTypes];
178 // build cells from existing tab view items
179 NSArray *existingItems = [tabView tabViewItems];
180 NSEnumerator *e = [existingItems objectEnumerator];
182 while(item = [e nextObject]){
183 if(![[self representedTabViewItems] containsObject:item])
184 [self addTabViewItem:item];
188 [self setPostsFrameChangedNotifications:YES];
189 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameDidChange:) name:NSViewFrameDidChangeNotification object:self];
192 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowStatusDidChange:) name:NSWindowDidBecomeKeyNotification object:[self window]];
193 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowStatusDidChange:) name:NSWindowDidResignKeyNotification object:[self window]];
194 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidMove:) name:NSWindowDidMoveNotification object:[self window]];
199 #pragma mark Accessors
201 - (NSMutableArray *)cells
206 - (NSEvent *)lastMouseDownEvent
208 return _lastMouseDownEvent;
211 - (void)setLastMouseDownEvent:(NSEvent *)event
214 [_lastMouseDownEvent release];
215 _lastMouseDownEvent = event;
223 - (void)setDelegate:(id)object
230 - (NSTabView *)tabView
235 - (void)setTabView:(NSTabView *)view
242 - (id<PSMTabStyle>)style
247 - (NSString *)styleName
252 - (void)setStyleNamed:(NSString *)name
255 if([name isEqualToString:@"Aqua"]){
256 style = [[PSMAquaTabStyle alloc] init];
258 else if ([name isEqualToString:@"Unified"]){
259 style = [[PSMUnifiedTabStyle alloc] init];
262 style = [[PSMMetalTabStyle alloc] init];
265 // restyle add tab button
267 NSImage *newButtonImage = [style addTabButtonImage];
269 [_addTabButton setUsualImage:newButtonImage];
270 newButtonImage = [style addTabButtonPressedImage];
272 [_addTabButton setAlternateImage:newButtonImage];
273 newButtonImage = [style addTabButtonRolloverImage];
275 [_addTabButton setRolloverImage:newButtonImage];
281 - (BOOL)canCloseOnlyTab
283 return _canCloseOnlyTab;
286 - (void)setCanCloseOnlyTab:(BOOL)value
288 _canCloseOnlyTab = value;
289 if ([_cells count] == 1) {
294 - (BOOL)allowsDragBetweenWindows
296 return _allowsDragBetweenWindows;
299 - (void)setAllowsDragBetweenWindows:(BOOL)flag
301 _allowsDragBetweenWindows = flag;
304 - (BOOL)hideForSingleTab
306 return _hideForSingleTab;
309 - (void)setHideForSingleTab:(BOOL)value
311 _hideForSingleTab = value;
315 - (BOOL)showAddTabButton
317 return _showAddTabButton;
320 - (void)setShowAddTabButton:(BOOL)value
322 _showAddTabButton = value;
328 return _cellMinWidth;
331 - (void)setCellMinWidth:(int)value
333 _cellMinWidth = value;
339 return _cellMaxWidth;
342 - (void)setCellMaxWidth:(int)value
344 _cellMaxWidth = value;
348 - (int)cellOptimumWidth
350 return _cellOptimumWidth;
353 - (void)setCellOptimumWidth:(int)value
355 _cellOptimumWidth = value;
359 - (BOOL)sizeCellsToFit
361 return _sizeCellsToFit;
364 - (void)setSizeCellsToFit:(BOOL)value
366 _sizeCellsToFit = value;
370 - (PSMRolloverButton *)addTabButton
372 return _addTabButton;
375 - (PSMOverflowPopUpButton *)overflowPopUpButton
377 return _overflowPopUpButton;
381 #pragma mark Functionality
382 - (void)addTabViewItem:(NSTabViewItem *)item
385 PSMTabBarCell *cell = [[PSMTabBarCell alloc] initWithControlView:self];
386 [cell setRepresentedObject:item];
387 // bind the indicator to the represented object's status (if it exists)
388 [[cell indicator] setHidden:YES];
389 if([item identifier] != nil){
390 if([[item identifier] respondsToSelector:@selector(content)]){
391 if([[[[cell representedObject] identifier] content] respondsToSelector:@selector(isProcessing)]){
392 NSMutableDictionary *bindingOptions = [NSMutableDictionary dictionary];
393 [bindingOptions setObject:NSNegateBooleanTransformerName forKey:@"NSValueTransformerName"];
394 [[cell indicator] bind:@"animate" toObject:[item identifier] withKeyPath:@"selection.isProcessing" options:nil];
395 [[cell indicator] bind:@"hidden" toObject:[item identifier] withKeyPath:@"selection.isProcessing" options:bindingOptions];
396 [[item identifier] addObserver:self forKeyPath:@"selection.isProcessing" options:nil context:nil];
401 // bind for the existence of an icon
402 [cell setHasIcon:NO];
403 if([item identifier] != nil){
404 if([[item identifier] respondsToSelector:@selector(content)]){
405 if([[[[cell representedObject] identifier] content] respondsToSelector:@selector(icon)]){
406 NSMutableDictionary *bindingOptions = [NSMutableDictionary dictionary];
407 [bindingOptions setObject:NSIsNotNilTransformerName forKey:@"NSValueTransformerName"];
408 [cell bind:@"hasIcon" toObject:[item identifier] withKeyPath:@"selection.icon" options:bindingOptions];
409 [[item identifier] addObserver:self forKeyPath:@"selection.icon" options:nil context:nil];
414 // bind for the existence of a counter
416 if([item identifier] != nil){
417 if([[item identifier] respondsToSelector:@selector(content)]){
418 if([[[[cell representedObject] identifier] content] respondsToSelector:@selector(objectCount)]){
419 [cell bind:@"count" toObject:[item identifier] withKeyPath:@"selection.objectCount" options:nil];
420 [[item identifier] addObserver:self forKeyPath:@"selection.objectCount" options:nil context:nil];
425 // bind my string value to the label on the represented tab
426 [cell bind:@"title" toObject:item withKeyPath:@"label" options:nil];
429 [_cells addObject:cell];
431 if([_cells count] == [tabView numberOfTabViewItems]){
432 [self update]; // don't update unless all are accounted for!
436 - (void)removeTabForCell:(PSMTabBarCell *)cell
439 [[cell indicator] unbind:@"animate"];
440 [[cell indicator] unbind:@"hidden"];
441 [cell unbind:@"hasIcon"];
442 [cell unbind:@"title"];
443 [cell unbind:@"count"];
446 if([[self subviews] containsObject:[cell indicator]]){
447 [[cell indicator] removeFromSuperview];
450 [[NSNotificationCenter defaultCenter] removeObserver:cell];
451 if([cell closeButtonTrackingTag] != 0){
452 [self removeTrackingRect:[cell closeButtonTrackingTag]];
454 if([cell cellTrackingTag] != 0){
455 [self removeTrackingRect:[cell cellTrackingTag]];
458 // pull from collection
459 [_cells removeObject:cell];
464 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
466 // the progress indicator, label, icon, or count has changed - must redraw
471 #pragma mark Hide/Show
473 - (void)hideTabBar:(BOOL)hide animate:(BOOL)animate
475 if(!_awakenedFromNib)
477 if(_isHidden && hide)
479 if(!_isHidden && !hide)
482 [[self subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
483 _hideIndicators = YES;
485 NSTimer *animationTimer;
489 _currentStep = (int)kPSMHideAnimationSteps;
491 float partnerOriginalHeight, partnerOriginalY, myOriginalHeight, myOriginalY, partnerTargetHeight, partnerTargetY, myTargetHeight, myTargetY;
493 // current (original) values
494 myOriginalHeight = [self frame].size.height;
495 myOriginalY = [self frame].origin.y;
497 partnerOriginalHeight = [partnerView frame].size.height;
498 partnerOriginalY = [partnerView frame].origin.y;
500 partnerOriginalHeight = [[self window] frame].size.height;
501 partnerOriginalY = [[self window] frame].origin.y;
504 // target values for partner
506 // above or below me?
507 if((myOriginalY - 22) > partnerOriginalY){
508 // partner is below me
511 myTargetY = myOriginalY + 21;
512 myTargetHeight = myOriginalHeight - 21;
513 partnerTargetY = partnerOriginalY;
514 partnerTargetHeight = partnerOriginalHeight + 21;
517 myTargetY = myOriginalY - 21;
518 myTargetHeight = myOriginalHeight + 21;
519 partnerTargetY = partnerOriginalY;
520 partnerTargetHeight = partnerOriginalHeight - 21;
523 // partner is above me
526 myTargetY = myOriginalY;
527 myTargetHeight = myOriginalHeight - 21;
528 partnerTargetY = partnerOriginalY - 21;
529 partnerTargetHeight = partnerOriginalHeight + 21;
532 myTargetY = myOriginalY;
533 myTargetHeight = myOriginalHeight + 21;
534 partnerTargetY = partnerOriginalY + 21;
535 partnerTargetHeight = partnerOriginalHeight - 21;
539 // for window movement
542 myTargetY = myOriginalY;
543 myTargetHeight = myOriginalHeight - 21;
544 partnerTargetY = partnerOriginalY + 21;
545 partnerTargetHeight = partnerOriginalHeight - 21;
548 myTargetY = myOriginalY;
549 myTargetHeight = myOriginalHeight + 21;
550 partnerTargetY = partnerOriginalY - 21;
551 partnerTargetHeight = partnerOriginalHeight + 21;
555 NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:myOriginalY], @"myOriginalY", [NSNumber numberWithFloat:partnerOriginalY], @"partnerOriginalY", [NSNumber numberWithFloat:myOriginalHeight], @"myOriginalHeight", [NSNumber numberWithFloat:partnerOriginalHeight], @"partnerOriginalHeight", [NSNumber numberWithFloat:myTargetY], @"myTargetY", [NSNumber numberWithFloat:partnerTargetY], @"partnerTargetY", [NSNumber numberWithFloat:myTargetHeight], @"myTargetHeight", [NSNumber numberWithFloat:partnerTargetHeight], @"partnerTargetHeight", nil];
556 animationTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0/20.0) target:self selector:@selector(animateShowHide:) userInfo:userInfo repeats:YES];
559 - (void)animateShowHide:(NSTimer *)timer
561 // moves the frame of the tab bar and window (or partner view) linearly to hide or show the tab bar
562 NSRect myFrame = [self frame];
563 float myCurrentY = ([[[timer userInfo] objectForKey:@"myOriginalY"] floatValue] + (([[[timer userInfo] objectForKey:@"myTargetY"] floatValue] - [[[timer userInfo] objectForKey:@"myOriginalY"] floatValue]) * (_currentStep/kPSMHideAnimationSteps)));
564 float myCurrentHeight = ([[[timer userInfo] objectForKey:@"myOriginalHeight"] floatValue] + (([[[timer userInfo] objectForKey:@"myTargetHeight"] floatValue] - [[[timer userInfo] objectForKey:@"myOriginalHeight"] floatValue]) * (_currentStep/kPSMHideAnimationSteps)));
565 float partnerCurrentY = ([[[timer userInfo] objectForKey:@"partnerOriginalY"] floatValue] + (([[[timer userInfo] objectForKey:@"partnerTargetY"] floatValue] - [[[timer userInfo] objectForKey:@"partnerOriginalY"] floatValue]) * (_currentStep/kPSMHideAnimationSteps)));
566 float partnerCurrentHeight = ([[[timer userInfo] objectForKey:@"partnerOriginalHeight"] floatValue] + (([[[timer userInfo] objectForKey:@"partnerTargetHeight"] floatValue] - [[[timer userInfo] objectForKey:@"partnerOriginalHeight"] floatValue]) * (_currentStep/kPSMHideAnimationSteps)));
568 NSRect myNewFrame = NSMakeRect(myFrame.origin.x, myCurrentY, myFrame.size.width, myCurrentHeight);
571 // resize self and view
572 [partnerView setFrame:NSMakeRect([partnerView frame].origin.x, partnerCurrentY, [partnerView frame].size.width, partnerCurrentHeight)];
573 [partnerView setNeedsDisplay:YES];
574 [self setFrame:myNewFrame];
576 // resize self and window
577 [[self window] setFrame:NSMakeRect([[self window] frame].origin.x, partnerCurrentY, [[self window] frame].size.width, partnerCurrentHeight) display:YES];
578 [self setFrame:myNewFrame];
583 if(_currentStep == kPSMHideAnimationSteps + 1){
585 [self viewDidEndLiveResize];
586 _hideIndicators = NO;
589 [[self window] display];
597 - (void)setPartnerView:(id)view
599 [partnerView release];
612 - (void)drawRect:(NSRect)rect
614 [style drawTabBar:self inRect:rect];
619 // abandon hope, all ye who enter here :-)
620 // this method handles all of the cell layout, and is called when something changes to require the refresh. This method is not called during drag and drop; see the PSMTabDragAssistant's calculateDragAnimationForTabBar: method, which does layout in that case.
622 // make sure all of our tabs are accounted for before updating
623 if ([tabView numberOfTabViewItems] != [_cells count]) {
627 // hide/show? (these return if already in desired state)
628 if((_hideForSingleTab) && ([_cells count] <= 1)){
629 [self hideTabBar:YES animate:YES];
631 [self hideTabBar:NO animate:YES];
634 // size all cells appropriately and create tracking rects
635 // nuke old tracking rects
636 int i, cellCount = [_cells count];
637 for(i = 0; i < cellCount; i++){
638 id cell = [_cells objectAtIndex:i];
639 [[NSNotificationCenter defaultCenter] removeObserver:cell];
640 if([cell closeButtonTrackingTag] != 0){
641 [self removeTrackingRect:[cell closeButtonTrackingTag]];
643 if([cell cellTrackingTag] != 0){
644 [self removeTrackingRect:[cell cellTrackingTag]];
648 // calculate number of cells to fit in control and cell widths
649 float availableWidth = [self availableCellWidth];
650 NSMutableArray *newWidths = [NSMutableArray arrayWithCapacity:cellCount];
651 int numberOfVisibleCells = 1;
652 float totalOccupiedWidth = 0.0;
653 NSMenu *overflowMenu = nil;
654 for(i = 0; i < cellCount; i++){
655 PSMTabBarCell *cell = [_cells objectAtIndex:i];
658 // supress close button?
659 if (cellCount == 1 && [self canCloseOnlyTab] == NO) {
660 [cell setCloseButtonSuppressed:YES];
662 [cell setCloseButtonSuppressed:NO];
665 // Determine cell width
667 width = [cell desiredWidthOfCell];
668 if (width > _cellMaxWidth) {
669 width = _cellMaxWidth;
672 width = _cellOptimumWidth;
676 totalOccupiedWidth += width;
677 if (totalOccupiedWidth >= availableWidth) {
678 numberOfVisibleCells = i;
680 int neededWidth = width - (totalOccupiedWidth - availableWidth);
681 // can I squeeze it in without violating min cell width?
682 int widthIfAllMin = (numberOfVisibleCells + 1) * _cellMinWidth;
684 if ((width + widthIfAllMin) <= availableWidth) {
685 // squeeze - distribute needed sacrifice among all cells
687 for(q = (i - 1); q >= 0; q--){
688 int desiredReduction = (int)neededWidth/(q+1);
689 if(([[newWidths objectAtIndex:q] floatValue] - desiredReduction) < _cellMinWidth){
690 int actualReduction = (int)[[newWidths objectAtIndex:q] floatValue] - _cellMinWidth;
691 [newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithFloat:_cellMinWidth]];
692 neededWidth -= actualReduction;
694 int newCellWidth = (int)[[newWidths objectAtIndex:q] floatValue] - desiredReduction;
695 [newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithFloat:newCellWidth]];
696 neededWidth -= desiredReduction;
700 int thisWidth = width - neededWidth;
701 [newWidths addObject:[NSNumber numberWithFloat:thisWidth]];
702 numberOfVisibleCells++;
704 // stretch - distribute leftover room among cells
705 int leftoverWidth = availableWidth - totalOccupiedWidth + width;
707 for(q = (i - 1); q >= 0; q--){
708 int desiredAddition = (int)leftoverWidth/(q+1);
709 int newCellWidth = (int)[[newWidths objectAtIndex:q] floatValue] + desiredAddition;
710 [newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithFloat:newCellWidth]];
711 leftoverWidth -= desiredAddition;
714 break; // done assigning widths; remaining cells go in overflow menu
716 int revisedWidth = availableWidth/(i + 1);
717 if(revisedWidth >= _cellMinWidth){
719 totalOccupiedWidth = 0;
720 for(q = 0; q < [newWidths count]; q++){
721 [newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithFloat:revisedWidth]];
722 totalOccupiedWidth += revisedWidth;
724 // just squeezed this one in...
725 [newWidths addObject:[NSNumber numberWithFloat:revisedWidth]];
726 totalOccupiedWidth += revisedWidth;
727 numberOfVisibleCells++;
729 // couldn't fit that last one...
734 numberOfVisibleCells = cellCount;
735 [newWidths addObject:[NSNumber numberWithFloat:width]];
739 // Set up cells with frames and rects
740 NSRect cellRect = [self genericCellRect];
741 for(i = 0; i < cellCount; i++){
742 PSMTabBarCell *cell = [_cells objectAtIndex:i];
744 if (i < numberOfVisibleCells) {
746 cellRect.size.width = [[newWidths objectAtIndex:i] floatValue];
747 [cell setFrame:cellRect];
748 NSTrackingRectTag tag;
750 // close button tracking rect
751 if ([cell hasCloseButton]) {
752 tag = [self addTrackingRect:[cell closeButtonRectForFrame:cellRect] owner:cell userData:nil assumeInside:NO];
753 [cell setCloseButtonTrackingTag:tag];
756 // entire tab tracking rect
757 tag = [self addTrackingRect:cellRect owner:cell userData:nil assumeInside:NO];
758 [cell setCellTrackingTag:tag];
759 [cell setEnabled:YES];
761 // selected? set tab states...
762 if([[cell representedObject] isEqualTo:[tabView selectedTabViewItem]]){
763 [cell setState:NSOnState];
764 tabState |= PSMTab_SelectedMask;
767 [[_cells objectAtIndex:i-1] setTabState:([(PSMTabBarCell *)[_cells objectAtIndex:i-1] tabState] | PSMTab_RightIsSelectedMask)];
769 // next cell - see below
771 [cell setState:NSOffState];
772 // see if prev cell was selected
774 if([[_cells objectAtIndex:i-1] state] == NSOnState){
775 tabState |= PSMTab_LeftIsSelectedMask;
781 tabState |= PSMTab_PositionLeftMask | PSMTab_PositionRightMask | PSMTab_PositionSingleMask;
783 tabState |= PSMTab_PositionLeftMask;
784 } else if(i-1 == cellCount){
785 tabState |= PSMTab_PositionRightMask;
787 [cell setTabState:tabState];
788 [cell setIsInOverflowMenu:NO];
791 if(![[cell indicator] isHidden] && !_hideIndicators){
792 [[cell indicator] setFrame:[cell indicatorRectForFrame:cellRect]];
793 if(![[self subviews] containsObject:[cell indicator]]){
794 [self addSubview:[cell indicator]];
795 [[cell indicator] startAnimation:self];
800 cellRect.origin.x += [[newWidths objectAtIndex:i] floatValue];
804 NSMenuItem *menuItem;
805 if(overflowMenu == nil){
806 overflowMenu = [[[NSMenu alloc] initWithTitle:@"TITLE"] autorelease];
807 [overflowMenu insertItemWithTitle:@"FIRST" action:nil keyEquivalent:@"" atIndex:0]; // Because the overflowPupUpButton is a pull down menu
809 menuItem = [[[NSMenuItem alloc] initWithTitle:[[cell attributedStringValue] string] action:@selector(overflowMenuAction:) keyEquivalent:@""] autorelease];
810 [menuItem setTarget:self];
811 [menuItem setRepresentedObject:[cell representedObject]];
812 [cell setIsInOverflowMenu:YES];
813 [[cell indicator] removeFromSuperview];
814 if ([[cell representedObject] isEqualTo:[tabView selectedTabViewItem]])
815 [menuItem setState:NSOnState];
817 [menuItem setImage:[[[[cell representedObject] identifier] content] icon]];
819 [menuItem setTitle:[[menuItem title] stringByAppendingFormat:@" (%d)",[cell count]]];
820 [overflowMenu addItem:menuItem];
826 cellRect.origin.y = 0;
827 cellRect.size.height = kPSMTabBarControlHeight;
828 cellRect.size.width = [style rightMarginForTabBarControl];
830 cellRect.origin.x = [self frame].size.width - [style rightMarginForTabBarControl] + 1;
831 if(![[self subviews] containsObject:_overflowPopUpButton]){
832 [self addSubview:_overflowPopUpButton];
834 [_overflowPopUpButton setFrame:cellRect];
835 [_overflowPopUpButton setMenu:overflowMenu];
836 if ([_overflowPopUpButton isHidden]) [_overflowPopUpButton setHidden:NO];
838 if (![_overflowPopUpButton isHidden]) [_overflowPopUpButton setHidden:YES];
842 if(!overflowMenu && _showAddTabButton){
843 if(![[self subviews] containsObject:_addTabButton])
844 [self addSubview:_addTabButton];
845 if([_addTabButton isHidden] && _showAddTabButton)
846 [_addTabButton setHidden:NO];
847 cellRect.size = [_addTabButton frame].size;
848 cellRect.origin.y = MARGIN_Y;
849 cellRect.origin.x += 2;
850 [_addTabButton setImage:[style addTabButtonImage]];
851 [_addTabButton setFrame:cellRect];
852 [_addTabButton setNeedsDisplay:YES];
854 [_addTabButton setHidden:YES];
855 [_addTabButton setNeedsDisplay:YES];
858 [self setNeedsDisplay:YES];
862 #pragma mark Mouse Tracking
864 - (BOOL)mouseDownCanMoveWindow
869 - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
874 - (void)mouseDown:(NSEvent *)theEvent
877 [self setLastMouseDownEvent:theEvent];
879 NSPoint mousePt = [self convertPoint:[theEvent locationInWindow] fromView:nil];
881 PSMTabBarCell *cell = [self cellForPoint:mousePt cellFrame:&cellFrame];
884 NSRect iconRect = [cell closeButtonRectForFrame:cellFrame];
885 if(NSMouseInRect(mousePt, iconRect,[self isFlipped])){
886 [cell setCloseButtonPressed:YES];
888 [cell setCloseButtonPressed:NO];
890 [self setNeedsDisplay:YES];
892 // HACK! Let the tabs react on the mouse down instead of mouse up
893 NSRect iconRect = [cell closeButtonRectForFrame:cellFrame];
894 if((NSMouseInRect(mousePt, iconRect,[self isFlipped]))){
895 //[self performSelector:@selector(closeTabClick:) withObject:cell];
896 [self closeTabClick:cell];
897 } else if(NSMouseInRect(mousePt, cellFrame,[self isFlipped])){
898 //[self performSelector:@selector(tabClick:) withObject:cell];
899 [self tabClick:cell];
901 //[self performSelector:@selector(tabNothing:) withObject:cell];
902 [self tabNothing:cell];
908 - (void)mouseDragged:(NSEvent *)theEvent
910 if([self lastMouseDownEvent] == nil){
914 if ([_cells count] < 2) {
919 NSPoint trackingStartPoint = [self convertPoint:[[self lastMouseDownEvent] locationInWindow] fromView:nil];
920 PSMTabBarCell *cell = [self cellForPoint:trackingStartPoint cellFrame:&cellFrame];
924 NSPoint currentPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
925 float dx = fabs(currentPoint.x - trackingStartPoint.x);
926 float dy = fabs(currentPoint.y - trackingStartPoint.y);
927 float distance = sqrt(dx * dx + dy * dy);
931 if(![[PSMTabDragAssistant sharedDragAssistant] isDragging]) {
932 [[PSMTabDragAssistant sharedDragAssistant] startDraggingCell:cell fromTabBar:self withMouseDownEvent:[self lastMouseDownEvent]];
936 - (void)mouseUp:(NSEvent *)theEvent
938 #if 0 // HACK! Tabs react on mouse down instead of mouse up
940 NSPoint mousePt = [self convertPoint:[theEvent locationInWindow] fromView:nil];
941 NSRect cellFrame, mouseDownCellFrame;
942 PSMTabBarCell *cell = [self cellForPoint:mousePt cellFrame:&cellFrame];
943 PSMTabBarCell *mouseDownCell = [self cellForPoint:[self convertPoint:[[self lastMouseDownEvent] locationInWindow] fromView:nil] cellFrame:&mouseDownCellFrame];
945 NSRect iconRect = [mouseDownCell closeButtonRectForFrame:mouseDownCellFrame];
946 if((NSMouseInRect(mousePt, iconRect,[self isFlipped])) && [mouseDownCell closeButtonPressed]){
947 [self performSelector:@selector(closeTabClick:) withObject:cell];
948 } else if(NSMouseInRect(mousePt, mouseDownCellFrame,[self isFlipped])){
949 [mouseDownCell setCloseButtonPressed:NO];
950 [self performSelector:@selector(tabClick:) withObject:cell];
952 [mouseDownCell setCloseButtonPressed:NO];
953 [self performSelector:@selector(tabNothing:) withObject:cell];
960 #pragma mark Drag and Drop
962 - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)theEvent
968 - (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)isLocal
970 return (isLocal ? NSDragOperationMove : NSDragOperationNone);
973 - (BOOL)ignoreModifierKeysWhileDragging
978 // NSDraggingDestination
979 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
981 if([[[sender draggingPasteboard] types] indexOfObject:@"PSMTabBarControlItemPBType"] != NSNotFound) {
983 if ([sender draggingSource] != self && ![self allowsDragBetweenWindows])
984 return NSDragOperationNone;
986 [[PSMTabDragAssistant sharedDragAssistant] draggingEnteredTabBar:self atPoint:[self convertPoint:[sender draggingLocation] fromView:nil]];
987 return NSDragOperationMove;
990 return NSDragOperationNone;
993 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
995 if ([[[sender draggingPasteboard] types] indexOfObject:@"PSMTabBarControlItemPBType"] != NSNotFound) {
997 if ([sender draggingSource] != self && ![self allowsDragBetweenWindows])
998 return NSDragOperationNone;
1000 [[PSMTabDragAssistant sharedDragAssistant] draggingUpdatedInTabBar:self atPoint:[self convertPoint:[sender draggingLocation] fromView:nil]];
1001 return NSDragOperationMove;
1004 return NSDragOperationNone;
1007 - (void)draggingExited:(id <NSDraggingInfo>)sender
1009 [[PSMTabDragAssistant sharedDragAssistant] draggingExitedTabBar:self];
1012 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
1017 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
1020 // HACK! Used below.
1021 NSTabViewItem *tvi = [[[PSMTabDragAssistant sharedDragAssistant] draggedCell] representedObject];
1024 [[PSMTabDragAssistant sharedDragAssistant] performDragOperation];
1027 // HACK! Notify the delegate that a tab was dragged to a new position.
1028 if (delegate && [delegate respondsToSelector:@selector(tabView:didDragTabViewItem:toIndex:)]) {
1029 int idx = [[self representedTabViewItems] indexOfObject:tvi];
1030 if (NSNotFound != idx) {
1031 [delegate tabView:[self tabView] didDragTabViewItem:tvi toIndex:idx];
1039 - (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation
1041 [[PSMTabDragAssistant sharedDragAssistant] draggedImageEndedAt:aPoint operation:operation];
1044 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
1050 #pragma mark Actions
1052 - (void)overflowMenuAction:(id)sender
1054 [tabView selectTabViewItem:[sender representedObject]];
1058 - (void)closeTabClick:(id)sender
1061 if(([_cells count] == 1) && (![self canCloseOnlyTab]))
1064 if(([self delegate]) && ([[self delegate] respondsToSelector:@selector(tabView:shouldCloseTabViewItem:)])){
1065 if(![[self delegate] tabView:tabView shouldCloseTabViewItem:[sender representedObject]]){
1066 // fix mouse downed close button
1067 [sender setCloseButtonPressed:NO];
1072 if(([self delegate]) && ([[self delegate] respondsToSelector:@selector(tabView:willCloseTabViewItem:)])){
1073 [[self delegate] tabView:tabView willCloseTabViewItem:[sender representedObject]];
1076 [[sender representedObject] retain];
1077 [tabView removeTabViewItem:[sender representedObject]];
1079 if(([self delegate]) && ([[self delegate] respondsToSelector:@selector(tabView:didCloseTabViewItem:)])){
1080 [[self delegate] tabView:tabView didCloseTabViewItem:[sender representedObject]];
1082 [[sender representedObject] release];
1086 - (void)tabClick:(id)sender
1088 [tabView selectTabViewItem:[sender representedObject]];
1092 - (void)tabNothing:(id)sender
1094 [self update]; // takes care of highlighting based on state
1097 - (void)frameDidChange:(NSNotification *)notification
1100 // trying to address the drawing artifacts for the progress indicators - hackery follows
1101 // this one fixes the "blanking" effect when the control hides and shows itself
1102 NSEnumerator *e = [_cells objectEnumerator];
1103 PSMTabBarCell *cell;
1104 while(cell = [e nextObject]){
1105 [[cell indicator] stopAnimation:self];
1106 [[cell indicator] startAnimation:self];
1108 [self setNeedsDisplay:YES];
1111 - (void)viewWillStartLiveResize
1113 NSEnumerator *e = [_cells objectEnumerator];
1114 PSMTabBarCell *cell;
1115 while(cell = [e nextObject]){
1116 [[cell indicator] stopAnimation:self];
1118 [self setNeedsDisplay:YES];
1121 -(void)viewDidEndLiveResize
1123 NSEnumerator *e = [_cells objectEnumerator];
1124 PSMTabBarCell *cell;
1125 while(cell = [e nextObject]){
1126 [[cell indicator] startAnimation:self];
1128 [self setNeedsDisplay:YES];
1131 - (void)windowDidMove:(NSNotification *)aNotification
1133 [self setNeedsDisplay:YES];
1136 - (void)windowStatusDidChange:(NSNotification *)notification
1138 // hide? must readjust things if I'm not supposed to be showing
1139 // this block of code only runs when the app launches
1140 if(_hideForSingleTab && ([_cells count] <= 1) && !_awakenedFromNib){
1141 // must adjust frames now before display
1142 NSRect myFrame = [self frame];
1144 NSRect partnerFrame = [partnerView frame];
1145 // above or below me?
1146 if(([self frame].origin.y - 22) > [partnerView frame].origin.y){
1147 // partner is below me
1148 [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y + 21, myFrame.size.width, myFrame.size.height - 21)];
1149 [partnerView setFrame:NSMakeRect(partnerFrame.origin.x, partnerFrame.origin.y, partnerFrame.size.width, partnerFrame.size.height + 21)];
1151 // partner is above me
1152 [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y, myFrame.size.width, myFrame.size.height - 21)];
1153 [partnerView setFrame:NSMakeRect(partnerFrame.origin.x, partnerFrame.origin.y - 21, partnerFrame.size.width, partnerFrame.size.height + 21)];
1155 [partnerView setNeedsDisplay:YES];
1156 [self setNeedsDisplay:YES];
1158 // for window movement
1159 NSRect windowFrame = [[self window] frame];
1160 [[self window] setFrame:NSMakeRect(windowFrame.origin.x, windowFrame.origin.y + 21, windowFrame.size.width, windowFrame.size.height - 21) display:YES];
1161 [self setFrame:NSMakeRect(myFrame.origin.x, myFrame.origin.y, myFrame.size.width, myFrame.size.height - 21)];
1164 [self setNeedsDisplay:YES];
1165 //[[self window] display];
1167 _awakenedFromNib = YES;
1172 #pragma mark NSTabView Delegate
1174 - (void)tabView:(NSTabView *)aTabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem
1176 // here's a weird one - this message is sent before the "tabViewDidChangeNumberOfTabViewItems"
1177 // message, thus I can end up updating when there are no cells, if no tabs were (yet) present
1178 if([_cells count] > 0){
1181 if([self delegate]){
1182 if([[self delegate] respondsToSelector:@selector(tabView:didSelectTabViewItem:)]){
1183 [[self delegate] performSelector:@selector(tabView:didSelectTabViewItem:) withObject:aTabView withObject:tabViewItem];
1188 - (BOOL)tabView:(NSTabView *)aTabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem
1190 if([self delegate]){
1191 if([[self delegate] respondsToSelector:@selector(tabView:shouldSelectTabViewItem:)]){
1192 return (int)[[self delegate] performSelector:@selector(tabView:shouldSelectTabViewItem:) withObject:aTabView withObject:tabViewItem];
1200 - (void)tabView:(NSTabView *)aTabView willSelectTabViewItem:(NSTabViewItem *)tabViewItem
1202 if([self delegate]){
1203 if([[self delegate] respondsToSelector:@selector(tabView:willSelectTabViewItem:)]){
1204 [[self delegate] performSelector:@selector(tabView:willSelectTabViewItem:) withObject:aTabView withObject:tabViewItem];
1209 - (void)tabViewDidChangeNumberOfTabViewItems:(NSTabView *)aTabView
1211 NSArray *tabItems = [tabView tabViewItems];
1212 // go through cells, remove any whose representedObjects are not in [tabView tabViewItems]
1213 NSEnumerator *e = [_cells objectEnumerator];
1214 PSMTabBarCell *cell;
1215 while(cell = [e nextObject]){
1216 if(![tabItems containsObject:[cell representedObject]]){
1217 [self removeTabForCell:cell];
1221 // go through tab view items, add cell for any not present
1222 NSMutableArray *cellItems = [self representedTabViewItems];
1223 NSEnumerator *ex = [tabItems objectEnumerator];
1224 NSTabViewItem *item;
1225 while(item = [ex nextObject]){
1226 if(![cellItems containsObject:item]){
1227 [self addTabViewItem:item];
1232 // HACK! Make sure '_cells' is ordered the same as 'tabItems'.
1233 NSMutableArray *temp = [[NSMutableArray alloc] initWithArray:_cells];
1234 e = [_cells objectEnumerator];
1235 int count = [temp count];
1236 while ((cell = [e nextObject])) {
1237 int idx = [tabItems indexOfObject:[cell representedObject]];
1238 if (NSNotFound != idx && idx < count) {
1239 [temp replaceObjectAtIndex:idx withObject:cell];
1246 if ([_cells count] == [tabView numberOfTabViewItems]) {
1247 [self update]; // don't update unless all are accounted for!
1251 // pass along for other delegate responses
1252 if([self delegate]){
1253 if([[self delegate] respondsToSelector:@selector(tabViewDidChangeNumberOfTabViewItems:)]){
1254 [[self delegate] performSelector:@selector(tabViewDidChangeNumberOfTabViewItems:) withObject:aTabView];
1260 #pragma mark Archiving
1262 - (void)encodeWithCoder:(NSCoder *)aCoder
1264 [super encodeWithCoder:aCoder];
1265 if ([aCoder allowsKeyedCoding]) {
1266 [aCoder encodeObject:_cells forKey:@"PSMcells"];
1267 [aCoder encodeObject:tabView forKey:@"PSMtabView"];
1268 [aCoder encodeObject:_overflowPopUpButton forKey:@"PSMoverflowPopUpButton"];
1269 [aCoder encodeObject:_addTabButton forKey:@"PSMaddTabButton"];
1270 [aCoder encodeObject:style forKey:@"PSMstyle"];
1271 [aCoder encodeBool:_canCloseOnlyTab forKey:@"PSMcanCloseOnlyTab"];
1272 [aCoder encodeBool:_hideForSingleTab forKey:@"PSMhideForSingleTab"];
1273 [aCoder encodeBool:_showAddTabButton forKey:@"PSMshowAddTabButton"];
1274 [aCoder encodeBool:_sizeCellsToFit forKey:@"PSMsizeCellsToFit"];
1275 [aCoder encodeInt:_cellMinWidth forKey:@"PSMcellMinWidth"];
1276 [aCoder encodeInt:_cellMaxWidth forKey:@"PSMcellMaxWidth"];
1277 [aCoder encodeInt:_cellOptimumWidth forKey:@"PSMcellOptimumWidth"];
1278 [aCoder encodeInt:_currentStep forKey:@"PSMcurrentStep"];
1279 [aCoder encodeBool:_isHidden forKey:@"PSMisHidden"];
1280 [aCoder encodeBool:_hideIndicators forKey:@"PSMhideIndicators"];
1281 [aCoder encodeObject:partnerView forKey:@"PSMpartnerView"];
1282 [aCoder encodeBool:_awakenedFromNib forKey:@"PSMawakenedFromNib"];
1283 [aCoder encodeObject:_lastMouseDownEvent forKey:@"PSMlastMouseDownEvent"];
1284 [aCoder encodeObject:delegate forKey:@"PSMdelegate"];
1289 - (id)initWithCoder:(NSCoder *)aDecoder
1291 self = [super initWithCoder:aDecoder];
1293 if ([aDecoder allowsKeyedCoding]) {
1294 _cells = [[aDecoder decodeObjectForKey:@"PSMcells"] retain];
1295 tabView = [[aDecoder decodeObjectForKey:@"PSMtabView"] retain];
1296 _overflowPopUpButton = [[aDecoder decodeObjectForKey:@"PSMoverflowPopUpButton"] retain];
1297 _addTabButton = [[aDecoder decodeObjectForKey:@"PSMaddTabButton"] retain];
1298 style = [[aDecoder decodeObjectForKey:@"PSMstyle"] retain];
1299 _canCloseOnlyTab = [aDecoder decodeBoolForKey:@"PSMcanCloseOnlyTab"];
1300 _hideForSingleTab = [aDecoder decodeBoolForKey:@"PSMhideForSingleTab"];
1301 _showAddTabButton = [aDecoder decodeBoolForKey:@"PSMshowAddTabButton"];
1302 _sizeCellsToFit = [aDecoder decodeBoolForKey:@"PSMsizeCellsToFit"];
1303 _cellMinWidth = [aDecoder decodeIntForKey:@"PSMcellMinWidth"];
1304 _cellMaxWidth = [aDecoder decodeIntForKey:@"PSMcellMaxWidth"];
1305 _cellOptimumWidth = [aDecoder decodeIntForKey:@"PSMcellOptimumWidth"];
1306 _currentStep = [aDecoder decodeIntForKey:@"PSMcurrentStep"];
1307 _isHidden = [aDecoder decodeBoolForKey:@"PSMisHidden"];
1308 _hideIndicators = [aDecoder decodeBoolForKey:@"PSMhideIndicators"];
1309 partnerView = [[aDecoder decodeObjectForKey:@"PSMpartnerView"] retain];
1310 _awakenedFromNib = [aDecoder decodeBoolForKey:@"PSMawakenedFromNib"];
1311 _lastMouseDownEvent = [[aDecoder decodeObjectForKey:@"PSMlastMouseDownEvent"] retain];
1312 delegate = [[aDecoder decodeObjectForKey:@"PSMdelegate"] retain];
1319 #pragma mark IB Palette
1321 - (NSSize)minimumFrameSizeFromKnobPosition:(int)position
1323 return NSMakeSize(100.0, 22.0);
1326 - (NSSize)maximumFrameSizeFromKnobPosition:(int)knobPosition
1328 return NSMakeSize(10000.0, 22.0);
1331 - (void)placeView:(NSRect)newFrame
1333 // this is called any time the view is resized in IB
1334 [self setFrame:newFrame];
1339 #pragma mark Convenience
1341 - (NSMutableArray *)representedTabViewItems
1343 NSMutableArray *temp = [NSMutableArray arrayWithCapacity:[_cells count]];
1344 NSEnumerator *e = [_cells objectEnumerator];
1345 PSMTabBarCell *cell;
1346 while(cell = [e nextObject]){
1347 [temp addObject:[cell representedObject]];
1352 - (id)cellForPoint:(NSPoint)point cellFrame:(NSRectPointer)outFrame
1354 NSRect aRect = [self genericCellRect];
1356 if(!NSPointInRect(point,aRect)){
1360 int i, cnt = [_cells count];
1361 for(i = 0; i < cnt; i++){
1362 PSMTabBarCell *cell = [_cells objectAtIndex:i];
1363 float width = [cell width];
1364 aRect.size.width = width;
1366 if(NSPointInRect(point, aRect)){
1372 aRect.origin.x += width;
1377 - (PSMTabBarCell *)lastVisibleTab
1379 int i, cellCount = [_cells count];
1380 for(i = 0; i < cellCount; i++){
1381 if([[_cells objectAtIndex:i] isInOverflowMenu])
1382 return [_cells objectAtIndex:(i-1)];
1384 return [_cells objectAtIndex:(cellCount - 1)];
1387 - (int)numberOfVisibleTabs
1389 int i, cellCount = [_cells count];
1390 for(i = 0; i < cellCount; i++){
1391 if([[_cells objectAtIndex:i] isInOverflowMenu])