//
//  MGScopeBar.m
//  MGScopeBar
//
//  Created by Matt Gemmell on 15/03/2008.
//  Copyright 2008 Instinctive Code.
//

#import "MGScopeBar.h"
#import "MGRecessedPopUpButtonCell.h"


#define SCOPE_BAR_H_INSET				8.0																		// inset on left and right
#define SCOPE_BAR_HEIGHT				25.0																	// used in -sizeToFit
#define SCOPE_BAR_START_COLOR_GRAY		[NSColor colorWithCalibratedWhite:0.75 alpha:1.0]						// bottom color of gray gradient
#define SCOPE_BAR_END_COLOR_GRAY		[NSColor colorWithCalibratedWhite:0.90 alpha:1.0]						// top color of gray gradient
#define SCOPE_BAR_START_COLOR_BLUE		[NSColor colorWithCalibratedRed:0.71 green:0.75 blue:0.81 alpha:1.0]	// bottom color of blue gradient
#define SCOPE_BAR_END_COLOR_BLUE		[NSColor colorWithCalibratedRed:0.80 green:0.82 blue:0.87 alpha:1.0]	// top color of blue gradient
#define SCOPE_BAR_BORDER_COLOR			[NSColor colorWithCalibratedWhite:0.69 alpha:1.0]						// bottom line's color
#define SCOPE_BAR_BORDER_WIDTH			1.0																		// bottom line's width

#define SCOPE_BAR_SEPARATOR_COLOR		[NSColor colorWithCalibratedWhite:0.52 alpha:1.0]	// color of vertical-line separators between groups
#define SCOPE_BAR_SEPARATOR_WIDTH		1.0													// width of vertical-line separators between groups
#define SCOPE_BAR_SEPARATOR_HEIGHT		16.0												// separators are vertically centered in the bar

#define SCOPE_BAR_LABEL_COLOR			[NSColor colorWithCalibratedWhite:0.45 alpha:1.0]	// color of groups' labels
#define SCOPE_BAR_FONTSIZE				12.0												// font-size of labels and buttons
#define SCOPE_BAR_ITEM_SPACING			6.0													// spacing between buttons/separators/labels
#define SCOPE_BAR_BUTTON_IMAGE_SIZE		16.0												// size of buttons' images (width and height)

#define SCOPE_BAR_HIDE_POPUP_BG			YES	// whether the bezel background of an NSPopUpButton is hidden when none of its menu-items are selected.

// Appearance metrics. These were chosen to mimic the Finder's "Find" (Spotlight / Smart Group / etc) window's scope-bar.
#define MENU_PADDING					25.0					// how much wider a popup-button is than a regular button with the same title.
#define MENU_MIN_WIDTH					60.0					// minimum width a popup-button can be narrowed to.

// NSPopUpButton titles used for groups which allow multiple selection.
#define POPUP_TITLE_EMPTY_SELECTION		NSLocalizedString(@"(None)", nil)		// title used when no items in the popup are selected.
#define POPUP_TITLE_MULTIPLE_SELECTION	NSLocalizedString(@"(Multiple)", nil)	// title used when multiple items in the popup are selected.


// ---- end of configurable settings ---- //


// Keys for internal use.
#define GROUP_IDENTIFIERS				@"Identifiers"			// NSMutableArray of identifier strings.
#define GROUP_BUTTONS					@"Buttons"				// NSMutableArray of either NSButtons or NSMenuItems, one per item.
#define GROUP_SELECTION_MODE			@"SelectionMode"		// MGScopeBarGroupSelectionMode (int) as NSNumber.
#define GROUP_MENU_MODE					@"MenuMode"				// BOOL, YES if group is collected in a popup-menu, else NO.
#define GROUP_POPUP_BUTTON				@"PopupButton"			// NSPopUpButton (only present if group is in menu-mode).
#define GROUP_HAS_SEPARATOR				@"HasSeparator"			// BOOL, YES if group has a separator before it.
#define GROUP_HAS_LABEL					@"HasLabel"				// BOOL, YES if group has a label.
#define GROUP_LABEL_FIELD				@"LabelField"			// NSTextField for the label (optional; only if group has a label)
#define GROUP_TOTAL_BUTTONS_WIDTH		@"TotalButtonsWidth"	// Width of all buttons in a group plus spacings between them (doesn't include label etc)
#define GROUP_WIDEST_BUTTON_WIDTH		@"WidestButtonWidth"	// Width of widest button, used when making popup-menus.
#define GROUP_CUMULATIVE_WIDTH			@"CumulativeWidth"		// Width from left of leftmost group to right of this group (all groups fully expanded).


@interface MGScopeBar (MGPrivateMethods)

- (IBAction)scopeButtonClicked:(id)sender;
- (NSButton *)getButtonForItem:(NSString *)identifier inGroup:(int)groupNumber; // returns relevant button/menu-item
- (void)updateSelectedState:(BOOL)selected forItem:(NSString *)identifier inGroup:(int)groupNumber informDelegate:(BOOL)inform;
- (NSButton *)buttonForItem:(NSString *)identifier inGroup:(int)groupNumber 
				  withTitle:(NSString *)title image:(NSImage *)image; // creates a new NSButton
- (NSMenuItem *)menuItemForItem:(NSString *)identifier inGroup:(int)groupNumber 
					  withTitle:(NSString *)title image:(NSImage *)image; // creates a new NSMenuitem
- (NSPopUpButton *)popupButtonForGroup:(NSDictionary *)group;
- (void)setControl:(NSObject *)control forIdentifier:(NSString *)identifier inGroup:(int)groupNumber;
- (void)updateMenuTitleForGroupAtIndex:(int)groupNumber;

@end


@implementation MGScopeBar


#pragma mark Setup and teardown


- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        _smartResizeEnabled = YES;
		// Everything else is reset in -reloadData.
    }
    return self;
}


- (void)dealloc
{
	delegate = nil;
	if (_accessoryView) {
		[_accessoryView removeFromSuperview];
		_accessoryView = nil; // weak ref
	}
	[_separatorPositions release];
	[_groups release];
	[_identifiers release];
	[_selectedItems release];
	
	[super dealloc];
}


#pragma mark Data management


- (void)reloadData
{
	// Resize if necessary.
	[self sizeToFit];
	
	// Remove any old objects.
	if (_accessoryView) {
		[_accessoryView removeFromSuperview];
		_accessoryView = nil; // weak ref
	}
	
	NSArray *subviews = [[self subviews] copy]; // so we don't mutate the collection we're iterating over.
	[subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
	[subviews release]; // because copies are retained.
	
	[_separatorPositions release];
	_separatorPositions = nil;
	[_groups release];
	_groups = nil;
	[_identifiers release];
	_identifiers = nil;
	[_selectedItems release];
	_selectedItems = nil;
	_firstCollapsedGroup = NSNotFound;
	_lastWidth = NSNotFound;
	_totalGroupsWidth = 0;
	_totalGroupsWidthForPopups = 0;
	
	// Configure contents via delegate.
	if (self.delegate && [delegate conformsToProtocol:@protocol(MGScopeBarDelegate)]) {
		int numGroups = [delegate numberOfGroupsInScopeBar:self];
		
		if (numGroups > 0) {
			_separatorPositions = [[NSMutableArray alloc] initWithCapacity:numGroups];
			_groups = [[NSMutableArray alloc] initWithCapacity:numGroups];
			_identifiers = [[NSMutableDictionary alloc] initWithCapacity:0];
			_selectedItems = [[NSMutableArray alloc] initWithCapacity:numGroups];
			
			int xCoord = SCOPE_BAR_H_INSET;
			NSRect ctrlRect = NSZeroRect;
			BOOL providesImages = [delegate respondsToSelector:@selector(scopeBar:imageForItem:inGroup:)];
			
			int groupNum;
			for (groupNum = 0; groupNum < numGroups; groupNum++) {
				// Add separator if appropriate.
				BOOL addSeparator = (groupNum > 0); // default behavior.
				if ([delegate respondsToSelector:@selector(scopeBar:showSeparatorBeforeGroup:)]) {
					addSeparator = [delegate scopeBar:self showSeparatorBeforeGroup:groupNum];
				}
				if (addSeparator) {
					[_separatorPositions addObject:[NSNumber numberWithInt:xCoord]];
					xCoord += SCOPE_BAR_SEPARATOR_WIDTH + SCOPE_BAR_ITEM_SPACING;
					
					_totalGroupsWidth += SCOPE_BAR_SEPARATOR_WIDTH + SCOPE_BAR_ITEM_SPACING;
					_totalGroupsWidthForPopups += SCOPE_BAR_SEPARATOR_WIDTH + SCOPE_BAR_ITEM_SPACING;
				} else {
					[_separatorPositions addObject:[NSNull null]];
				}
				
				// Add label if appropriate.
				NSString *groupLabel = [delegate scopeBar:self labelForGroup:groupNum];
				NSTextField *labelField = nil;
				BOOL hasLabel = NO;
				if (groupLabel && [groupLabel length] > 0) {
					hasLabel = YES;
					ctrlRect = NSMakeRect(xCoord, 6, 15, 50);
					labelField = [[NSTextField alloc] initWithFrame:ctrlRect];
					[labelField setStringValue:groupLabel];
					[labelField setEditable:NO];
					[labelField setBordered:NO];
					[labelField setDrawsBackground:NO];
					[labelField setTextColor:SCOPE_BAR_LABEL_COLOR];
					[labelField setFont:[NSFont boldSystemFontOfSize:SCOPE_BAR_FONTSIZE]];
					[labelField sizeToFit];
					ctrlRect.size = [labelField frame].size;
					[labelField setFrame:ctrlRect];
					[self addSubview:labelField];
					[labelField release];
					
					xCoord += ctrlRect.size.width + SCOPE_BAR_ITEM_SPACING;
					
					_totalGroupsWidth += ctrlRect.size.width + SCOPE_BAR_ITEM_SPACING;
					_totalGroupsWidthForPopups += ctrlRect.size.width + SCOPE_BAR_ITEM_SPACING;
				}
				
				// Create group information for use during interaction.
				NSArray *identifiers = [delegate scopeBar:self itemIdentifiersForGroup:groupNum];
				NSMutableArray *usedIdentifiers = [NSMutableArray arrayWithCapacity:[identifiers count]];
				NSMutableArray *buttons = [NSMutableArray arrayWithCapacity:[identifiers count]];
				MGScopeBarGroupSelectionMode selMode = [delegate scopeBar:self selectionModeForGroup:groupNum];
				if (selMode != MGRadioSelectionMode && selMode != MGMultipleSelectionMode) {
					// Sanity check, since this is just an int.
					selMode = MGRadioSelectionMode;
				}
				NSMutableDictionary *groupInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
												  usedIdentifiers, GROUP_IDENTIFIERS, 
												  buttons, GROUP_BUTTONS, 
												  [NSNumber numberWithInt:selMode], GROUP_SELECTION_MODE, 
												  [NSNumber numberWithBool:NO], GROUP_MENU_MODE, 
												  [NSNumber numberWithBool:hasLabel], GROUP_HAS_LABEL, 
												  [NSNumber numberWithBool:addSeparator], GROUP_HAS_SEPARATOR, 
												  nil];
				if (hasLabel) {
					[groupInfo setObject:labelField forKey:GROUP_LABEL_FIELD];
				}
				[_groups addObject:groupInfo];
				[_selectedItems addObject:[NSMutableArray arrayWithCapacity:0]];
				
				// Add buttons for this group.
				float widestButtonWidth = 0;
				float totalButtonsWidth = 0;
				for (NSString *itemID in identifiers) {
					if (![usedIdentifiers containsObject:itemID]) {
						[usedIdentifiers addObject:itemID];
					} else {
						// Identifier already used for this group; skip it.
						continue;
					}
					
					NSString *title = [delegate scopeBar:self titleOfItem:itemID inGroup:groupNum];
					NSImage *image = nil;
					if (providesImages) {
						image = [delegate scopeBar:self imageForItem:itemID inGroup:groupNum];
					}
					NSButton *button = [self buttonForItem:itemID inGroup:groupNum withTitle:title image:image];
					
					ctrlRect = [button frame];
					ctrlRect.origin.x = xCoord;
					[button setFrame:ctrlRect];
					[self addSubview:button];
					[buttons addObject:button];
					
					// Adjust x-coordinate for next item in the bar.
					xCoord += ctrlRect.size.width + SCOPE_BAR_ITEM_SPACING;
					
					// Update total and widest button widths.
					if (totalButtonsWidth > 0) {
						// Add spacing before this item, since it's not the first in the group.
						totalButtonsWidth += SCOPE_BAR_ITEM_SPACING;
					}
					totalButtonsWidth += ctrlRect.size.width;
					if (ctrlRect.size.width > widestButtonWidth) {
						widestButtonWidth = ctrlRect.size.width;
					}
				}
				
				// Add the accumulated buttons' width and the widest button's width to groupInfo.
				[groupInfo setObject:[NSNumber numberWithFloat:totalButtonsWidth] forKey:GROUP_TOTAL_BUTTONS_WIDTH];
				[groupInfo setObject:[NSNumber numberWithFloat:widestButtonWidth] forKey:GROUP_WIDEST_BUTTON_WIDTH];
				
				_totalGroupsWidth += totalButtonsWidth;
				_totalGroupsWidthForPopups += widestButtonWidth + MENU_PADDING;
				
				float cumulativeWidth = _totalGroupsWidth + (groupNum * SCOPE_BAR_ITEM_SPACING);
				[groupInfo setObject:[NSNumber numberWithFloat:cumulativeWidth] forKey:GROUP_CUMULATIVE_WIDTH];
				
				// If this is a radio-mode group, select the first item automatically.
				if (selMode == MGRadioSelectionMode) {
					[self updateSelectedState:YES forItem:[identifiers objectAtIndex:0] inGroup:groupNum informDelegate:YES];
				}
			}
			
			_totalGroupsWidth += ((numGroups - 1) * SCOPE_BAR_ITEM_SPACING);
			_totalGroupsWidthForPopups += ((numGroups - 1) * SCOPE_BAR_ITEM_SPACING);
		}
		
		// Add accessoryView, if provided.
		if ([delegate respondsToSelector:@selector(accessoryViewForScopeBar:)]) {
			_accessoryView = [delegate accessoryViewForScopeBar:self];
			if (_accessoryView) {
				// Remove NSViewMaxXMargin flag from resizing mask, if present.
				NSUInteger mask = [_accessoryView autoresizingMask];
				if (mask & NSViewMaxXMargin) {
					mask &= ~NSViewMaxXMargin;
				}
				
				// Add NSViewMinXMargin flag to resizing mask, if not present.
				if (!(mask & NSViewMinXMargin)) {
					mask = (mask | NSViewMinXMargin);
				}
				
				// Update view sizing mask.
				[_accessoryView setAutoresizingMask:mask];
				
				// Adjust frame appropriately.
				NSRect frame = [_accessoryView frame];
				frame.origin.x = round(NSMaxX([self bounds]) - (frame.size.width + SCOPE_BAR_H_INSET));
				frame.origin.y = round(((SCOPE_BAR_HEIGHT - frame.size.height) / 2.0));
				[_accessoryView setFrame:frame];
				
				// Add as subview.
				[self addSubview:_accessoryView];
			}
		}
		
		// Layout subviews appropriately.
		[self adjustSubviews];
	}
	
	[self setNeedsDisplay:YES];
}


