summaryrefslogtreecommitdiff
path: root/frontends/cocoa/PSMTabBarControl/PSMTabBarController.m
blob: 68e1bc498accd81c1e9aefb5f49dbc480100613f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
//
//  PSMTabBarController.m
//  PSMTabBarControl
//
//  Created by Kent Sutherland on 11/24/06.
//  Copyright 2006 Kent Sutherland. All rights reserved.
//

#import "PSMTabBarController.h"
#import "PSMTabBarControl.h"
#import "PSMTabBarCell.h"
#import "PSMTabStyle.h"
#import "NSString_AITruncation.h"

#define MAX_OVERFLOW_MENUITEM_TITLE_LENGTH      60

@interface PSMTabBarController (Private)
- (NSArray *)_generateWidthsFromCells:(NSArray *)cells;
- (void)_setupCells:(NSArray *)cells withWidths:(NSArray *)widths;
@end

@implementation PSMTabBarController

/*!
    @method     initWithTabBarControl:
    @abstract   Creates a new PSMTabBarController instance.
    @discussion Creates a new PSMTabBarController for controlling a PSMTabBarControl. Should only be called by
                PSMTabBarControl.
    @param      A PSMTabBarControl.
    @returns    A newly created PSMTabBarController instance.
 */

- (id)initWithTabBarControl:(PSMTabBarControl *)control {
	if((self = [super init])) {
		_control = control;
		_cellTrackingRects = [[NSMutableArray alloc] init];
		_closeButtonTrackingRects = [[NSMutableArray alloc] init];
		_cellFrames = [[NSMutableArray alloc] init];
		_addButtonRect = NSZeroRect;
	}
	return self;
}

- (void)dealloc {
	[_cellTrackingRects release];
	[_closeButtonTrackingRects release];
	[_cellFrames release];
	[super dealloc];
}

/*!
    @method     addButtonRect
    @abstract   Returns the position for the add tab button.
    @discussion Returns the position for the add tab button.
    @returns    The rect  for the add button rect.
 */

- (NSRect)addButtonRect {
	return _addButtonRect;
}

/*!
    @method     overflowMenu
    @abstract   Returns current overflow menu or nil if there is none.
    @discussion Returns current overflow menu or nil if there is none.
    @returns    The current overflow menu.
 */

- (NSMenu *)overflowMenu {
	return _overflowMenu;
}

/*!
    @method     cellTrackingRectAtIndex:
    @abstract   Returns the rect for the tracking rect at the requested index.
    @discussion Returns the rect for the tracking rect at the requested index.
    @param      Index of a cell.
    @returns    The tracking rect of the cell at the requested index.
 */

- (NSRect)cellTrackingRectAtIndex:(NSUInteger)index {
	NSRect rect;
	if(index < [_cellTrackingRects count]) {
		rect = [[_cellTrackingRects objectAtIndex:index] rectValue];
	} else {
		NSLog(@"cellTrackingRectAtIndex: Invalid index (%ld)", (long)index);
		rect = NSZeroRect;
	}
	return rect;
}

/*!
    @method     closeButtonTrackingRectAtIndex:
    @abstract   Returns the tracking rect for the close button at the requested index.
    @discussion Returns the tracking rect for the close button at the requested index.
    @param      Index of a cell.
    @returns    The close button tracking rect of the cell at the requested index.
 */

- (NSRect)closeButtonTrackingRectAtIndex:(NSUInteger)index {
	NSRect rect;
	if(index < [_closeButtonTrackingRects count]) {
		rect = [[_closeButtonTrackingRects objectAtIndex:index] rectValue];
	} else {
		NSLog(@"closeButtonTrackingRectAtIndex: Invalid index (%ld)", (long)index);
		rect = NSZeroRect;
	}
	return rect;
}

/*!
    @method     cellFrameAtIndex:
    @abstract   Returns the frame for the cell at the requested index.
    @discussion Returns the frame for the cell at the requested index.
    @param      Index of a cell.
    @returns    The frame of the cell at the requested index.
 */

- (NSRect)cellFrameAtIndex:(NSUInteger)index {
	NSRect rect;

	if(index < [_cellFrames count]) {
		rect = [[_cellFrames objectAtIndex:index] rectValue];
	} else {
		NSLog(@"cellFrameAtIndex: Invalid index (%ld)", (long)index);
		rect = NSZeroRect;
	}
	return rect;
}

