Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial checkin of MGScopeBar.

git-svn-id: http://svn.cocoasourcecode.com/MGScopeBar@19 fad7f400-0e54-0410-bf1d-e368f886e4d4
  • Loading branch information...
commit 9e5d11f54d65cbdf1918bf945ac7a07a333630c9 0 parents
mattgemmell authored
20 AppController.h
@@ -0,0 +1,20 @@
+//
+// AppController.h
+// MGScopeBar
+//
+// Created by Matt Gemmell on 16/03/2008.
+//
+
+#import <Cocoa/Cocoa.h>
+#import "MGScopeBarDelegateProtocol.h"
+
+@interface AppController : NSObject <MGScopeBarDelegate> {
+ IBOutlet NSTextField *labelField;
+ IBOutlet MGScopeBar *scopeBar;
+ IBOutlet NSView *accessoryView;
+ NSMutableArray *groups;
+}
+
+@property(retain) NSMutableArray *groups;
+
+@end
215 AppController.m
@@ -0,0 +1,215 @@
+//
+// AppController.m
+// MGScopeBar
+//
+// Created by Matt Gemmell on 16/03/2008.
+//
+
+#import "AppController.h"
+#import "MGScopeBar.h"
+
+
+// Keys for our sample data.
+#define GROUP_LABEL @"Label" // string
+#define GROUP_SEPARATOR @"HasSeparator" // BOOL as NSNumber
+#define GROUP_SELECTION_MODE @"SelectionMode" // MGScopeBarGroupSelectionMode (int) as NSNumber
+#define GROUP_ITEMS @"Items" // array of dictionaries, each containing the following keys:
+#define ITEM_IDENTIFIER @"Identifier" // string
+#define ITEM_NAME @"Name" // string
+
+
+@implementation AppController
+
+
+#pragma mark Setup and teardown
+
+
+- (void)awakeFromNib
+{
+ // In this method we basically just set up some sample data for the scope bar,
+ // so that we can respond to the MGScopeBarDelegate methods easily.
+
+ self.groups = [NSMutableArray arrayWithCapacity:0];
+ scopeBar.delegate = self;
+
+ // Add first group of items.
+ NSArray *items = [NSArray arrayWithObjects:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"HereItem", ITEM_IDENTIFIER,
+ @"Here", ITEM_NAME,
+ nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"ThereItem", ITEM_IDENTIFIER,
+ @"There", ITEM_NAME,
+ nil],
+ nil];
+
+ [self.groups addObject:[NSDictionary dictionaryWithObjectsAndKeys:
+ @"Search:", GROUP_LABEL,
+ [NSNumber numberWithBool:NO], GROUP_SEPARATOR,
+ [NSNumber numberWithInt:MGRadioSelectionMode], GROUP_SELECTION_MODE, // single selection group.
+ items, GROUP_ITEMS,
+ nil]];
+
+ // Add second group of items.
+ items = [NSArray arrayWithObjects:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"ContentsItem", ITEM_IDENTIFIER,
+ @"Contents", ITEM_NAME,
+ nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"FileNamesItem", ITEM_IDENTIFIER,
+ @"File Names", ITEM_NAME,
+ nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"MetadataItem", ITEM_IDENTIFIER,
+ @"Metadata", ITEM_NAME,
+ nil],
+ nil];
+
+ [self.groups addObject:[NSDictionary dictionaryWithObjectsAndKeys:
+ // deliberately not specifying a label
+ [NSNumber numberWithBool:YES], GROUP_SEPARATOR,
+ [NSNumber numberWithInt:MGMultipleSelectionMode], GROUP_SELECTION_MODE, // multiple selection group.
+ items, GROUP_ITEMS,
+ nil]];
+
+ // Add third group of items.
+ items = [NSArray arrayWithObjects:
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"AllFilesItem", ITEM_IDENTIFIER,
+ @"All Files", ITEM_NAME,
+ nil],
+ [NSDictionary dictionaryWithObjectsAndKeys:
+ @"ImagesOnlyItem", ITEM_IDENTIFIER,
+ @"Images Only", ITEM_NAME,
+ nil],
+ nil];
+
+ [self.groups addObject:[NSDictionary dictionaryWithObjectsAndKeys:
+ @"Kind:", GROUP_LABEL,
+ [NSNumber numberWithBool:YES], GROUP_SEPARATOR,
+ [NSNumber numberWithInt:MGRadioSelectionMode], GROUP_SELECTION_MODE, // single selection group.
+ items, GROUP_ITEMS,
+ nil]];
+
+ // Tell the scope bar to ask us for data (since we're the scope-bar's delegate).
+ [scopeBar reloadData];
+
+ // Since our first group is a radio-mode group, the scope bar will automatically select its first item.
+ // The scope bar will take care of deselecting other items when you select a new item in a radio-mode group.
+
+ // We'll also select the first item in our second group, which is a multiple-selection group.
+ // You can (and must) use this method to programmatically select/deselect items in the bar.
+ [scopeBar setSelected:YES forItem:@"ContentsItem" inGroup:1]; // remember that group-numbers are zero-based.
+
+ // Clear out the label field below the scope bar.
+ [labelField setStringValue:@""];
+}
+
+
+- (void)dealloc
+{
+ self.groups = nil;
+ [super dealloc];
+}
+
+
+#pragma mark MGScopeBarDelegate methods
+
+
+- (int)numberOfGroupsInScopeBar:(MGScopeBar *)theScopeBar
+{
+ return [self.groups count];
+}
+
+
+- (NSArray *)scopeBar:(MGScopeBar *)theScopeBar itemIdentifiersForGroup:(int)groupNumber
+{
+ return [[self.groups objectAtIndex:groupNumber] valueForKeyPath:[NSString stringWithFormat:@"%@.%@", GROUP_ITEMS, ITEM_IDENTIFIER]];
+}
+
+
+- (NSString *)scopeBar:(MGScopeBar *)theScopeBar labelForGroup:(int)groupNumber
+{
+ return [[self.groups objectAtIndex:groupNumber] objectForKey:GROUP_LABEL]; // might be nil, which is fine (nil means no label).
+}
+
+
+- (NSString *)scopeBar:(MGScopeBar *)theScopeBar titleOfItem:(NSString *)identifier inGroup:(int)groupNumber
+{
+ NSArray *items = [[self.groups objectAtIndex:groupNumber] objectForKey:GROUP_ITEMS];
+ if (items) {
+ // We'll iterate here, since this is just a demo. This avoids having to keep an NSDictionary of identifiers
+ // for each group as well as an array for ordering. In a more realistic scenario, you'd probably want to be
+ // able to look-up an item by its identifier in constant time.
+ for (NSDictionary *item in items) {
+ if ([[item objectForKey:ITEM_IDENTIFIER] isEqualToString:identifier]) {
+ return [item objectForKey:ITEM_NAME];
+ break;
+ }
+ }
+ }
+ return nil;
+}
+
+
+- (MGScopeBarGroupSelectionMode)scopeBar:(MGScopeBar *)theScopeBar selectionModeForGroup:(int)groupNumber
+{
+ return [[[self.groups objectAtIndex:groupNumber] objectForKey:GROUP_SELECTION_MODE] intValue];
+}
+
+
+- (BOOL)scopeBar:(MGScopeBar *)theScopeBar showSeparatorBeforeGroup:(int)groupNumber
+{
+ // Optional method. If not implemented, all groups except the first will have a separator before them.
+ return [[[self.groups objectAtIndex:groupNumber] objectForKey:GROUP_SEPARATOR] boolValue];
+}
+
+
+- (NSImage *)scopeBar:(MGScopeBar *)scopeBar imageForItem:(NSString *)identifier inGroup:(int)groupNumber
+{
+ // Optional method. If not implemented (or if you return nil), items will not have an image.
+ if (groupNumber == 0) {
+ return [NSImage imageNamed:@"NSComputer"];
+
+ } else if (groupNumber == 2) {
+ if ([identifier isEqualToString:@"AllFilesItem"]) {
+ return [NSImage imageNamed:@"NSGenericDocument"];
+
+ } else if ([identifier isEqualToString:@"ImagesOnlyItem"]) {
+ return [[NSWorkspace sharedWorkspace] iconForFileType:@"png"];
+ }
+ }
+
+ return nil;
+}
+
+
+- (NSView *)accessoryViewForScopeBar:(MGScopeBar *)scopeBar
+{
+ // Optional method. If not implemented (or if you return nil), the scope-bar will not have an accessory view.
+ return accessoryView;
+}
+
+
+- (void)scopeBar:(MGScopeBar *)theScopeBar selectedStateChanged:(BOOL)selected
+ forItem:(NSString *)identifier inGroup:(int)groupNumber
+{
+ // Display some text showing what just happened.
+ NSString *displayString = [NSString stringWithFormat:@"\"%@\" %@ in group %d.",
+ [self scopeBar:theScopeBar titleOfItem:identifier inGroup:groupNumber],
+ (selected) ? @"selected" : @"deselected",
+ groupNumber];
+ [labelField setStringValue:displayString];
+ //NSLog(@"%@", displayString);
+}
+
+
+#pragma mark Accessors and properties
+
+
+@synthesize groups;
+
+
+@end
BIN  English.lproj/InfoPlist.strings
Binary file not shown
3,278 English.lproj/MainMenu.xib
3,278 additions, 0 deletions not shown
28 Info.plist
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>com.yourcompany.${PRODUCT_NAME:identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
21 MGRecessedPopUpButtonCell.h
@@ -0,0 +1,21 @@
+//
+// MGRecessedPopUpButtonCell.h
+// MGScopeBar
+//
+// Created by Matt Gemmell on 20/03/2008.
+// Copyright 2008 Instinctive Code.
+//
+
+#import <Cocoa/Cocoa.h>
+
+/*
+ This cell class is used only for NSPopUpButtons which do NOT automatically
+ get their titles from their selected menu-items, since such popup-buttons
+ are weirdly broken when using the recessed bezel-style.
+*/
+
+@interface MGRecessedPopUpButtonCell : NSPopUpButtonCell {
+ NSButton *recessedButton; // we use a separate NSButton to do the bezel-drawing.
+}
+
+@end
54 MGRecessedPopUpButtonCell.m
@@ -0,0 +1,54 @@
+//
+// MGRecessedPopUpButtonCell.m
+// MGScopeBar
+//
+// Created by Matt Gemmell on 20/03/2008.
+// Copyright 2008 Instinctive Code.
+//
+
+#import "MGRecessedPopUpButtonCell.h"
+
+
+@implementation MGRecessedPopUpButtonCell
+
+
+- (id)initTextCell:(NSString *)title pullsDown:(BOOL)pullsDown
+{
+ if ((self = [super initTextCell:title pullsDown:pullsDown])) {
+ recessedButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 30, 20)]; // arbitrary frame.
+ [recessedButton setTitle:@""];
+ [recessedButton setBezelStyle:NSRecessedBezelStyle];
+ [recessedButton setButtonType:NSPushOnPushOffButton];
+ [[recessedButton cell] setHighlightsBy:NSCellIsBordered | NSCellIsInsetButton];
+ [recessedButton setShowsBorderOnlyWhileMouseInside:NO];
+ [recessedButton setState:NSOnState]; // ensures it looks pushed-in.
+ }
+ return self;
+}
+
+
+- (void)dealloc
+{
+ [recessedButton release];
+ [super dealloc];
+}
+
+
+- (void)drawTitleWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
+{
+ // Inset title rect since its position is broken when NSPopUpButton
+ // isn't using its selected item as its title.
+ NSRect titleFrame = cellFrame;
+ titleFrame.origin.y += 1.0;
+ [super drawTitleWithFrame:titleFrame inView:controlView];
+}
+
+
+- (void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView
+{
+ [recessedButton setFrame:frame];
+ [recessedButton drawRect:frame];
+}
+
+
+@end
51 MGScopeBar.h
@@ -0,0 +1,51 @@
+//
+// MGScopeBar.h
+// MGScopeBar
+//
+// Created by Matt Gemmell on 15/03/2008.
+// Copyright 2008 Instinctive Code.
+//
+
+#import <Cocoa/Cocoa.h>
+#import "MGScopeBarDelegateProtocol.h"
+
+@interface MGScopeBar : NSView {
+@private
+ IBOutlet id <MGScopeBarDelegate, NSObject> delegate; // weak ref.
+ NSMutableArray *_separatorPositions; // x-coords of separators, indexed by their group-number.
+ NSMutableArray *_groups; // groups of items.
+ NSView *_accessoryView; // weak ref since it's a subview.
+ NSMutableDictionary *_identifiers; // map of identifiers to items.
+ NSMutableArray *_selectedItems; // all selected items in all groups; see note below.
+ float _lastWidth; // previous width of view from when we last resized.
+ int _firstCollapsedGroup; // index of first group collapsed into a popup.
+ float _totalGroupsWidthForPopups; // total width needed to show all groups expanded (excluding padding and accessory).
+ float _totalGroupsWidth; // total width needed to show all groups as native-width popups (excluding padding and accessory).
+ BOOL _smartResizeEnabled; // whether to do our clever collapsing/expanding when resizing.
+}
+
+@property(assign) id delegate; // should implement the MGScopeBarDelegate protocol.
+
+- (void)reloadData; // causes the scope-bar to reload all groups/items from its delegate.
+- (void)sizeToFit; // only resizes vertically to optimum height; does not affect width.
+- (void)adjustSubviews; // performs Smart Resizing if enabled. You should only need to call this yourself if you change the width of the accessoryView.
+
+// Smart Resize is the intelligent conversion of button-groups into NSPopUpButtons and vice-versa, based on available space.
+// This functionality is enabled (YES) by default. Changing this setting will automatically call -reloadData.
+- (BOOL)smartResizeEnabled;
+- (void)setSmartResizeEnabled:(BOOL)enabled;
+
+// The following method must be used to manage selections in the scope-bar; do not attempt to manipulate buttons etc directly.
+- (void)setSelected:(BOOL)selected forItem:(NSString *)identifier inGroup:(int)groupNumber;
+- (NSArray *)selectedItems;
+
+/*
+ Note: The -selectedItems method returns an array of arrays.
+ Each index in the returned array represents the group of items at that index.
+ The contents of each sub-array are the identifiers of each selected item in that group.
+ Sub-arrays may be empty, but will always be present (i.e. you will always find an NSArray).
+ Depending on the group's selection-mode, sub-arrays may contain zero, one or many identifiers.
+ The identifiers in each sub-array are not in any particular order.
+ */
+
+@end
1,034 MGScopeBar.m
@@ -0,0 +1,1034 @@
+//
+// 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
+ }
+ [[self subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
+ [_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 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;
+ [_accessoryView setAutoresizingMask:mask];
+ }
+
+ // Add NSViewMinXMargin flag to resizing mask, if not present.
+ if (!(mask & NSViewMinXMargin)) {
+ mask = (mask | NSViewMinXMargin);
+ }
+
+ // 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);
+ int 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));
+ int nextXCoord = NSNotFound;
+ if (adjusting) {
+ for (int 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:(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];
+ 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:(int)groupNumber
+{
+ if (!_identifiers) {
+ _identifiers = [[NSMutableDictionary alloc] initWithCapacity:0];
+ }
+
+ NSMutableArray *identArray = [_identifiers objectForKey:identifier];
+ if (!identArray) {
+ identArray = [[NSMutableArray alloc] initWithCapacity:groupNumber + 1];
+ [_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.
+ for (int 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) {
+ 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];
+ }
+
+ 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
BIN  MGScopeBar.xcodeproj/TemplateIcon.icns
Binary file not shown
310 MGScopeBar.xcodeproj/project.pbxproj
@@ -0,0 +1,310 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 45;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1DDD58160DA1D0A300B32029 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1DDD58140DA1D0A300B32029 /* MainMenu.xib */; };
+ 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
+ 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
+ 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+ C95B43D00EB73CE3008EE468 /* MGRecessedPopUpButtonCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C95B43CF0EB73CE3008EE468 /* MGRecessedPopUpButtonCell.m */; };
+ C9FB61150E8FF8360019B961 /* AppController.m in Sources */ = {isa = PBXBuildFile; fileRef = C9FB61140E8FF8360019B961 /* AppController.m */; };
+ C9FB611A0E8FF8810019B961 /* MGScopeBar.m in Sources */ = {isa = PBXBuildFile; fileRef = C9FB61180E8FF8810019B961 /* MGScopeBar.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = "<absolute>"; };
+ 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
+ 1DDD58150DA1D0A300B32029 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = English.lproj/MainMenu.xib; sourceTree = "<group>"; };
+ 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = "<absolute>"; };
+ 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
+ 32CA4F630368D1EE00C91783 /* MGScopeBar_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGScopeBar_Prefix.pch; sourceTree = "<group>"; };
+ 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 8D1107320486CEB800E47090 /* MGScopeBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MGScopeBar.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ C95B43CE0EB73CE3008EE468 /* MGRecessedPopUpButtonCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGRecessedPopUpButtonCell.h; sourceTree = "<group>"; };
+ C95B43CF0EB73CE3008EE468 /* MGRecessedPopUpButtonCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGRecessedPopUpButtonCell.m; sourceTree = "<group>"; };
+ C9FB61130E8FF8360019B961 /* AppController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppController.h; sourceTree = "<group>"; };
+ C9FB61140E8FF8360019B961 /* AppController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppController.m; sourceTree = "<group>"; };
+ C9FB61170E8FF8810019B961 /* MGScopeBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGScopeBar.h; sourceTree = "<group>"; };
+ C9FB61180E8FF8810019B961 /* MGScopeBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGScopeBar.m; sourceTree = "<group>"; };
+ C9FB61190E8FF8810019B961 /* MGScopeBarDelegateProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGScopeBarDelegateProtocol.h; sourceTree = "<group>"; };
+ C9FB61A80E8FFE7B0019B961 /* TODO */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = TODO; sourceTree = "<group>"; };
+ C9FB61AA0E8FFE950019B961 /* ReadMe.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ReadMe.txt; sourceTree = "<group>"; };
+ C9FB61AC0E8FFEA90019B961 /* Source Code License.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = "Source Code License.rtf"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 8D11072E0486CEB800E47090 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 080E96DDFE201D6D7F000001 /* Classes */ = {
+ isa = PBXGroup;
+ children = (
+ C9FB61130E8FF8360019B961 /* AppController.h */,
+ C9FB61140E8FF8360019B961 /* AppController.m */,
+ C9FB61160E8FF8590019B961 /* MGScopeBar */,
+ );
+ name = Classes;
+ sourceTree = "<group>";
+ };
+ 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */,
+ );
+ name = "Linked Frameworks";
+ sourceTree = "<group>";
+ };
+ 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 29B97324FDCFA39411CA2CEA /* AppKit.framework */,
+ 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */,
+ 29B97325FDCFA39411CA2CEA /* Foundation.framework */,
+ );
+ name = "Other Frameworks";
+ sourceTree = "<group>";
+ };
+ 19C28FACFE9D520D11CA2CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 8D1107320486CEB800E47090 /* MGScopeBar.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 29B97314FDCFA39411CA2CEA /* MGScopeBar */ = {
+ isa = PBXGroup;
+ children = (
+ C9FB61AA0E8FFE950019B961 /* ReadMe.txt */,
+ C9FB61AC0E8FFEA90019B961 /* Source Code License.rtf */,
+ C9FB61A80E8FFE7B0019B961 /* TODO */,
+ 080E96DDFE201D6D7F000001 /* Classes */,
+ 29B97315FDCFA39411CA2CEA /* Other Sources */,
+ 29B97317FDCFA39411CA2CEA /* Resources */,
+ 29B97323FDCFA39411CA2CEA /* Frameworks */,
+ 19C28FACFE9D520D11CA2CBB /* Products */,
+ );
+ name = MGScopeBar;
+ sourceTree = "<group>";
+ };
+ 29B97315FDCFA39411CA2CEA /* Other Sources */ = {
+ isa = PBXGroup;
+ children = (
+ 32CA4F630368D1EE00C91783 /* MGScopeBar_Prefix.pch */,
+ 29B97316FDCFA39411CA2CEA /* main.m */,
+ );
+ name = "Other Sources";
+ sourceTree = "<group>";
+ };
+ 29B97317FDCFA39411CA2CEA /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 8D1107310486CEB800E47090 /* Info.plist */,
+ 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
+ 1DDD58140DA1D0A300B32029 /* MainMenu.xib */,
+ );
+ name = Resources;
+ sourceTree = "<group>";
+ };
+ 29B97323FDCFA39411CA2CEA /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */,
+ 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ C9FB61160E8FF8590019B961 /* MGScopeBar */ = {
+ isa = PBXGroup;
+ children = (
+ C9FB61170E8FF8810019B961 /* MGScopeBar.h */,
+ C9FB61180E8FF8810019B961 /* MGScopeBar.m */,
+ C9FB61190E8FF8810019B961 /* MGScopeBarDelegateProtocol.h */,
+ C95B43CE0EB73CE3008EE468 /* MGRecessedPopUpButtonCell.h */,
+ C95B43CF0EB73CE3008EE468 /* MGRecessedPopUpButtonCell.m */,
+ );
+ name = MGScopeBar;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 8D1107260486CEB800E47090 /* MGScopeBar */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "MGScopeBar" */;
+ buildPhases = (
+ 8D1107290486CEB800E47090 /* Resources */,
+ 8D11072C0486CEB800E47090 /* Sources */,
+ 8D11072E0486CEB800E47090 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = MGScopeBar;
+ productInstallPath = "$(HOME)/Applications";
+ productName = MGScopeBar;
+ productReference = 8D1107320486CEB800E47090 /* MGScopeBar.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 29B97313FDCFA39411CA2CEA /* Project object */ = {
+ isa = PBXProject;
+ buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "MGScopeBar" */;
+ compatibilityVersion = "Xcode 3.1";
+ hasScannedForEncodings = 1;
+ mainGroup = 29B97314FDCFA39411CA2CEA /* MGScopeBar */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 8D1107260486CEB800E47090 /* MGScopeBar */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 8D1107290486CEB800E47090 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
+ 1DDD58160DA1D0A300B32029 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 8D11072C0486CEB800E47090 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D11072D0486CEB800E47090 /* main.m in Sources */,
+ C9FB61150E8FF8360019B961 /* AppController.m in Sources */,
+ C9FB611A0E8FF8810019B961 /* MGScopeBar.m in Sources */,
+ C95B43D00EB73CE3008EE468 /* MGRecessedPopUpButtonCell.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 089C165DFE840E0CC02AAC07 /* English */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+ 1DDD58140DA1D0A300B32029 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 1DDD58150DA1D0A300B32029 /* English */,
+ );
+ name = MainMenu.xib;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ C01FCF4B08A954540054247B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ COPY_PHASE_STRIP = NO;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_ENABLE_FIX_AND_CONTINUE = YES;
+ GCC_MODEL_TUNING = G5;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = MGScopeBar_Prefix.pch;
+ INFOPLIST_FILE = Info.plist;
+ INSTALL_PATH = "$(HOME)/Applications";
+ PRODUCT_NAME = MGScopeBar;
+ };
+ name = Debug;
+ };
+ C01FCF4C08A954540054247B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ GCC_MODEL_TUNING = G5;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = MGScopeBar_Prefix.pch;
+ INFOPLIST_FILE = Info.plist;
+ INSTALL_PATH = "$(HOME)/Applications";
+ PRODUCT_NAME = MGScopeBar;
+ };
+ name = Release;
+ };
+ C01FCF4F08A954540054247B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = "$(ARCHS_STANDARD_32_BIT)";
+ GCC_C_LANGUAGE_STANDARD = c99;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ PREBINDING = NO;
+ SDKROOT = macosx10.5;
+ };
+ name = Debug;
+ };
+ C01FCF5008A954540054247B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = "$(ARCHS_STANDARD_32_BIT)";
+ GCC_C_LANGUAGE_STANDARD = c99;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ PREBINDING = NO;
+ SDKROOT = macosx10.5;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "MGScopeBar" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C01FCF4B08A954540054247B /* Debug */,
+ C01FCF4C08A954540054247B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ C01FCF4E08A954540054247B /* Build configuration list for PBXProject "MGScopeBar" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C01FCF4F08A954540054247B /* Debug */,
+ C01FCF5008A954540054247B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 29B97313FDCFA39411CA2CEA /* Project object */;
+}
46 MGScopeBarDelegateProtocol.h
@@ -0,0 +1,46 @@
+//
+// MGScopeBarDelegateProtocol.h
+// MGScopeBar
+//
+// Created by Matt Gemmell on 15/03/2008.
+// Copyright 2008 Instinctive Code.
+//
+
+#import <Cocoa/Cocoa.h>
+
+
+// Selection modes for the buttons within a group.
+typedef enum _MGScopeBarGroupSelectionMode {
+ MGRadioSelectionMode = 0, // Exactly one item in the group will be selected at a time (no more, and no less).
+ MGMultipleSelectionMode = 1 // Any number of items in the group (including none) may be selected at a time.
+} MGScopeBarGroupSelectionMode;
+
+
+@class MGScopeBar;
+@protocol MGScopeBarDelegate
+
+
+// Methods used to configure the scope bar.
+// Note: all groupNumber parameters are zero-based.
+
+@required
+- (int)numberOfGroupsInScopeBar:(MGScopeBar *)theScopeBar;
+- (NSArray *)scopeBar:(MGScopeBar *)theScopeBar itemIdentifiersForGroup:(int)groupNumber;
+- (NSString *)scopeBar:(MGScopeBar *)theScopeBar labelForGroup:(int)groupNumber; // return nil or an empty string for no label.
+- (MGScopeBarGroupSelectionMode)scopeBar:(MGScopeBar *)theScopeBar selectionModeForGroup:(int)groupNumber;
+- (NSString *)scopeBar:(MGScopeBar *)theScopeBar titleOfItem:(NSString *)identifier inGroup:(int)groupNumber;
+
+@optional
+// If the following method is not implemented, all groups except the first will have a separator before them.
+- (BOOL)scopeBar:(MGScopeBar *)theScopeBar showSeparatorBeforeGroup:(int)groupNumber;
+- (NSImage *)scopeBar:(MGScopeBar *)theScopeBar imageForItem:(NSString *)identifier inGroup:(int)groupNumber; // default is no image. Will be shown at 16x16.
+- (NSView *)accessoryViewForScopeBar:(MGScopeBar *)theScopeBar; // default is no accessory view.
+
+
+// Notification methods.
+
+@optional
+- (void)scopeBar:(MGScopeBar *)theScopeBar selectedStateChanged:(BOOL)selected forItem:(NSString *)identifier inGroup:(int)groupNumber;
+
+
+@end
7 MGScopeBar_Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'MGScopeBar' target in the 'MGScopeBar' project
+//
+
+#ifdef __OBJC__
+ #import <Cocoa/Cocoa.h>
+#endif
75 ReadMe.txt
@@ -0,0 +1,75 @@
+MGScopeBar
+By Matt Legend Gemmell
+http://mattgemmell.com/
+http://instinctivecode.com/
+
+
+
+What is MGScopeBar?
+-------------------
+
+MGScopeBar is a control which provides a "scope bar" or "filter bar", much like that found in iTunes, the Finder (in the Find/Spotlight window), and Mail.
+
+
+
+What platforms does it support?
+-------------------------------
+
+MGScopeBar supports Mac OS X 10.5 (Leopard) or later.
+
+
+
+What are the licensing requirements?
+------------------------------------
+
+A license documented is included with the source code, but essentially it's a BSD-like license but requiring attribution. You're free to use the code in any kind of project, commercial or otherwise. You're also free to redistribute it, either modified or as-is. Read the license document for more details, and contact me if you have questions.
+
+
+
+How do I use it in my project?
+------------------------------
+
+Just copy the five files whose names start with "MG" into your project, and you're good to go. You can also use the AppController class as a handy reference, since it provides a demo of how MGScopeBar works.
+
+
+
+What can it do?
+---------------
+
+MGScopeBar gives you a scope bar control which gets its data from a delegate; it's very like NSTableView and other similar controls, so you should find it easy to use.
+
+You can specify multiple "groups" of buttons, each of which can have:
+
+1. An optional separator before the group.
+
+2. An optional label to the left of the first button (and after the separator).
+
+3. A series of buttons, each of which has a title, unique identifier string, and optional icon.
+
+4. A selection-mode for the group; either radio-mode (only one item selected at a time), or multiple-selection (zero or more items can be selected at a time).
+
+Scope bars also support an optional accessory view, displayed at the right side of the bar.
+
+You can choose whether to use the Smart Resize feature (which is on by default). Smart Resize causes the scope bar to automatically collapse button-groups into popup-menus to better fit the available space.
+
+You should read the MGScopeBarDelegateProtocol.h file to see a list of the delegate methods your delegate object will need to implement.
+
+
+
+How do I know which buttons are selected in which groups?
+---------------------------------------------------------
+
+There's a delegate method which will be called whenever the user interacts with the scope bar in such a way as to change the selection in any group; see the delegate protocol (and the example code in AppController) for more details.
+
+You can also call the -selectedItems method on MGScopeBar to find out exactly what's selected at any time. See the MGScopeBar.h file for an explanation of the data returned from this method.
+
+
+
+Getting in touch
+----------------
+
+Whilst I can't provide specific help with integrating MGScopeBar into your application, I always welcome feedback, suggestions and bug reports. Feel free to get in touch with me via my gmail address (matt.gemmell) anytime.
+
+I hope you enjoy using MGScopeBar.
+
+-Matt Legend Gemmell
104 Source Code License.rtf
@@ -0,0 +1,104 @@
+{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf350
+{\fonttbl\f0\fnil\fcharset0 LucidaGrande;}
+{\colortbl;\red255\green255\blue255;\red51\green51\blue51;\red0\green180\blue128;\red255\green0\blue0;
+\red31\green105\blue199;\red119\green119\blue119;}
+{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc2\leveljcn2\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid0\'02\'05.;}{\levelnumbers\'01;}}{\listname ;}\listid1}}
+{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}}
+\deftab720
+\pard\pardeftab720\ql\qnatural
+
+\f0\b\fs24 \cf2 Matt Gemmell / Instinctive Code Source Code License\
+
+\b0\fs22 Last updated: 19th May 2008
+\fs24 \
+\
+\
+Thanks for downloading some of our source code!\
+\
+This is the license agreement for the source code which this document accompanies (don\'92t worry: you\'92re allowed to use it in your own products, commercial or otherwise).\
+\
+The full license text is further down this page, and you should only use the source code if you agree to the terms in that text. For convenience, though, we\'92ve put together a human-readable
+\b non-authoritative
+\b0 interpretation of the license which will hopefully answer any questions you have.\
+\
+\
+
+\b \cf3 Green
+\b0 \cf2 text shows
+\b \cf3 what you can do with the code
+\b0 \cf2 .\
+
+\b \cf4 Red
+\b0 \cf2 text means
+\b \cf4 restrictions you must abide by
+\b0 \cf2 .\
+\
+Basically, the license says that:\
+\
+\pard\tx220\tx720\pardeftab720\li720\fi-720\ql\qnatural
+\ls1\ilvl0\cf2 {\listtext 1. }You can
+\b \cf3 use the code in your own products, including commercial and/or closed-source products
+\b0 \cf2 .\
+{\listtext 2. }You can
+\b \cf3 modify the code
+\b0 \cf0 as you wish\cf2 , and
+\b \cf3 use the modified code in your products
+\b0 \cf2 .\
+{\listtext 3. }You can
+\b \cf3 redistribute the original, unmodified code
+\b0 \cf2 , but you
+\b \cf4 have to include the full license text below
+\b0 \cf2 .\
+{\listtext 4. }You can
+\b \cf3 redistribute the modified code
+\b0 \cf2 as you wish (
+\b \cf4 without the full license text below
+\b0 \cf2 ).\
+{\listtext 5. }In all cases, you
+\b \cf4 must include a credit mentioning Matt Gemmell
+\b0 \cf2 as the original author of the source.\
+{\listtext 6. }Matt Gemmell is \cf0 not liable for anything you do with the code\cf2 , no matter what. So be sensible.\
+{\listtext 7. }You
+\b \cf4 can\'92t use the name Matt Gemmell, the name Instinctive Code, the Instinctive Code logo or any other related marks to promote your products
+\b0 \cf2 based on the code.\
+{\listtext 8. }If you agree to all of that, go ahead and use the source. Otherwise, don\'92t!\
+\pard\pardeftab720\ql\qnatural
+\cf2 \
+
+\b \
+\
+Suggested Attribution Format\
+
+\b0 \
+The license requires that you give credit to Matt Gemmell, as the original author of any of our source that you use. The placement and format of the credit is up to you, but we prefer the credit to be in the software\'92s \'93About\'94 window. Alternatively, you could put the credit in a list of acknowledgements within the software, in the software\'92s documentation, or on the web page for the software. The suggested format for the attribution is:\
+\
+\pard\pardeftab720\ql\qnatural
+
+\b \cf0 Includes <Name of Code> code by {\field{\*\fldinst{HYPERLINK "http://mattgemmell.com/"}}{\fldrslt \cf5 Matt Gemmell}}\cf6 .
+\b0 \
+\pard\pardeftab720\ql\qnatural
+\cf2 \
+where <Name of Code> would be replaced by the name of the specific source-code package you made use of. Where possible, please link the text \'93Matt Gemmell\'94 to the following URL, or include the URL as plain text: {\field{\*\fldinst{HYPERLINK "http://mattgemmell.com/"}}{\fldrslt \cf5 http://mattgemmell.com/}}\
+\
+\
+
+\b Full Source Code License Text\
+\
+
+\b0 Below you can find the actual text of the license agreement.
+\b \
+\
+\pard\pardeftab720\ql\qnatural
+\cf6 \
+License Agreement for Source Code provided by Matt Gemmell
+\b0 \
+\
+This software is supplied to you by Matt Gemmell in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this software.\
+\
+In consideration of your agreement to abide by the following terms, and subject to these terms, Matt Gemmell grants you a personal, non-exclusive license, to use, reproduce, modify and redistribute the software, with or without modifications, in source and/or binary forms; provided that if you redistribute the software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the software, and that in all cases attribution of Matt Gemmell as the original author of the source code shall be included in all such resulting software products or distributions.\uc0\u8232 \
+Neither the name, trademarks, service marks or logos of Matt Gemmell or Instinctive Code may be used to endorse or promote products derived from the software without specific prior written permission from Matt Gemmell. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Matt Gemmell herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the software may be incorporated.\
+\
+The software is provided by Matt Gemmell on an "AS IS" basis. MATT GEMMELL AND INSTINCTIVE CODE MAKE NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.\
+\
+IN NO EVENT SHALL MATT GEMMELL OR INSTINCTIVE CODE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF MATT GEMMELL OR INSTINCTIVE CODE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
+}
5 TODO
@@ -0,0 +1,5 @@
+MGScopeBar TODO
+
+
+- Maybe properly support a blue gradient appearance (like Mail in Tiger)?
+ - Easy to change background gradient, but buttons/popups still highlight gray.
14 main.m
@@ -0,0 +1,14 @@
+//
+// main.m
+// MGScopeBar
+//
+// Created by Matt Gemmell on 28/09/2008.
+// Copyright Instinctive Code 2008. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+int main(int argc, char *argv[])
+{
+ return NSApplicationMain(argc, (const char **) argv);
+}
Please sign in to comment.
Something went wrong with that request. Please try again.