Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

1069 lines (895 sloc) 40.73 kb
//
// MGScopeBar.m
// MGScopeBar
//
// Copyright (c) 2008 Matt Gemmell
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
// OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
// EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
#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:(NSInteger)groupNumber; // returns relevant button/menu-item
- (void)updateSelectedState:(BOOL)selected forItem:(NSString *)identifier inGroup:(NSInteger)groupNumber informDelegate:(BOOL)inform;
- (NSButton *)buttonForItem:(NSString *)identifier inGroup:(NSInteger)groupNumber
withTitle:(NSString *)title image:(NSImage *)image; // creates a new NSButton
- (NSMenuItem *)menuItemForItem:(NSString *)identifier inGroup:(NSInteger)groupNumber
withTitle:(NSString *)title image:(NSImage *)image; // creates a new NSMenuitem
- (NSPopUpButton *)popupButtonForGroup:(NSDictionary *)group;
- (void)setControl:(NSObject *)control forIdentifier:(NSString *)identifier inGroup:(NSInteger)groupNumber;
- (void)updateMenuTitleForGroupAtIndex:(NSUInteger)groupNumber;
@end
@implementation MGScopeBar
@synthesize delegate;
#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:)];
for (int 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 systemFontOfSize: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.
//NSLog(@"We'd be too wide if we expanded right now");
break;
} // else, continue trying to expand groups.
theoreticalOccupiedWidth = ((theoreticalOccupiedWidth - contractedWidth) + expandedWidth);
//NSLog(@"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;
//NSLog(@"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) {
//NSLog(@"Got %@ - modifying groups %@", ((narrower) ? @"narrower" : @"wider"), NSStringFromRange(changedRange));
NSInteger nextXCoord = NSNotFound;
if (adjusting) {
for (NSUInteger 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];
for (int 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]);
}
for (int 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:(NSInteger)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:(NSInteger)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 systemFontOfSize: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:(NSInteger)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 systemFontOfSize: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];
for (int 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:(NSInteger)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];
}
NSUInteger count = [identArray count];
if (groupNumber >= count) {
// Pad identArray with nulls if appropriate, so this control lies at index groupNumber.
for (NSUInteger i = count; i < groupNumber; i++) {
[identArray addObject:[NSNull null]];
}
[identArray addObject:control];
} else {
[identArray replaceObjectAtIndex:groupNumber withObject:control];
}
}
- (void)updateMenuTitleForGroupAtIndex:(NSUInteger)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 >= [_groups count]) {
return;
}
NSDictionary *group = [_groups objectAtIndex:groupNumber];
if (group) {
NSPopUpButton *popup = [group objectForKey:GROUP_POPUP_BUTTON];
if (popup) {
NSArray *groupSelection = [_selectedItems objectAtIndex:groupNumber];
NSUInteger 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];
NSInteger 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:(NSInteger)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:(NSInteger)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];
}
- (BOOL) isItemSelectedWithIdentifier:(NSString*)identifier inGroup:(NSInteger)groupNumber;
{
NSArray *identifiers = [_selectedItems objectAtIndex:groupNumber];
return [identifiers containsObject:identifier];
}
- (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];
}
}
@end
Jump to Line
Something went wrong with that request. Please try again.