/*!
    @method     setSelectedCell:
    @abstract   Changes the cell states so the given cell is the currently selected cell.
    @discussion Makes the given cell the active cell and properly recalculates the tab states for surrounding cells.
    @param      An instance of PSMTabBarCell to make active.
 */

- (void)setSelectedCell:(PSMTabBarCell *)cell {
	NSArray *cells = [_control cells];
	NSEnumerator *enumerator = [cells objectEnumerator];
	PSMTabBarCell *lastCell = nil, *nextCell;

	//deselect the previously selected tab
	while((nextCell = [enumerator nextObject]) && ([nextCell state] == NSOffState)) {
		lastCell = nextCell;
	}

	[nextCell setState:NSOffState];
	[nextCell setTabState:PSMTab_PositionMiddleMask];

	if(lastCell && lastCell != [_control lastVisibleTab]) {
		[lastCell setTabState:~[lastCell tabState] & PSMTab_RightIsSelectedMask];
	}

	if((nextCell = [enumerator nextObject])) {
		[nextCell setTabState:~[lastCell tabState] & PSMTab_LeftIsSelectedMask];
	}

	[cell setState:NSOnState];
	[cell setTabState:PSMTab_SelectedMask];

	if(![cell isInOverflowMenu]) {
		NSUInteger cellIndex = [cells indexOfObject:cell];

		if(cellIndex > 0) {
			nextCell = [cells objectAtIndex:cellIndex - 1];
			[nextCell setTabState:[nextCell tabState] | PSMTab_RightIsSelectedMask];
		}

		if(cellIndex < [cells count] - 1) {
			nextCell = [cells objectAtIndex:cellIndex + 1];
			[nextCell setTabState:[nextCell tabState] | PSMTab_LeftIsSelectedMask];
		}
	}
}

/*!
    @method     layoutCells
    @abstract   Recalculates cell positions and states.
    @discussion This method calculates the proper frame, tabState and overflow menu status for all cells in the
                tab bar control.
 */

- (void)layoutCells {
	NSArray *cells = [_control cells];
	NSInteger cellCount = [cells count];

	// make sure all of our tabs are accounted for before updating
	if([[_control tabView] numberOfTabViewItems] != cellCount) {
		return;
	}

	[_cellTrackingRects removeAllObjects];
	[_closeButtonTrackingRects removeAllObjects];
	[_cellFrames removeAllObjects];

	NSArray *cellWidths = [self _generateWidthsFromCells:cells];
	[self _setupCells:cells withWidths:cellWidths];

	//set up the rect from the add tab button
	_addButtonRect = [_control genericCellRect];
	_addButtonRect.size = [[_control addTabButton] frame].size;
	if([_control orientation] == PSMTabBarHorizontalOrientation) {
		_addButtonRect.origin.y = MARGIN_Y;
		_addButtonRect.origin.x += [[cellWidths valueForKeyPath:@"@sum.floatValue"] doubleValue] + 2;
	} else {
		_addButtonRect.origin.x = 0;
		_addButtonRect.origin.y = [[cellWidths lastObject] doubleValue];
	}
}

/*!
 *  @method _shrinkWidths:towardMinimum:withAvailableWidth:
 *  @abstract Decreases widths in an array toward a minimum until they fit within availableWidth, if possible
 *  @param An array of NSNumbers
 *  @param The target minimum
 *  @param The maximum available width
 *  @returns The amount by which the total array width was shrunk
 */
- (NSInteger)_shrinkWidths:(NSMutableArray *)newWidths towardMinimum:(NSInteger)minimum withAvailableWidth:(CGFloat)availableWidth {
	BOOL changed = NO;
	NSInteger count = [newWidths count];
	NSInteger totalWidths = [[newWidths valueForKeyPath:@"@sum.intValue"] integerValue];
	NSInteger originalTotalWidths = totalWidths;

	do {
		changed = NO;

		for(NSInteger q = (count - 1); q >= 0; q--) {
			CGFloat cellWidth = [[newWidths objectAtIndex:q] doubleValue];
			if(cellWidth - 1 >= minimum) {
				cellWidth--;
				totalWidths--;

				[newWidths replaceObjectAtIndex:q
				 withObject:[NSNumber numberWithDouble:cellWidth]];

				changed = YES;
			}
		}
	} while(changed && (totalWidths > availableWidth));

	return(originalTotalWidths - totalWidths);
}