#pragma mark Utility methods


- (void)sizeToFit
{
	NSRect frame = [self frame];
	if (frame.size.height != SCOPE_BAR_HEIGHT) {
		float delta = SCOPE_BAR_HEIGHT - frame.size.height;
		frame.size.height += delta;
		frame.origin.y -= delta;
		[self setFrame:frame];
	}
}


- (void)adjustSubviews
{
	if (!_smartResizeEnabled) {
		return;
	}
	
	/*
	 We need to work out which groups we can show fully expanded, and which must be collapsed into popup-buttons.
	 Any kind of frame-change may have happened, so we need to take care to create or remove buttons or popup-buttons as needed.
	*/
	
	// Bail out if we have nothing to do.
	if (!_groups || [_groups count] == 0) {
		return;
	}
	
	// Obtain current width of view.
	float viewWidth = [self bounds].size.width;
	
	// Abort if there hasn't been any genuine change in width.
	if ((viewWidth == _lastWidth) && (_lastWidth != NSNotFound)) {
		return;
	}
	
	// Determine whether we got narrower or wider.
	float narrower = ((_lastWidth == NSNotFound) || (viewWidth < _lastWidth));
	
	// Find width available for showing groups.
	float availableWidth = viewWidth - (SCOPE_BAR_H_INSET * 2.0);
	if (_accessoryView) {
		// Account for _accessoryView, leaving a normal amount of spacing to the left of it.
		availableWidth -= ([_accessoryView frame].size.width + SCOPE_BAR_ITEM_SPACING);
	}
	
	BOOL shouldAdjustPopups = (availableWidth < _totalGroupsWidthForPopups);
	NSInteger oldFirstCollapsedGroup = _firstCollapsedGroup;
	
	// Work out which groups we should now check for collapsibility/expandability.
	NSEnumerator *groupsEnumerator = nil;
	NSRange enumRange;
	BOOL proceed = YES;
	
	if (narrower) {
		// Got narrower, so work backwards from oldFirstCollapsedGroup (excluding that group, since it's already collapsed),
		// checking to see if we need to collapse any more groups to the left.
		enumRange = NSMakeRange(0, oldFirstCollapsedGroup);
		// If no groups were previously collapsed, work backwards from the last group (including that group).
		if (oldFirstCollapsedGroup == NSNotFound) {
			enumRange.length = [_groups count];
		}
		groupsEnumerator = [[_groups subarrayWithRange:enumRange] reverseObjectEnumerator];
		
	} else {
		// Got wider, so work forwards from oldFirstCollapsedGroup (including that group) checking to see if we can 
		// expand any groups into full buttons.
		enumRange = NSMakeRange(oldFirstCollapsedGroup, [_groups count] - oldFirstCollapsedGroup);
		// If no groups were previously collapsed, we have nothing to do here.
		if (oldFirstCollapsedGroup == NSNotFound) {
			proceed = NO;
		}
		if (proceed) {
			groupsEnumerator = [[_groups subarrayWithRange:enumRange] objectEnumerator];
		}
	}
	
	// Get the current occupied width within this view.
	float currentOccupiedWidth = 0;
	NSDictionary *group = [_groups objectAtIndex:0];
	BOOL menuMode = [[group objectForKey:GROUP_MENU_MODE] boolValue];
	NSButton *firstButton = nil;
	if (menuMode) {
		firstButton = [group objectForKey:GROUP_POPUP_BUTTON];
	} else {
		firstButton = [[group objectForKey:GROUP_BUTTONS] objectAtIndex:0];
	}
	float leftLimit = NSMinX([firstButton frame]);
	// Account for label in first group, if present.
	if ([[group objectForKey:GROUP_HAS_LABEL] boolValue]) {
		NSTextField *label = (NSTextField *)[group objectForKey:GROUP_LABEL_FIELD];
		leftLimit -= (SCOPE_BAR_ITEM_SPACING + [label frame].size.width);
	}
	
	group = [_groups lastObject];
	menuMode = [[group objectForKey:GROUP_MENU_MODE] boolValue];
	NSButton *lastButton = nil;
	if (menuMode) {
		lastButton = [group objectForKey:GROUP_POPUP_BUTTON];
	} else {
		lastButton = [[group objectForKey:GROUP_BUTTONS] lastObject];
	}
	float rightLimit = NSMaxX([lastButton frame]);
	currentOccupiedWidth = rightLimit - leftLimit;
	
	// Work out whether we need to try collapsing groups at all, if we're narrowing.
	// We have already handled the case of not requiring to expand groups if we're widening, above.
	if (proceed && narrower) {
		if (availableWidth >= currentOccupiedWidth) {
			// We still have enough room for what we're showing; no change needed.
			proceed = NO;
		}
	}
	
	if (proceed) {
		// Disable screen updates.
		NSDisableScreenUpdates();
		
		// See how many further groups we can expand or contract.
		float theoreticalOccupiedWidth = currentOccupiedWidth;
		for (NSDictionary *groupInfo in groupsEnumerator) {
			BOOL complete = NO;
			float expandedWidth = [[groupInfo objectForKey:GROUP_TOTAL_BUTTONS_WIDTH] floatValue];
			float contractedWidth = [[groupInfo objectForKey:GROUP_WIDEST_BUTTON_WIDTH] floatValue] + MENU_PADDING;
			
			if (narrower) {
				// We're narrowing. See if collapsing this group brings us within availableWidth.
				if (((theoreticalOccupiedWidth - expandedWidth) + contractedWidth) <= availableWidth) {
					// We're now within width constraints, so we're done iterating.
					complete = YES;
				} // else, continue trying to to collapse groups.
				theoreticalOccupiedWidth = ((theoreticalOccupiedWidth - expandedWidth) + contractedWidth);
				
			} else {
				// We're widening. See if we can expand this group and still be within availableWidth.
				if (((theoreticalOccupiedWidth - contractedWidth) + expandedWidth) > availableWidth) {
					// We'd be too wide if we expanded this group. Terminate iteration without updating _firstCollapsedGroup.
					//DLog(@"We'd be too wide if we expanded right now");
					break;
				} // else, continue trying to expand groups.
				theoreticalOccupiedWidth = ((theoreticalOccupiedWidth - contractedWidth) + expandedWidth);
				//DLog(@"We can continue expanding");
			}
			
			// Update _firstCollapsedGroup appropriately.
			if (_firstCollapsedGroup == NSNotFound) {
				_firstCollapsedGroup = ((narrower) ? [_groups count] : -1);
				oldFirstCollapsedGroup = _firstCollapsedGroup;
			}
			_firstCollapsedGroup += ((narrower) ? -1 : 1);
			
			// Terminate if we now fit the available space as best we can.
			if (complete) {
				break;
			}
		}
		
		// Work out how many groups we need to actually change.
		NSRange changedRange = NSMakeRange(0, [_groups count]);
		BOOL adjusting = YES;
		//DLog(@"Old firstCollapsedGroup: %d, new: %d", oldFirstCollapsedGroup, _firstCollapsedGroup);
		if (_firstCollapsedGroup != oldFirstCollapsedGroup) {
			if (narrower) {
				// Narrower. _firstCollapsedGroup will be less (earlier) than oldFirstCollapsedGroup.
				changedRange.location = _firstCollapsedGroup;
				changedRange.length = (oldFirstCollapsedGroup - _firstCollapsedGroup);
			} else {
				// Wider. _firstCollapsedGroup will be greater (later) than oldFirstCollapsedGroup.
				changedRange.location = oldFirstCollapsedGroup;
				changedRange.length = (_firstCollapsedGroup - oldFirstCollapsedGroup);
			}
		} else {
			// _firstCollapsedGroup and oldFirstCollapsedGroup are the same; nothing needs changed.
			adjusting = NO;
		}
		
		// If a change is required, ensure that each group is expanded or contracted as appropriate.
		if (adjusting || shouldAdjustPopups) {
			//DLog(@"Got %@ - modifying groups %@", ((narrower) ? @"narrower" : @"wider"), NSStringFromRange(changedRange));
			NSInteger nextXCoord = NSNotFound;
			if (adjusting) {
				int i;
				for (i = changedRange.location; i < NSMaxRange(changedRange); i++) {
					NSMutableDictionary *groupInfo = [_groups objectAtIndex:i];
					
					if (nextXCoord == NSNotFound) {
						BOOL menuMode = [[groupInfo objectForKey:GROUP_MENU_MODE] boolValue];
						NSButton *firstButton = nil;
						if (!menuMode) {
							firstButton = [[groupInfo objectForKey:GROUP_BUTTONS] objectAtIndex:0];
						} else {
							firstButton = [groupInfo objectForKey:GROUP_POPUP_BUTTON];
						}
						nextXCoord = [firstButton frame].origin.x;
					} else {
						// Add group-spacing, separator and label as appropriate.
						nextXCoord += SCOPE_BAR_ITEM_SPACING;
						if ([[groupInfo objectForKey:GROUP_HAS_SEPARATOR] boolValue]) {
							nextXCoord += (SCOPE_BAR_SEPARATOR_WIDTH + SCOPE_BAR_ITEM_SPACING);
						}
						if ([[groupInfo objectForKey:GROUP_HAS_LABEL] boolValue]) {
							NSTextField *labelField = (NSTextField *)[groupInfo objectForKey:GROUP_LABEL_FIELD];
							float labelWidth = [labelField frame].size.width;
							nextXCoord += (labelWidth + SCOPE_BAR_ITEM_SPACING);
						}
					}
					
					NSPopUpButton *popup = nil;
					if (narrower) {
						// Remove buttons.
						NSArray *buttons = [groupInfo objectForKey:GROUP_BUTTONS];
						[buttons makeObjectsPerformSelector:@selector(removeFromSuperview)];
						
						// Create popup and add it to this view.
						popup = [self popupButtonForGroup:groupInfo];
						NSRect popupFrame = [popup frame];
						popupFrame.origin.x = nextXCoord;
						[popup setFrame:popupFrame];
						[groupInfo setObject:popup forKey:GROUP_POPUP_BUTTON];
						[self addSubview:popup positioned:NSWindowBelow relativeTo:_accessoryView];
						nextXCoord += popupFrame.size.width;
						
						// Ensure popup has appropriate title.
						[self updateMenuTitleForGroupAtIndex:i];
						
					} else {
						// Remove and release popup.
						popup = [groupInfo objectForKey:GROUP_POPUP_BUTTON];
						[popup removeFromSuperview];
						[groupInfo removeObjectForKey:GROUP_POPUP_BUTTON];
						
						// Replace menuItems with buttons.
						float buttonX = nextXCoord;
						NSMutableArray *menuItems = [groupInfo objectForKey:GROUP_BUTTONS];
						NSArray *selectedItems = [_selectedItems objectAtIndex:i];
						int i;
						for (i = 0; i < [menuItems count]; i++) {
							NSMenuItem *menuItem = [menuItems objectAtIndex:i];
							NSString *itemIdentifier = [menuItem representedObject];
							NSButton *button = [self buttonForItem:itemIdentifier 
														   inGroup:[menuItem tag] 
														 withTitle:[menuItem title] 
															 image:[menuItem image]];
							NSRect buttonFrame = [button frame];
							buttonFrame.origin.x = buttonX;
							[button setFrame:buttonFrame];
							if ([selectedItems containsObject:itemIdentifier]) {
								[button setState:NSOnState];
							}
							[self addSubview:button positioned:NSWindowBelow relativeTo:_accessoryView];
							[menuItems replaceObjectAtIndex:i withObject:button];
							buttonX += [button frame].size.width + SCOPE_BAR_ITEM_SPACING;
						}
						nextXCoord = (buttonX - SCOPE_BAR_ITEM_SPACING);
					}
					
					// Update GROUP_MENU_MODE for this group.
					[groupInfo setObject:[NSNumber numberWithBool:narrower] forKey:GROUP_MENU_MODE];
				}
			}
			
			// Modify positions/sizes of groups and separators as required.
			float startIndex = MIN(changedRange.location, _firstCollapsedGroup);
			float xCoord = 0;
			float perGroupDelta = 0;
			if (shouldAdjustPopups) {
				perGroupDelta = ((_totalGroupsWidthForPopups - availableWidth) / [_groups count]);
			}
			int i;
			for (i = startIndex; i < [_groups count]; i++) {
				NSDictionary *groupInfo = [_groups objectAtIndex:i];
				BOOL menuMode = [[groupInfo objectForKey:GROUP_MENU_MODE] boolValue];
				
				// Further contract or expand popups if appropriate.
				if (shouldAdjustPopups) {
					float fullPopupWidth = [[groupInfo objectForKey:GROUP_WIDEST_BUTTON_WIDTH] floatValue] + MENU_PADDING;
					float popupWidth = fullPopupWidth - perGroupDelta;
					popupWidth = MAX(popupWidth, MENU_MIN_WIDTH);
					popupWidth = MIN(popupWidth, fullPopupWidth);
					
					NSPopUpButton *button = [groupInfo objectForKey:GROUP_POPUP_BUTTON];
					NSRect buttonRect = [button frame];
					buttonRect.size.width = popupWidth;
					[button setFrame:buttonRect];
				}
				
				// Reposition groups appropriately.
				if (i > startIndex) {
					// Reposition separator if present.
					if ([[groupInfo objectForKey:GROUP_HAS_SEPARATOR] boolValue]) {
						[_separatorPositions replaceObjectAtIndex:i withObject:[NSNumber numberWithInt:xCoord]];
						xCoord += (SCOPE_BAR_SEPARATOR_WIDTH + SCOPE_BAR_ITEM_SPACING);
					}
					
					// Reposition label if present.
					if ([[groupInfo objectForKey:GROUP_HAS_LABEL] boolValue]) {
						NSTextField *label = [groupInfo objectForKey:GROUP_LABEL_FIELD];
						NSRect labelFrame = [label frame];
						labelFrame.origin.x = xCoord;
						[label setFrame:labelFrame];
						xCoord = NSMaxX(labelFrame) + SCOPE_BAR_ITEM_SPACING;
					}
					
					// Reposition buttons or popup.
					if (menuMode) {
						NSPopUpButton *button = [groupInfo objectForKey:GROUP_POPUP_BUTTON];
						NSRect buttonRect = [button frame];
						buttonRect.origin.x = xCoord;
						[button setFrame:buttonRect];
						xCoord = NSMaxX(buttonRect) + SCOPE_BAR_ITEM_SPACING;
						
					} else {
						NSArray *buttons = [groupInfo objectForKey:GROUP_BUTTONS];
						for (NSButton *button in buttons) {
							NSRect buttonRect = [button frame];
							buttonRect.origin.x = xCoord;
							[button setFrame:buttonRect];
							xCoord = NSMaxX(buttonRect) + SCOPE_BAR_ITEM_SPACING;
						}
					}
					
				} else {
					// Set up initial value of xCoord.
					NSButton *button = nil;
					if (menuMode) {
						button = [groupInfo objectForKey:GROUP_POPUP_BUTTON];
					} else {
						button = [[groupInfo objectForKey:GROUP_BUTTONS] lastObject];
					}
					xCoord = NSMaxX([button frame]) + SCOPE_BAR_ITEM_SPACING;
				}
			}
			
			// Reset _firstCollapsedGroup to NSNotFound if necessary.
			if (!narrower) {
				if (_firstCollapsedGroup >= [_groups count]) {
					_firstCollapsedGroup = NSNotFound;
				}
			}
		}
		
		// Re-enable screen updates.
		NSEnableScreenUpdates();
	}
	
	// Take note of our width for comparison next time.
	_lastWidth = viewWidth;
}


- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize
{
	[super resizeSubviewsWithOldSize:oldBoundsSize];
	[self adjustSubviews];
}


- (NSButton *)getButtonForItem:(NSString *)identifier inGroup:(int)groupNumber
{
	NSButton *button = nil;
	NSArray *group = [_identifiers objectForKey:identifier];
	if (group && [group count] > groupNumber) {
		NSObject *element = [group objectAtIndex:groupNumber];
		if (element != [NSNull null]) {
			button = (NSButton *)element;
		}
	}
	
	return button;
}


- (NSButton *)buttonForItem:(NSString *)identifier inGroup:(int)groupNumber 
				  withTitle:(NSString *)title image:(NSImage *)image
{
	NSRect ctrlRect = NSMakeRect(0, 0, 50, 20); // arbitrary size; will be resized later.
	NSButton *button = [[NSButton alloc] initWithFrame:ctrlRect];
	[button setTitle:title];
	[[button cell] setRepresentedObject:identifier];
	[button setTag:groupNumber];
	[button setFont:[NSFont boldSystemFontOfSize:SCOPE_BAR_FONTSIZE]];
	[button setTarget:self];
	[button setAction:@selector(scopeButtonClicked:)];
	[button setBezelStyle:NSRecessedBezelStyle];
	[button setButtonType:NSPushOnPushOffButton];
	[[button cell] setHighlightsBy:NSCellIsBordered | NSCellIsInsetButton];
	[button setShowsBorderOnlyWhileMouseInside:YES];
	if (image) {
		[image setSize:NSMakeSize(SCOPE_BAR_BUTTON_IMAGE_SIZE, SCOPE_BAR_BUTTON_IMAGE_SIZE)];
		[button setImagePosition:NSImageLeft];
		[button setImage:image];
	}
	[button sizeToFit];
	ctrlRect = [button frame];
	ctrlRect.origin.y = floor(([self frame].size.height - ctrlRect.size.height) / 2.0);
	[button setFrame:ctrlRect];
	
	[self setControl:button forIdentifier:identifier inGroup:groupNumber];
	
	return [button autorelease];
}