/*!
 * @function   potentialMinimumForArray()
 * @abstract   Calculate the minimum total for a given array of widths
 * @discussion The array is summed using, for each item, the minimum between the current value and the passed minimum value.
 *             This is useful for getting a sum if the array has size-to-fit widths which will be allowed to be less than the
 *             specified minimum.
 * @param      An array of widths
 * @param      The minimum
 * @returns    The smallest possible sum for the array
 */
static NSInteger potentialMinimumForArray(NSArray *array, NSInteger minimum){
	NSInteger runningTotal = 0;
	NSInteger count = [array count];

	for(NSInteger i = 0; i < count; i++) {
		NSInteger currentValue = [[array objectAtIndex:i] integerValue];
		runningTotal += MIN(currentValue, minimum);
	}

	return runningTotal;
}

/*!
    @method     _generateWidthsFromCells:
    @abstract   Calculates the width of cells that would be visible.
    @discussion Calculates the width of cells in the tab bar and returns an array of widths for the cells that would be
                visible. Uses large blocks of code that were previously in PSMTabBarControl's update method.
    @param      An array of PSMTabBarCells.
    @returns    An array of numbers representing the widths of cells that would be visible.
 */

- (NSArray *)_generateWidthsFromCells:(NSArray *)cells {
	NSInteger cellCount = [cells count], i, numberOfVisibleCells = ([_control orientation] == PSMTabBarHorizontalOrientation) ? 1 : 0;
	NSMutableArray *newWidths = [NSMutableArray arrayWithCapacity:cellCount];
	id <PSMTabStyle> style = [_control style];
	CGFloat availableWidth = [_control availableCellWidth], currentOrigin = 0, totalOccupiedWidth = 0.0, width;
	NSRect cellRect = [_control genericCellRect], controlRect = [_control frame];
	PSMTabBarCell *currentCell;

	if([_control orientation] == PSMTabBarVerticalOrientation) {
		currentOrigin = [style topMarginForTabBarControl];
	}

	//Don't let cells overlap the add tab button if it is visible
	if([_control showAddTabButton]) {
		availableWidth -= [self addButtonRect].size.width;
	}

	for(i = 0; i < cellCount; i++) {
		currentCell = [cells objectAtIndex:i];

		// supress close button?
		[currentCell setCloseButtonSuppressed:((cellCount == 1 && [_control canCloseOnlyTab] == NO) ||
											   [_control disableTabClose] ||
											   ([[_control delegate] respondsToSelector:@selector(tabView:disableTabCloseForTabViewItem:)] &&
												[[_control delegate] tabView:[_control tabView] disableTabCloseForTabViewItem:[currentCell representedObject]]))];

		if([_control orientation] == PSMTabBarHorizontalOrientation) {
			// Determine cell width
			if([_control sizeCellsToFit]) {
				width = [currentCell desiredWidthOfCell];
				if(width > [_control cellMaxWidth]) {
					width = [_control cellMaxWidth];
				}
			} else {
				width = [_control cellOptimumWidth];
			}

			width = ceil(width);

			//check to see if there is not enough space to place all tabs as preferred
			if(totalOccupiedWidth + width >= availableWidth) {
				//There's not enough space to add currentCell at its preferred width!

				//If we're not going to use the overflow menu, cram all the tab cells into the bar regardless of minimum width
				if(![_control useOverflowMenu]) {
					NSInteger j, averageWidth = (availableWidth / cellCount);

					numberOfVisibleCells = cellCount;
					[newWidths removeAllObjects];

					for(j = 0; j < cellCount; j++) {
						CGFloat desiredWidth = [[cells objectAtIndex:j] desiredWidthOfCell];
						[newWidths addObject:[NSNumber numberWithDouble:(desiredWidth < averageWidth && [_control sizeCellsToFit]) ? desiredWidth : averageWidth]];
					}

					totalOccupiedWidth = [[newWidths valueForKeyPath:@"@sum.intValue"] integerValue];
					break;
				}

				//We'll be using the overflow menu if needed.
				numberOfVisibleCells = i;
				if([_control sizeCellsToFit]) {
					BOOL remainingCellsMustGoToOverflow = NO;

					totalOccupiedWidth = [[newWidths valueForKeyPath:@"@sum.intValue"] integerValue];

					/* Can I squeeze it in without violating min cell width? This is the width we would take up
					 * if every cell so far were at the control minimum size (or their current size if that is less than the control minimum).
					 */
					if((potentialMinimumForArray(newWidths, [_control cellMinWidth]) + MIN(width, [_control cellMinWidth])) <= availableWidth) {
						/* It's definitely possible for cells so far to be visible.
						 * Shrink other cells to allow this one to fit
						 */
						NSInteger cellMinWidth = [_control cellMinWidth];

						/* Start off adding it to the array; we know that it will eventually fit because
						 * (the potential minimum <= availableWidth)
						 *
						 * This allows average and minimum aggregates on the NSArray to work.
						 */
						[newWidths addObject:[NSNumber numberWithDouble:width]];
						numberOfVisibleCells++;

						totalOccupiedWidth += width;

						//First, try to shrink tabs toward the average. Tabs smaller than average won't change
						totalOccupiedWidth -= [self _shrinkWidths:newWidths
											   towardMinimum:[[newWidths valueForKeyPath:@"@avg.intValue"] integerValue]
											   withAvailableWidth:availableWidth];



						if(totalOccupiedWidth > availableWidth) {
							//Next, shrink tabs toward the smallest of the existing tabs. The smallest tab won't change.
							NSInteger smallestTabWidth = [[newWidths valueForKeyPath:@"@min.intValue"] integerValue];
							if(smallestTabWidth > cellMinWidth) {
								totalOccupiedWidth -= [self _shrinkWidths:newWidths
													   towardMinimum:smallestTabWidth
													   withAvailableWidth:availableWidth];
							}
						}

						if(totalOccupiedWidth > availableWidth) {
							//Finally, shrink tabs toward the imposed minimum size.  All tabs larger than the minimum wll change.
							totalOccupiedWidth -= [self _shrinkWidths:newWidths
												   towardMinimum:cellMinWidth
												   withAvailableWidth:availableWidth];
						}

						if(totalOccupiedWidth > availableWidth) {
							NSLog(@"**** -[PSMTabBarController generateWidthsFromCells:] This is a failure (available %f, total %f, width is %f)",
								  availableWidth, totalOccupiedWidth, width);
							remainingCellsMustGoToOverflow = YES;
						}

						if(totalOccupiedWidth < availableWidth) {
							/* We're not using all available space not but exceeded available width before;
							 * stretch all cells to fully fit the bar
							 */
							NSInteger leftoverWidth = availableWidth - totalOccupiedWidth;
							if(leftoverWidth > 0) {
								NSInteger q;
								for(q = numberOfVisibleCells - 1; q >= 0; q--) {
									NSInteger desiredAddition = (NSInteger)leftoverWidth / (q + 1);
									NSInteger newCellWidth = (NSInteger)[[newWidths objectAtIndex:q] doubleValue] + desiredAddition;
									[newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithDouble:newCellWidth]];
									leftoverWidth -= desiredAddition;
									totalOccupiedWidth += desiredAddition;
								}
							}
						}
					} else {
						// stretch - distribute leftover room among cells, since we can't add this cell
						NSInteger leftoverWidth = availableWidth - totalOccupiedWidth;
						NSInteger q;
						for(q = i - 1; q >= 0; q--) {
							NSInteger desiredAddition = (NSInteger)leftoverWidth / (q + 1);
							NSInteger newCellWidth = (NSInteger)[[newWidths objectAtIndex:q] doubleValue] + desiredAddition;
							[newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithDouble:newCellWidth]];
							leftoverWidth -= desiredAddition;
						}

						remainingCellsMustGoToOverflow = YES;
					}

					// done assigning widths; remaining cells go in overflow menu
					if(remainingCellsMustGoToOverflow) {
						break;
					}
				} else {
					//We're not using size-to-fit
					NSInteger revisedWidth = availableWidth / (i + 1);
					if(revisedWidth >= [_control cellMinWidth]) {
						NSUInteger q;
						totalOccupiedWidth = 0;

						for(q = 0; q < [newWidths count]; q++) {
							[newWidths replaceObjectAtIndex:q withObject:[NSNumber numberWithDouble:revisedWidth]];
							totalOccupiedWidth += revisedWidth;
						}
						// just squeezed this one in...
						[newWidths addObject:[NSNumber numberWithDouble:revisedWidth]];
						totalOccupiedWidth += revisedWidth;
						numberOfVisibleCells++;
					} else {
						// couldn't fit that last one...
						break;
					}
				}
			} else {
				//(totalOccupiedWidth < availableWidth)
				numberOfVisibleCells = cellCount;
				[newWidths addObject:[NSNumber numberWithDouble:width]];
				totalOccupiedWidth += width;
			}
		} else {
			//lay out vertical tabs
			if(currentOrigin + cellRect.size.height <= controlRect.size.height) {
				[newWidths addObject:[NSNumber numberWithDouble:currentOrigin]];
				numberOfVisibleCells++;
				currentOrigin += cellRect.size.height;
			} else {
				//out of room, the remaining tabs go into overflow
				if([newWidths count] > 0 && controlRect.size.height - currentOrigin < 17) {
					[newWidths removeLastObject];
					numberOfVisibleCells--;
				}
				break;
			}
		}
	}

	//make sure there are at least two items in the horizontal tab bar
	if([_control orientation] == PSMTabBarHorizontalOrientation) {
		if(numberOfVisibleCells < 2 && [cells count] > 1) {
			PSMTabBarCell *cell1 = [cells objectAtIndex:0], *cell2 = [cells objectAtIndex:1];
			NSNumber *cellWidth;

			[newWidths removeAllObjects];
			totalOccupiedWidth = 0;

			cellWidth = [NSNumber numberWithDouble:[cell1 desiredWidthOfCell] < availableWidth * 0.5f ?[cell1 desiredWidthOfCell] : availableWidth * 0.5f];
			[newWidths addObject:cellWidth];
			totalOccupiedWidth += [cellWidth doubleValue];

			cellWidth = [NSNumber numberWithDouble:[cell2 desiredWidthOfCell] < (availableWidth - totalOccupiedWidth) ?[cell2 desiredWidthOfCell] : (availableWidth - totalOccupiedWidth)];
			[newWidths addObject:cellWidth];
			totalOccupiedWidth += [cellWidth doubleValue];

			if(totalOccupiedWidth < availableWidth) {
				[newWidths replaceObjectAtIndex:0 withObject:[NSNumber numberWithDouble:availableWidth - [cellWidth doubleValue]]];
			}

			numberOfVisibleCells = 2;
		}
	}

	return newWidths;
}

/*!
    @method     _setupCells:withWidths
    @abstract   Creates tracking rect arrays and sets the frames of the visible cells.
    @discussion Creates tracking rect arrays and sets the cells given in the widths array.
 */

- (void)_setupCells:(NSArray *)cells withWidths:(NSArray *)widths {
	NSUInteger i, tabState, cellCount = [cells count];
	NSRect cellRect = [_control genericCellRect];
	PSMTabBarCell *cell;
	NSTabViewItem *selectedTabViewItem = [[_control tabView] selectedTabViewItem];
	NSMenuItem *menuItem;

	[_overflowMenu release], _overflowMenu = nil;

	for(i = 0; i < cellCount; i++) {
		cell = [cells objectAtIndex:i];

		if(i < [widths count]) {
			tabState = 0;

			// set cell frame
			if([_control orientation] == PSMTabBarHorizontalOrientation) {
				cellRect.size.width = [[widths objectAtIndex:i] doubleValue];
			} else {
				cellRect.size.width = [_control frame].size.width;
				cellRect.origin.y = [[widths objectAtIndex:i] doubleValue];
				cellRect.origin.x = 0;
			}

			[_cellFrames addObject:[NSValue valueWithRect:cellRect]];

			//add tracking rects to arrays
			[_closeButtonTrackingRects addObject:[NSValue valueWithRect:[cell closeButtonRectForFrame:cellRect]]];
			[_cellTrackingRects addObject:[NSValue valueWithRect:cellRect]];

			if([[cell representedObject] isEqualTo:selectedTabViewItem]) {
				[cell setState:NSOnState];
				tabState |= PSMTab_SelectedMask;
				// previous cell
				if(i > 0) {
					[[cells objectAtIndex:i - 1] setTabState:([(PSMTabBarCell *)[cells objectAtIndex:i - 1] tabState] | PSMTab_RightIsSelectedMask)];
				}
				// next cell - see below
			} else {
				[cell setState:NSOffState];
				// see if prev cell was selected
				if((i > 0) && ([[cells objectAtIndex:i - 1] state] == NSOnState)) {
					tabState |= PSMTab_LeftIsSelectedMask;
				}
			}

			// more tab states
			if([widths count] == 1) {
				tabState |= PSMTab_PositionLeftMask | PSMTab_PositionRightMask | PSMTab_PositionSingleMask;
			} else if(i == 0) {
				tabState |= PSMTab_PositionLeftMask;
			} else if(i == [widths count] - 1) {
				tabState |= PSMTab_PositionRightMask;
			}

			[cell setTabState:tabState];
			[cell setIsInOverflowMenu:NO];

			// indicator
			if(![[cell indicator] isHidden] && ![_control isTabBarHidden]) {
				if(![[_control subviews] containsObject:[cell indicator]]) {
					[_control addSubview:[cell indicator]];
					[[cell indicator] startAnimation:self];
				}
			}

			// next...
			cellRect.origin.x += [[widths objectAtIndex:i] doubleValue];
		} else {
			[cell setState:NSOffState];
			[cell setIsInOverflowMenu:YES];
			[[cell indicator] removeFromSuperview];

			//position the cell well offscreen
			if([_control orientation] == PSMTabBarHorizontalOrientation) {
				cellRect.origin.x += [[_control style] rightMarginForTabBarControl] + 20;
			} else {
				cellRect.origin.y = [_control frame].size.height + 2;
			}

			[_cellFrames addObject:[NSValue valueWithRect:cellRect]];

			if(_overflowMenu == nil) {
				_overflowMenu = [[NSMenu alloc] init];
				[_overflowMenu insertItemWithTitle:@"" action:nil keyEquivalent:@"" atIndex:0]; // Because the overflowPupUpButton is a pull down menu
				[_overflowMenu setDelegate:self];
			}

			// Each item's title is limited to 60 characters. If more than 60 characters, use an ellipsis to indicate that more exists.
			menuItem = [_overflowMenu addItemWithTitle:[[[cell attributedStringValue] string] stringWithEllipsisByTruncatingToLength:MAX_OVERFLOW_MENUITEM_TITLE_LENGTH]
						action:@selector(overflowMenuAction:)
						keyEquivalent:@""];
			[menuItem setTarget:_control];
			[menuItem setRepresentedObject:[cell representedObject]];

			if([cell count] > 0) {
				[menuItem setTitle:[[menuItem title] stringByAppendingFormat:@" (%lu)", (unsigned long)[cell count]]];
			}
		}
	}
}

- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)menuItem atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel {
	if(menu == _overflowMenu) {
		if([[[menuItem representedObject] identifier] respondsToSelector:@selector(icon)]) {
			[menuItem setImage:[[[menuItem representedObject] identifier] valueForKey:@"icon"]];
		}
	}

	return TRUE;
}

- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu {
	if(menu == _overflowMenu) {
		return [_overflowMenu numberOfItems];
	} else {
		NSLog(@"Warning: Unexpected menu delegate call for menu %@", menu);
		return 0;
	}
}

@end

/*
   PSMTabBarController will store what the current tab frame state should be like based off the last layout. PSMTabBarControl
   has to handle fetching the new frame and then changing the tab cell frame.
    Tab states will probably be changed immediately.

   Tabs that aren't going to be visible need to have their frame set offscreen. Treat them as if they were visible.

   The overflow menu is rebuilt and stored by the controller.

   Arrays of tracking rects will be created here, but not applied.
    Tracking rects are removed and added by PSMTabBarControl at the end of an animate/display cycle.

   The add tab button frame is handled by this controller. Visibility and location are set by the control.

   isInOverflowMenu should probably be removed in favor of a call that returns yes/no to if a cell is in overflow. (Not yet implemented)

   Still need to rewrite most of the code in PSMTabDragAssistant.
 */