- (NSMenuItem *)menuItemForItem:(NSString *)identifier inGroup:(int)groupNumber 
					  withTitle:(NSString *)title image:(NSImage *)image
{
	NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:title action:@selector(scopeButtonClicked:) keyEquivalent:@""];
	[menuItem setTarget:self];
	[menuItem setImage:image];
	[menuItem setRepresentedObject:identifier];
	[menuItem setTag:groupNumber];
	
	[self setControl:menuItem forIdentifier:identifier inGroup:groupNumber];
	
	return [menuItem autorelease];
}


- (NSPopUpButton *)popupButtonForGroup:(NSDictionary *)group
{
	float popWidth = floor([[group objectForKey:GROUP_WIDEST_BUTTON_WIDTH] floatValue] + MENU_PADDING);
	NSRect popFrame = NSMakeRect(0, 0, popWidth, 20); // arbitrary height.
	NSPopUpButton *popup = [[NSPopUpButton alloc] initWithFrame:popFrame pullsDown:NO];
	
	// Since we're not using the selected item's title, we need to specify a NSMenuItem for the title.
	BOOL multiSelect = ([[group objectForKey:GROUP_SELECTION_MODE] intValue] == MGMultipleSelectionMode);
	if (multiSelect) {
		MGRecessedPopUpButtonCell *cell = [[MGRecessedPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO];
		[popup setCell:cell];
		[cell release];
		
		[[popup cell] setUsesItemFromMenu:NO];
		NSMenuItem *titleItem = [[NSMenuItem alloc] init];
		[[popup cell] setMenuItem:titleItem];
		[titleItem release];
	}
	
	// Configure appearance and behaviour.
	[popup setFont:[NSFont boldSystemFontOfSize:SCOPE_BAR_FONTSIZE]];
	[popup setBezelStyle:NSRecessedBezelStyle];
	[popup setButtonType:NSPushOnPushOffButton];
	[[popup cell] setHighlightsBy:NSCellIsBordered | NSCellIsInsetButton];
	[popup setShowsBorderOnlyWhileMouseInside:NO];
	[[popup cell] setAltersStateOfSelectedItem:NO];
	[[popup cell] setArrowPosition:NSPopUpArrowAtBottom];
	[popup setPreferredEdge:NSMaxXEdge];
	
	// Add appropriate items.
	[popup removeAllItems];
	NSMutableArray *buttons = [group objectForKey:GROUP_BUTTONS];
	int i;
	for (i = 0; i < [buttons count]; i++) {
		NSButton *button = (NSButton *)[buttons objectAtIndex:i];
		NSMenuItem *menuItem = [self menuItemForItem:[[button cell] representedObject] 
											 inGroup:[button tag] 
										   withTitle:[button title] 
											   image:[button image]];
		[menuItem setState:[button state]];
		[buttons replaceObjectAtIndex:i withObject:menuItem];
		[[popup menu] addItem:menuItem];
	}
	
	// Vertically center the popup within our frame.
	if (!multiSelect) {
		[popup sizeToFit];
	}
	popFrame = [popup frame];
	popFrame.origin.y = ceil(([self frame].size.height - popFrame.size.height) / 2.0);
	[popup setFrame:popFrame];
	
	return [popup autorelease];
}


- (void)setControl:(NSObject *)control forIdentifier:(NSString *)identifier inGroup:(int)groupNumber
{
	if (!_identifiers) {
		_identifiers = [[NSMutableDictionary alloc] initWithCapacity:0];
	}
	
	NSMutableArray *identArray = [_identifiers objectForKey:identifier];
	if (!identArray) {
		identArray = [[[NSMutableArray alloc] initWithCapacity:groupNumber + 1] autorelease];
		[_identifiers setObject:identArray forKey:identifier];
	}
	
	int count = [identArray count];
	if (groupNumber >= count) {
		// Pad identArray with nulls if appropriate, so this control lies at index groupNumber.
		int i;
		for (i = count; i < groupNumber; i++) {
			[identArray addObject:[NSNull null]];
		}
		[identArray addObject:control];
	} else {
		[identArray replaceObjectAtIndex:groupNumber withObject:control];
	}
}


- (void)updateMenuTitleForGroupAtIndex:(int)groupNumber
{
	// Ensure that this group's popup (if present) has the correct title,
	// accounting for the group's selection-mode and selected item(s).
	
	if (groupNumber < 0 || groupNumber >= [_groups count]) {
		return;
	}
	
	NSDictionary *group = [_groups objectAtIndex:groupNumber];
	if (group) {
		NSPopUpButton *popup = [group objectForKey:GROUP_POPUP_BUTTON];
		if (popup) {
			NSArray *groupSelection = [_selectedItems objectAtIndex:groupNumber];
			int numSelected = [groupSelection count];
			if (numSelected == 0) {
				// No items selected.
				[popup setTitle:POPUP_TITLE_EMPTY_SELECTION];
				[[[popup cell] menuItem] setImage:nil];
				
			} else if (numSelected > 1) {
				// Multiple items selected.
				[popup setTitle:POPUP_TITLE_MULTIPLE_SELECTION];
				[[[popup cell] menuItem] setImage:nil];
				
			} else {
				// One item selected.
				NSString *identifier = [groupSelection objectAtIndex:0];
				NSArray *items = [group objectForKey:GROUP_BUTTONS];
				NSMenuItem *item = nil;
				for (NSMenuItem *thisItem in items) {
					if ([[thisItem representedObject] isEqualToString:identifier]) {
						item = thisItem;
						break;
					}
				}
				if (item) {
					[popup setTitle:[item title]];
					[[[popup cell] menuItem] setImage:[item image]];
				}
			}
			
			if (SCOPE_BAR_HIDE_POPUP_BG) {
				BOOL hasBackground = [[popup cell] isBordered];
				if (numSelected == 0 && hasBackground) {
					[[popup cell] setBordered:NO];
				} else if (!hasBackground) {
					[[popup cell] setBordered:YES];
				}
			}
		}
	}
}


#pragma mark Drawing


- (void)drawRect:(NSRect)rect
{
    // Draw gradient background.
	NSGradient *gradient = [[[NSGradient alloc] initWithStartingColor:SCOPE_BAR_START_COLOR_GRAY 
														  endingColor:SCOPE_BAR_END_COLOR_GRAY] autorelease];
	[gradient drawInRect:[self bounds] angle:90.0];
	
	// Draw border.
	NSRect lineRect = [self bounds];
	lineRect.size.height = SCOPE_BAR_BORDER_WIDTH;
	[SCOPE_BAR_BORDER_COLOR set];
	NSRectFill(lineRect);
	
	// Draw separators.
	if ([_separatorPositions count] > 0) {
		[SCOPE_BAR_SEPARATOR_COLOR set];
		NSRect sepRect = NSMakeRect(0, 0, SCOPE_BAR_SEPARATOR_WIDTH, SCOPE_BAR_SEPARATOR_HEIGHT);
		sepRect.origin.y = (([self bounds].size.height - sepRect.size.height) / 2.0);
		for (NSObject *sepPosn in _separatorPositions) {
			if (sepPosn != [NSNull null]) {
				sepRect.origin.x = [(NSNumber *)sepPosn intValue];
				NSRectFill(sepRect);
			}
		}
	}
}


#pragma mark Interaction


- (IBAction)scopeButtonClicked:(id)sender
{
	NSButton *button = (NSButton *)sender;
	BOOL menuMode = [sender isKindOfClass:[NSMenuItem class]];
	NSString *identifier = [((menuMode) ? sender : [sender cell]) representedObject];
	int groupNumber = [sender tag];
	BOOL nowSelected = YES;
	if (menuMode) {
		// MenuItem. Ensure item has appropriate state.
		nowSelected = ![[_selectedItems objectAtIndex:groupNumber] containsObject:identifier];
		[sender setState:((nowSelected) ? NSOnState : NSOffState)];
	} else {
		// Button. Item will already have appropriate state.
		nowSelected = ([button state] != NSOffState);
	}
	[self setSelected:nowSelected forItem:identifier inGroup:groupNumber];
}


#pragma mark Accessors and properties


- (void)setSelected:(BOOL)selected forItem:(NSString *)identifier inGroup:(int)groupNumber
{
	// Change state of other items in group appropriately, informing delegate if possible.
	// First we find the appropriate group-info for the item's identifier.
	if (identifier && groupNumber >= 0 && groupNumber < [_groups count]) {
		NSDictionary *group = [_groups objectAtIndex:groupNumber];
		BOOL nowSelected = selected;
		BOOL informDelegate = YES;
		
		if (group) {
			[group retain];
			NSDisableScreenUpdates();
			
			// We found the group which this item belongs to. Obtain selection-mode and identifiers.
			MGScopeBarGroupSelectionMode selMode = [[group objectForKey:GROUP_SELECTION_MODE] intValue];
			BOOL radioMode = (selMode == MGRadioSelectionMode);
			
			if (radioMode) {
				// This is a radio-mode group. Ensure this item isn't already selected.
				NSArray *groupSelections = [[_selectedItems objectAtIndex:groupNumber] copy];
				
				if (nowSelected) {
					// Before selecting this item, we first need to deselect any other selected items in this group.
					for (NSString *selectedIdentifier in groupSelections) {
						// Reselect the just-deselected item without informing the delegate, since nothing really changed.
						[self updateSelectedState:NO forItem:selectedIdentifier inGroup:groupNumber informDelegate:NO];
					}
				} else {
					// Prevent deselection if this item is already selected.
					if ([groupSelections containsObject:identifier]) {
						nowSelected = YES;
						informDelegate = NO;
					}
				}
				[groupSelections release];	
			}
			
			// Change selected state of this item.
			[self updateSelectedState:nowSelected forItem:identifier inGroup:groupNumber informDelegate:informDelegate];
			
			// Update popup-menu's title if appropriate.
			if ([[group objectForKey:GROUP_MENU_MODE] boolValue]) {
				[self updateMenuTitleForGroupAtIndex:groupNumber];
			}
			
			[group release];
			NSEnableScreenUpdates();
		}
	}
}


- (void)updateSelectedState:(BOOL)selected forItem:(NSString *)identifier inGroup:(int)groupNumber informDelegate:(BOOL)inform
{
	// This method simply updates the selected state of the item's control, maintains selectedItems, and informs the delegate.
	// All management of dependencies (such as deselecting other selected items in a radio-selection-mode group) is performed 
	// in the setSelected:forItem:inGroup: method.
	
	// Determine whether we can inform the delegate about this change.
	SEL stateChangedSel = @selector(scopeBar:selectedStateChanged:forItem:inGroup:);
	BOOL responds = (delegate && [delegate respondsToSelector:stateChangedSel]);
	
	// Ensure selected status of item's control reflects desired value.
	NSButton *button = [self getButtonForItem:identifier inGroup:groupNumber];
	if (selected && [button state] == NSOffState) {
		[button setState:NSOnState];
	} else if (!selected && [button state] != NSOffState) {
		[button setState:NSOffState];
	}
	
	// Maintain _selectedItems appropriately.
	if (_selectedItems && [_selectedItems count] > groupNumber) {
		NSMutableArray *groupSelections = [_selectedItems objectAtIndex:groupNumber];
		BOOL alreadySelected = [groupSelections containsObject:identifier];
		if (selected && !alreadySelected) {
			[groupSelections addObject:identifier];
		} else if (!selected && alreadySelected) {
			[groupSelections removeObject:identifier];
		}
	}
	
	// Inform delegate about this change if possible.
	if (inform && responds) {
		[delegate scopeBar:self selectedStateChanged:selected forItem:identifier inGroup:groupNumber];
	}
}


- (NSArray *)selectedItems
{
	return [[_selectedItems copy] autorelease];
}


- (void)setDelegate:(id)newDelegate
{
	if (delegate != newDelegate) {
		delegate = newDelegate;
		[self reloadData];
	}
}


- (BOOL)smartResizeEnabled
{
	return _smartResizeEnabled;
}


- (void)setSmartResizeEnabled:(BOOL)enabled
{
	if (enabled != _smartResizeEnabled) {
		_smartResizeEnabled = enabled;
		[self reloadData];
	}
}


@synthesize delegate;


@end
