Initial commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// AppDelegate.h
|
||||
// PXSourceList
|
||||
//
|
||||
// Created by Alex Rozanski on 08/01/2010.
|
||||
// Copyright 2010 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "PXSourceList.h"
|
||||
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate, PXSourceListDataSource, PXSourceListDelegate> {
|
||||
IBOutlet PXSourceList *sourceList;
|
||||
IBOutlet NSTextField *selectedItemLabel;
|
||||
|
||||
NSMutableArray *sourceListItems;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// AppDelegate.m
|
||||
// PXSourceList
|
||||
//
|
||||
// Created by Alex Rozanski on 08/01/2010.
|
||||
// Copyright 2010 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
#import "SourceListItem.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Init/Dealloc
|
||||
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
[selectedItemLabel setStringValue:@"(none)"];
|
||||
|
||||
sourceListItems = [[NSMutableArray alloc] init];
|
||||
|
||||
//Set up the "Library" parent item and children
|
||||
SourceListItem *libraryItem = [SourceListItem itemWithTitle:@"LIBRARY" identifier:@"library"];
|
||||
SourceListItem *musicItem = [SourceListItem itemWithTitle:@"Music" identifier:@"music"];
|
||||
[musicItem setIcon:[NSImage imageNamed:@"music.png"]];
|
||||
SourceListItem *moviesItem = [SourceListItem itemWithTitle:@"Movies" identifier:@"movies"];
|
||||
[moviesItem setIcon:[NSImage imageNamed:@"movies.png"]];
|
||||
SourceListItem *podcastsItem = [SourceListItem itemWithTitle:@"Podcasts" identifier:@"podcasts"];
|
||||
[podcastsItem setIcon:[NSImage imageNamed:@"podcasts.png"]];
|
||||
[podcastsItem setBadgeValue:10];
|
||||
SourceListItem *audiobooksItem = [SourceListItem itemWithTitle:@"Audiobooks" identifier:@"audiobooks"];
|
||||
[audiobooksItem setIcon:[NSImage imageNamed:@"audiobooks.png"]];
|
||||
[libraryItem setChildren:[NSArray arrayWithObjects:musicItem, moviesItem, podcastsItem,
|
||||
audiobooksItem, nil]];
|
||||
|
||||
//Set up the "Playlists" parent item and children
|
||||
SourceListItem *playlistsItem = [SourceListItem itemWithTitle:@"PLAYLISTS" identifier:@"playlists"];
|
||||
SourceListItem *playlist1Item = [SourceListItem itemWithTitle:@"Playlist1" identifier:@"playlist1"];
|
||||
|
||||
//Create a second-level group to demonstrate
|
||||
SourceListItem *playlist2Item = [SourceListItem itemWithTitle:@"Playlist2" identifier:@"playlist2"];
|
||||
SourceListItem *playlist3Item = [SourceListItem itemWithTitle:@"Playlist3" identifier:@"playlist3"];
|
||||
[playlist1Item setIcon:[NSImage imageNamed:@"playlist.png"]];
|
||||
[playlist2Item setIcon:[NSImage imageNamed:@"playlist.png"]];
|
||||
[playlist3Item setIcon:[NSImage imageNamed:@"playlist.png"]];
|
||||
|
||||
SourceListItem *playlistGroup = [SourceListItem itemWithTitle:@"Playlist Group" identifier:@"playlistgroup"];
|
||||
SourceListItem *playlistGroupItem = [SourceListItem itemWithTitle:@"Child Playlist" identifier:@"childplaylist"];
|
||||
[playlistGroup setIcon:[NSImage imageNamed:@"playlistFolder.png"]];
|
||||
[playlistGroupItem setIcon:[NSImage imageNamed:@"playlist.png"]];
|
||||
[playlistGroup setChildren:[NSArray arrayWithObject:playlistGroupItem]];
|
||||
|
||||
[playlistsItem setChildren:[NSArray arrayWithObjects:playlist1Item, playlistGroup,playlist2Item,
|
||||
playlist3Item, nil]];
|
||||
|
||||
[sourceListItems addObject:libraryItem];
|
||||
[sourceListItems addObject:playlistsItem];
|
||||
|
||||
[sourceList reloadData];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[sourceListItems release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Source List Data Source Methods
|
||||
|
||||
- (NSUInteger)sourceList:(PXSourceList*)sourceList numberOfChildrenOfItem:(id)item
|
||||
{
|
||||
//Works the same way as the NSOutlineView data source: `nil` means a parent item
|
||||
if(item==nil) {
|
||||
return [sourceListItems count];
|
||||
}
|
||||
else {
|
||||
return [[item children] count];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (id)sourceList:(PXSourceList*)aSourceList child:(NSUInteger)index ofItem:(id)item
|
||||
{
|
||||
//Works the same way as the NSOutlineView data source: `nil` means a parent item
|
||||
if(item==nil) {
|
||||
return [sourceListItems objectAtIndex:index];
|
||||
}
|
||||
else {
|
||||
return [[item children] objectAtIndex:index];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (id)sourceList:(PXSourceList*)aSourceList objectValueForItem:(id)item
|
||||
{
|
||||
return [item title];
|
||||
}
|
||||
|
||||
|
||||
- (void)sourceList:(PXSourceList*)aSourceList setObjectValue:(id)object forItem:(id)item
|
||||
{
|
||||
[item setTitle:object];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList isItemExpandable:(id)item
|
||||
{
|
||||
return [item hasChildren];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList itemHasBadge:(id)item
|
||||
{
|
||||
return [item hasBadge];
|
||||
}
|
||||
|
||||
|
||||
- (NSInteger)sourceList:(PXSourceList*)aSourceList badgeValueForItem:(id)item
|
||||
{
|
||||
return [item badgeValue];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList itemHasIcon:(id)item
|
||||
{
|
||||
return [item hasIcon];
|
||||
}
|
||||
|
||||
|
||||
- (NSImage*)sourceList:(PXSourceList*)aSourceList iconForItem:(id)item
|
||||
{
|
||||
return [item icon];
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Source List Delegate Methods
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList isGroupAlwaysExpanded:(id)group
|
||||
{
|
||||
if([[group identifier] isEqualToString:@"library"])
|
||||
return YES;
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
- (void)sourceListSelectionDidChange:(NSNotification *)notification
|
||||
{
|
||||
NSIndexSet *selectedIndexes = [sourceList selectedRowIndexes];
|
||||
|
||||
//Set the label text to represent the new selection
|
||||
if([selectedIndexes count]>1)
|
||||
[selectedItemLabel setStringValue:@"(multiple)"];
|
||||
else if([selectedIndexes count]==1) {
|
||||
NSString *identifier = [[sourceList itemAtRow:[selectedIndexes firstIndex]] identifier];
|
||||
|
||||
[selectedItemLabel setStringValue:identifier];
|
||||
}
|
||||
else {
|
||||
[selectedItemLabel setStringValue:@"(none)"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)sourceListDeleteKeyPressedOnRows:(NSNotification *)notification
|
||||
{
|
||||
NSIndexSet *rows = [[notification userInfo] objectForKey:@"rows"];
|
||||
|
||||
NSLog(@"Delete key pressed on rows %@", rows);
|
||||
|
||||
//Do something here
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// PXSourceList.h
|
||||
// PXSourceList
|
||||
//
|
||||
// Created by Alex Rozanski on 05/09/2009.
|
||||
// Copyright 2009-10 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "PXSourceListDelegate.h"
|
||||
#import "PXSourceListDataSource.h"
|
||||
|
||||
@interface PXSourceList: NSOutlineView <NSOutlineViewDelegate, NSOutlineViewDataSource, NSMenuDelegate>
|
||||
{
|
||||
id <PXSourceListDelegate> _secondaryDelegate; //Used to store the publicly visible delegate
|
||||
id <PXSourceListDataSource> _secondaryDataSource; //Used to store the publicly visible data source
|
||||
|
||||
NSSize _iconSize; //The size of icons in the Source List. Defaults to 16x16
|
||||
BOOL _isRowRightClicked; //Whether an item has been right-clicked
|
||||
NSUInteger _rightClickedRowIndex; //The index of the item that has been right-clicked
|
||||
}
|
||||
|
||||
@property NSSize iconSize;
|
||||
|
||||
@property (assign) id<PXSourceListDataSource> dataSource;
|
||||
@property (assign) id<PXSourceListDelegate> delegate;
|
||||
|
||||
- (NSUInteger)numberOfGroups; //Returns the number of groups in the Source List
|
||||
- (BOOL)isGroupItem:(id)item; //Returns whether `item` is a group
|
||||
- (BOOL)isGroupAlwaysExpanded:(id)group; //Returns whether `group` is displayed as always expanded
|
||||
|
||||
- (BOOL)itemHasBadge:(id)item; //Returns whether `item` has a badge
|
||||
- (NSInteger)badgeValueForItem:(id)item; //Returns the badge value for `item`
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,880 @@
|
||||
//
|
||||
// PXSourceList.m
|
||||
// PXSourceList
|
||||
//
|
||||
// Created by Alex Rozanski on 05/09/2009.
|
||||
// Copyright 2009-10 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
|
||||
#import "PXSourceList.h"
|
||||
|
||||
//Layout constants
|
||||
#define MIN_BADGE_WIDTH 22.0 //The minimum badge width for each item (default 22.0)
|
||||
#define BADGE_HEIGHT 14.0 //The badge height for each item (default 14.0)
|
||||
#define BADGE_MARGIN 5.0 //The spacing between the badge and the cell for that row
|
||||
#define ROW_RIGHT_MARGIN 5.0 //The spacing between the right edge of the badge and the edge of the table column
|
||||
#define ICON_SPACING 2.0 //The spacing between the icon and it's adjacent cell
|
||||
#define DISCLOSURE_TRIANGLE_SPACE 18.0 //The indentation reserved for disclosure triangles for non-group items
|
||||
|
||||
//Drawing constants
|
||||
#define BADGE_BACKGROUND_COLOR [NSColor colorWithCalibratedRed:(152/255.0) green:(168/255.0) blue:(202/255.0) alpha:1]
|
||||
#define BADGE_HIDDEN_BACKGROUND_COLOR [NSColor colorWithDeviceWhite:(180/255.0) alpha:1]
|
||||
#define BADGE_SELECTED_TEXT_COLOR [NSColor keyboardFocusIndicatorColor]
|
||||
#define BADGE_SELECTED_UNFOCUSED_TEXT_COLOR [NSColor colorWithCalibratedRed:(153/255.0) green:(169/255.0) blue:(203/255.0) alpha:1]
|
||||
#define BADGE_SELECTED_HIDDEN_TEXT_COLOR [NSColor colorWithCalibratedWhite:(170/255.0) alpha:1]
|
||||
#define BADGE_FONT [NSFont boldSystemFontOfSize:11]
|
||||
|
||||
#define RIGHT_CLICKED_BGCOLOR [NSColor colorWithCalibratedRed:(207/255.0) green:(221/255.0) blue:(247/255.0) alpha:1]
|
||||
#define RIGHT_CLICKED_OUTLINE_COLOR [NSColor colorWithCalibratedRed:(84/255.0) green:(101/255.0) blue:(226/255.0) alpha:1]
|
||||
#define RIGHT_CLICKED_SELECTED_OUTLINE_COLOR [NSColor whiteColor]
|
||||
|
||||
//Delegate notification constants
|
||||
NSString * const PXSLSelectionIsChangingNotification = @"PXSourceListSelectionIsChanging";
|
||||
NSString * const PXSLSelectionDidChangeNotification = @"PXSourceListSelectionDidChange";
|
||||
NSString * const PXSLItemWillExpandNotification = @"PXSourceListItemWillExpand";
|
||||
NSString * const PXSLItemDidExpandNotification = @"PXSourceListItemDidExpand";
|
||||
NSString * const PXSLItemWillCollapseNotification = @"PXSourceListItemWillCollapse";
|
||||
NSString * const PXSLItemDidCollapseNotification = @"PXSourceListItemDidCollapse";
|
||||
NSString * const PXSLDeleteKeyPressedOnRowsNotification = @"PXSourceListDeleteKeyPressedOnRows";
|
||||
|
||||
@interface PXSourceList ()
|
||||
|
||||
- (NSSize)sizeOfBadgeAtRow:(NSInteger)rowIndex;
|
||||
- (void)drawBadgeForRow:(NSInteger)rowIndex inRect:(NSRect)badgeFrame;
|
||||
- (void)registerDelegateToReceiveNotification:(NSString*)notification withSelector:(SEL)selector;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
//MARK: -
|
||||
|
||||
@implementation PXSourceList
|
||||
|
||||
@synthesize iconSize = _iconSize;
|
||||
@dynamic dataSource;
|
||||
@dynamic delegate;
|
||||
|
||||
#pragma mark Init/Dealloc
|
||||
|
||||
- (id)initWithCoder:(NSCoder*)decoder
|
||||
{
|
||||
if(self=[super initWithCoder:decoder])
|
||||
{
|
||||
[self setDelegate:(id<PXSourceListDelegate>)[super delegate]];
|
||||
[super setDelegate:self];
|
||||
|
||||
[self setDataSource:(id<PXSourceListDataSource>)[super dataSource]];
|
||||
[super setDataSource:self];
|
||||
|
||||
_iconSize = NSMakeSize(16,16);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
//Remove ourselves as the delegate and data source to be safe
|
||||
[super setDataSource:nil];
|
||||
[super setDelegate:nil];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Custom Accessors
|
||||
|
||||
- (void)setDelegate:(id<PXSourceListDelegate>)aDelegate
|
||||
{
|
||||
//Unregister the old delegate from receiving notifications
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_secondaryDelegate name:nil object:self];
|
||||
|
||||
_secondaryDelegate = aDelegate;
|
||||
|
||||
//Register the new delegate to receive notifications
|
||||
[self registerDelegateToReceiveNotification:PXSLSelectionIsChangingNotification
|
||||
withSelector:@selector(sourceListSelectionIsChanging:)];
|
||||
[self registerDelegateToReceiveNotification:PXSLSelectionDidChangeNotification
|
||||
withSelector:@selector(sourceListSelectionDidChange:)];
|
||||
[self registerDelegateToReceiveNotification:PXSLItemWillExpandNotification
|
||||
withSelector:@selector(sourceListItemWillExpand:)];
|
||||
[self registerDelegateToReceiveNotification:PXSLItemDidExpandNotification
|
||||
withSelector:@selector(sourceListItemDidExpand:)];
|
||||
[self registerDelegateToReceiveNotification:PXSLItemWillCollapseNotification
|
||||
withSelector:@selector(sourceListItemWillCollapse:)];
|
||||
[self registerDelegateToReceiveNotification:PXSLItemDidCollapseNotification
|
||||
withSelector:@selector(sourceListItemWillCollapse:)];
|
||||
[self registerDelegateToReceiveNotification:PXSLDeleteKeyPressedOnRowsNotification
|
||||
withSelector:@selector(sourceListDeleteKeyPressedOnRows:)];
|
||||
}
|
||||
|
||||
|
||||
- (void)setDataSource:(id<PXSourceListDataSource>)aDataSource
|
||||
{
|
||||
_secondaryDataSource = aDataSource;
|
||||
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
- (void)setIconSize:(NSSize)newIconSize
|
||||
{
|
||||
_iconSize = newIconSize;
|
||||
|
||||
CGFloat rowHeight = [self rowHeight];
|
||||
|
||||
//Make sure icon height does not exceed row height; if so constrain, keeping width and height in proportion
|
||||
if(_iconSize.height>rowHeight)
|
||||
{
|
||||
_iconSize.width = _iconSize.width * (rowHeight/_iconSize.height);
|
||||
_iconSize.height = rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Data Management
|
||||
|
||||
- (void)reloadData
|
||||
{
|
||||
[super reloadData];
|
||||
|
||||
//Expand items that are displayed as always expanded
|
||||
if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)] &&
|
||||
[_secondaryDelegate respondsToSelector:@selector(sourceList:isGroupAlwaysExpanded:)])
|
||||
{
|
||||
for(NSUInteger i=0;i<[self numberOfGroups];i++)
|
||||
{
|
||||
id item = [_secondaryDataSource sourceList:self child:i ofItem:nil];
|
||||
|
||||
if([self isGroupAlwaysExpanded:item]) {
|
||||
[self expandItem:item expandChildren:NO];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Select the first non-group row if no rows are selected, and empty selection is disallowed
|
||||
if(![self allowsEmptySelection]&&[self numberOfSelectedRows]==0)
|
||||
{
|
||||
for(NSUInteger i=0;i<[self numberOfRows];i++)
|
||||
{
|
||||
if(![self isGroupItem:[self itemAtRow:i]]) {
|
||||
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:i] byExtendingSelection:NO];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSUInteger)numberOfGroups
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:numberOfChildrenOfItem:)]) {
|
||||
return [_secondaryDataSource sourceList:self numberOfChildrenOfItem:nil];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)isGroupItem:(id)item
|
||||
{
|
||||
//Groups are defined as root items (at level 0)
|
||||
return 0==[self levelForItem:item];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)isGroupAlwaysExpanded:(id)group
|
||||
{
|
||||
//Make sure that the item IS a group to prevent unwanted queries sent to the data source
|
||||
if([self isGroupItem:group]) {
|
||||
//Query the data source
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:isGroupAlwaysExpanded:)]) {
|
||||
return [_secondaryDelegate sourceList:self isGroupAlwaysExpanded:group];
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)itemHasBadge:(id)item
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:itemHasBadge:)]) {
|
||||
return [_secondaryDataSource sourceList:self itemHasBadge:item];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSInteger)badgeValueForItem:(id)item
|
||||
{
|
||||
//Make sure that the item has a badge
|
||||
if(![self itemHasBadge:item]) {
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:badgeValueForItem:)]) {
|
||||
return [_secondaryDataSource sourceList:self badgeValueForItem:item];
|
||||
}
|
||||
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Selection Handling
|
||||
|
||||
- (void)selectRowIndexes:(NSIndexSet*)indexes byExtendingSelection:(BOOL)extend
|
||||
{
|
||||
NSUInteger numberOfIndexes = [indexes count];
|
||||
|
||||
//Prevent empty selection if we don't want it
|
||||
if(![self allowsEmptySelection]&&0==numberOfIndexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Would use blocks but we're also targeting 10.5...
|
||||
//Get the selected indexes
|
||||
NSUInteger *selectedIndexes = malloc(sizeof(NSUInteger)*numberOfIndexes);
|
||||
[indexes getIndexes:selectedIndexes maxCount:numberOfIndexes inIndexRange:nil];
|
||||
|
||||
//Loop through the indexes and only add non-group row indexes
|
||||
//Allows selection across groups without selecting the group rows
|
||||
NSMutableIndexSet *newSelectionIndexes = [NSMutableIndexSet indexSet];
|
||||
for(NSInteger i=0;i<numberOfIndexes;i++)
|
||||
{
|
||||
if(![self isGroupItem:[self itemAtRow:selectedIndexes[i]]]) {
|
||||
[newSelectionIndexes addIndex:selectedIndexes[i]];
|
||||
}
|
||||
}
|
||||
|
||||
//If there are any non-group rows selected
|
||||
if([newSelectionIndexes count]>0) {
|
||||
[super selectRowIndexes:newSelectionIndexes byExtendingSelection:extend];
|
||||
}
|
||||
|
||||
//C memory management... *sigh*
|
||||
free(selectedIndexes);
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Layout
|
||||
|
||||
- (NSRect)frameOfOutlineCellAtRow:(NSInteger)row
|
||||
{
|
||||
//Return a zero-rect if the item is always expanded (a disclosure triangle will not be drawn)
|
||||
if([self isGroupAlwaysExpanded:[self itemAtRow:row]]) {
|
||||
return NSZeroRect;
|
||||
}
|
||||
|
||||
return [super frameOfOutlineCellAtRow:row];
|
||||
}
|
||||
|
||||
|
||||
- (NSRect)frameOfCellAtColumn:(NSInteger)column row:(NSInteger)row
|
||||
{
|
||||
id item = [self itemAtRow:row];
|
||||
|
||||
NSCell *cell = [self preparedCellAtColumn:column row:row];
|
||||
NSSize cellSize = [cell cellSize];
|
||||
NSRect cellFrame = [super frameOfCellAtColumn:column row:row];
|
||||
|
||||
if([self isGroupItem:item])
|
||||
{
|
||||
return NSMakeRect(NSMinX(cellFrame),
|
||||
NSMidY(cellFrame)-(cellSize.height/2.0),
|
||||
NSWidth(cellFrame),
|
||||
cellSize.height);
|
||||
}
|
||||
else
|
||||
{
|
||||
CGFloat leftIndent = [self levelForRow:row]*[self indentationPerLevel]+DISCLOSURE_TRIANGLE_SPACE;
|
||||
|
||||
//Calculate space left for a badge if need be
|
||||
CGFloat rightIndent = [self sizeOfBadgeAtRow:row].width+ROW_RIGHT_MARGIN;
|
||||
|
||||
//Allow space for an icon if need be
|
||||
if(![self isGroupItem:item]&&[_secondaryDataSource respondsToSelector:@selector(sourceList:itemHasIcon:)])
|
||||
{
|
||||
if([_secondaryDataSource sourceList:self itemHasIcon:item]) {
|
||||
leftIndent += [self iconSize].width+(ICON_SPACING*2);
|
||||
}
|
||||
}
|
||||
|
||||
return NSMakeRect(leftIndent,
|
||||
NSMidY(cellFrame)-(cellSize.height/2.0),
|
||||
NSWidth(cellFrame)-rightIndent-leftIndent,
|
||||
cellSize.height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//This method calculates and returns the size of the badge for the row index passed to the method. If the
|
||||
//row for the row index passed to the method does not have a badge, then NSZeroSize is returned.
|
||||
- (NSSize)sizeOfBadgeAtRow:(NSInteger)rowIndex
|
||||
{
|
||||
id rowItem = [self itemAtRow:rowIndex];
|
||||
|
||||
//Make sure that the item has a badge
|
||||
if(![self itemHasBadge:rowItem]) {
|
||||
return NSZeroSize;
|
||||
}
|
||||
|
||||
NSAttributedString *badgeAttrString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d", [self badgeValueForItem:rowItem]]
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:BADGE_FONT, NSFontAttributeName, nil]];
|
||||
|
||||
NSSize stringSize = [badgeAttrString size];
|
||||
|
||||
//Calculate the width needed to display the text or the minimum width if it's smaller
|
||||
CGFloat width = stringSize.width+(2*BADGE_MARGIN);
|
||||
|
||||
if(width<MIN_BADGE_WIDTH) {
|
||||
width = MIN_BADGE_WIDTH;
|
||||
}
|
||||
|
||||
[badgeAttrString release];
|
||||
|
||||
return NSMakeSize(width, BADGE_HEIGHT);
|
||||
}
|
||||
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Drawing
|
||||
|
||||
- (void)drawRow:(NSInteger)rowIndex clipRect:(NSRect)clipRect
|
||||
{
|
||||
//If the row has been right-clicked...
|
||||
if(_isRowRightClicked&&_rightClickedRowIndex==rowIndex)
|
||||
{
|
||||
NSRect rowRect = [self rectOfRow:rowIndex];
|
||||
NSUInteger selectedRow = [[self selectedRowIndexes] firstIndex];
|
||||
NSBezierPath *roundRectPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(rowRect, 2,2)
|
||||
xRadius:5
|
||||
yRadius:5];
|
||||
[roundRectPath setLineWidth:2];
|
||||
|
||||
if(_rightClickedRowIndex==selectedRow)
|
||||
{
|
||||
[RIGHT_CLICKED_SELECTED_OUTLINE_COLOR set];
|
||||
[roundRectPath stroke];
|
||||
}
|
||||
else
|
||||
{
|
||||
[RIGHT_CLICKED_BGCOLOR set];
|
||||
[roundRectPath fill];
|
||||
[RIGHT_CLICKED_OUTLINE_COLOR set];
|
||||
[roundRectPath stroke];
|
||||
}
|
||||
}
|
||||
|
||||
[super drawRow:rowIndex clipRect:clipRect];
|
||||
|
||||
id item = [self itemAtRow:rowIndex];
|
||||
|
||||
//Draw an icon if the item has one
|
||||
if(![self isGroupItem:item]&&[_secondaryDataSource respondsToSelector:@selector(sourceList:itemHasIcon:)])
|
||||
{
|
||||
if([_secondaryDataSource sourceList:self itemHasIcon:item])
|
||||
{
|
||||
NSRect cellFrame = [self frameOfCellAtColumn:0 row:rowIndex];
|
||||
NSSize iconSize = [self iconSize];
|
||||
NSRect iconRect = NSMakeRect(NSMinX(cellFrame)-iconSize.width-ICON_SPACING,
|
||||
NSMidY(cellFrame)-(iconSize.width/2.0),
|
||||
iconSize.width,
|
||||
iconSize.height);
|
||||
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:iconForItem:)])
|
||||
{
|
||||
NSImage *icon = [_secondaryDataSource sourceList:self iconForItem:item];
|
||||
|
||||
if(icon!=nil)
|
||||
{
|
||||
[icon setFlipped:[self isFlipped]];
|
||||
NSSize iconSize = [icon size];
|
||||
NSRect drawRect = NSMakeRect(NSMidX(iconRect)-(iconSize.width/2.0),
|
||||
NSMidY(iconRect)-(iconSize.height/2.0),
|
||||
iconSize.width,
|
||||
iconSize.height);
|
||||
|
||||
[icon drawInRect:drawRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Draw the badge if the item has one
|
||||
if([self itemHasBadge:item])
|
||||
{
|
||||
NSRect rowRect = [self rectOfRow:rowIndex];
|
||||
NSSize badgeSize = [self sizeOfBadgeAtRow:rowIndex];
|
||||
|
||||
NSRect badgeFrame = NSMakeRect(NSMaxX(rowRect)-badgeSize.width-ROW_RIGHT_MARGIN,
|
||||
NSMidY(rowRect)-(badgeSize.height/2.0),
|
||||
badgeSize.width,
|
||||
badgeSize.height);
|
||||
|
||||
[self drawBadgeForRow:rowIndex inRect:badgeFrame];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawBadgeForRow:(NSInteger)rowIndex inRect:(NSRect)badgeFrame
|
||||
{
|
||||
id rowItem = [self itemAtRow:rowIndex];
|
||||
|
||||
NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeFrame
|
||||
xRadius:(BADGE_HEIGHT/2.0)
|
||||
yRadius:(BADGE_HEIGHT/2.0)];
|
||||
|
||||
//Get window and control state to determine colours used
|
||||
BOOL isVisible = [[NSApp mainWindow] isVisible];
|
||||
BOOL isFocused = [[[self window] firstResponder] isEqual:self];
|
||||
NSInteger rowBeingEdited = [self editedRow];
|
||||
|
||||
//Set the attributes based on the row state
|
||||
NSDictionary *attributes;
|
||||
NSColor *backgroundColor;
|
||||
|
||||
if([[self selectedRowIndexes] containsIndex:rowIndex])
|
||||
{
|
||||
backgroundColor = [NSColor whiteColor];
|
||||
|
||||
//Set the text color based on window and control state
|
||||
NSColor *textColor;
|
||||
|
||||
if(isVisible && (isFocused || rowBeingEdited==rowIndex)) {
|
||||
textColor = BADGE_SELECTED_TEXT_COLOR;
|
||||
}
|
||||
else if(isVisible && !isFocused) {
|
||||
textColor = BADGE_SELECTED_UNFOCUSED_TEXT_COLOR;
|
||||
}
|
||||
else {
|
||||
textColor = BADGE_SELECTED_HIDDEN_TEXT_COLOR;
|
||||
}
|
||||
|
||||
attributes = [[NSDictionary alloc] initWithObjectsAndKeys:BADGE_FONT, NSFontAttributeName,
|
||||
textColor, NSForegroundColorAttributeName, nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
//Set the text colour based on window and control state
|
||||
NSColor *badgeColor = [NSColor whiteColor];
|
||||
|
||||
if(isVisible) {
|
||||
//If the data source returns a custom colour..
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:badgeBackgroundColorForItem:)]) {
|
||||
backgroundColor = [_secondaryDataSource sourceList:self badgeBackgroundColorForItem:rowItem];
|
||||
|
||||
if(backgroundColor==nil)
|
||||
backgroundColor = BADGE_BACKGROUND_COLOR;
|
||||
}
|
||||
else { //Otherwise use the default (purple-blue colour)
|
||||
backgroundColor = BADGE_BACKGROUND_COLOR;
|
||||
}
|
||||
|
||||
//If the delegate wants a custom badge text colour..
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:badgeTextColorForItem:)]) {
|
||||
badgeColor = [_secondaryDataSource sourceList:self badgeTextColorForItem:rowItem];
|
||||
|
||||
if(badgeColor==nil)
|
||||
badgeColor = [NSColor whiteColor];
|
||||
}
|
||||
}
|
||||
else { //Gray colour
|
||||
backgroundColor = BADGE_HIDDEN_BACKGROUND_COLOR;
|
||||
}
|
||||
|
||||
attributes = [[NSDictionary alloc] initWithObjectsAndKeys:BADGE_FONT, NSFontAttributeName,
|
||||
badgeColor, NSForegroundColorAttributeName, nil];
|
||||
}
|
||||
|
||||
[backgroundColor set];
|
||||
[badgePath fill];
|
||||
|
||||
//Draw the badge text
|
||||
NSAttributedString *badgeAttrString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d", [self badgeValueForItem:rowItem]]
|
||||
attributes:attributes];
|
||||
NSSize stringSize = [badgeAttrString size];
|
||||
NSPoint badgeTextPoint = NSMakePoint(NSMidX(badgeFrame)-(stringSize.width/2.0), //Center in the badge frame
|
||||
NSMidY(badgeFrame)-(stringSize.height/2.0)); //Center in the badge frame
|
||||
[badgeAttrString drawAtPoint:badgeTextPoint];
|
||||
|
||||
[attributes release];
|
||||
[badgeAttrString release];
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Keyboard Handling
|
||||
|
||||
- (void)keyDown:(NSEvent *)theEvent
|
||||
{
|
||||
NSIndexSet *selectedIndexes = [self selectedRowIndexes];
|
||||
|
||||
NSString *keyCharacters = [theEvent characters];
|
||||
|
||||
//Make sure we have a selection
|
||||
if([selectedIndexes count]>0)
|
||||
{
|
||||
if([keyCharacters length]>0)
|
||||
{
|
||||
unichar firstKey = [keyCharacters characterAtIndex:0];
|
||||
|
||||
if(firstKey==NSUpArrowFunctionKey||firstKey==NSDownArrowFunctionKey)
|
||||
{
|
||||
//Handle keyboard navigation across groups
|
||||
if([selectedIndexes count]==1&&!([theEvent modifierFlags] & NSShiftKeyMask))
|
||||
{
|
||||
int delta = firstKey==NSDownArrowFunctionKey?1:-1; //Search "backwards" if up arrow, "forwards" if down
|
||||
NSInteger newRow = [selectedIndexes firstIndex];
|
||||
|
||||
//Keep incrementing/decrementing the row until a non-header row is reached
|
||||
do {
|
||||
newRow+=delta;
|
||||
|
||||
//If out of bounds of the number of rows..
|
||||
if(newRow<0||newRow==[self numberOfRows])
|
||||
break;
|
||||
} while([self isGroupItem:[self itemAtRow:newRow]]);
|
||||
|
||||
|
||||
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(firstKey==NSDeleteCharacter||firstKey==NSBackspaceCharacter)
|
||||
{
|
||||
//Post the notification
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PXSLDeleteKeyPressedOnRowsNotification
|
||||
object:self
|
||||
userInfo:[NSDictionary dictionaryWithObject:selectedIndexes forKey:@"rows"]];
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//We don't care about it
|
||||
[super keyDown:theEvent];
|
||||
}
|
||||
|
||||
#pragma mark Context Menu Handling
|
||||
|
||||
- (void)rightMouseDown:(NSEvent*)event
|
||||
{
|
||||
NSInteger selectedRow = [self rowAtPoint:[self convertPoint:[event locationInWindow] fromView:nil]];
|
||||
|
||||
if(selectedRow!=-1&&![self isGroupItem:[self itemAtRow:selectedRow]])
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:contextMenuForItem:)])
|
||||
{
|
||||
NSMenu *contextMenu = [_secondaryDataSource sourceList:self contextMenuForItem:[self itemAtRow:selectedRow]];
|
||||
|
||||
if(contextMenu!=nil)
|
||||
{
|
||||
[contextMenu setDelegate:self];
|
||||
|
||||
//Row needs to be redrawn with round selection rectangle
|
||||
_isRowRightClicked = YES;
|
||||
_rightClickedRowIndex = selectedRow;
|
||||
|
||||
//Let's be economical
|
||||
[self setNeedsDisplayInRect:[self rectOfRow:selectedRow]];
|
||||
|
||||
[NSMenu popUpContextMenu:contextMenu withEvent:event forView:self];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)menuDidClose:(NSMenu *)menu
|
||||
{
|
||||
_isRowRightClicked = NO;
|
||||
|
||||
//Row needs to be redrawn to clear round selection rectangle
|
||||
[self setNeedsDisplayInRect:[self rectOfRow:_rightClickedRowIndex]];
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark NSOutlineView Data Source methods
|
||||
|
||||
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
|
||||
{
|
||||
if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
|
||||
return [_secondaryDataSource sourceList:self numberOfChildrenOfItem:item];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
|
||||
{
|
||||
if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
|
||||
return [_secondaryDataSource sourceList:self child:index ofItem:item];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
|
||||
{
|
||||
if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
|
||||
return [_secondaryDataSource sourceList:self isItemExpandable:item];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
|
||||
{
|
||||
if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
|
||||
return [_secondaryDataSource sourceList:self objectValueForItem:item];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
|
||||
{
|
||||
if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
|
||||
[_secondaryDataSource sourceList:self setObjectValue:object forItem:item];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (id)outlineView:(NSOutlineView *)outlineView itemForPersistentObject:(id)object
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:itemForPersistentObject:)]) {
|
||||
return [_secondaryDataSource sourceList:self itemForPersistentObject:object];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)outlineView:(NSOutlineView *)outlineView persistentObjectForItem:(id)item
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:persistentObjectForItem:)]) {
|
||||
return [_secondaryDataSource sourceList:self persistentObjectForItem:item];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pasteboard
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:writeItems:toPasteboard:)]) {
|
||||
return [_secondaryDataSource sourceList:self writeItems:items toPasteboard:pasteboard];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:validateDrop:proposedItem:proposedChildIndex:)]) {
|
||||
return [_secondaryDataSource sourceList:self validateDrop:info proposedItem:item proposedChildIndex:index];
|
||||
}
|
||||
|
||||
return NSDragOperationNone;
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:acceptDrop:item:childIndex:)]) {
|
||||
return [_secondaryDataSource sourceList:self acceptDrop:info item:item childIndex:index];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
- (NSArray *)outlineView:(NSOutlineView *)outlineView namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination forDraggedItems:(NSArray *)items
|
||||
{
|
||||
if([_secondaryDataSource respondsToSelector:@selector(sourceList:namesOfPromisedFilesDroppedAtDestination:forDraggedItems:)]) {
|
||||
return [_secondaryDataSource sourceList:self namesOfPromisedFilesDroppedAtDestination:dropDestination forDraggedItems:items];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark NSOutlineView Delegate methods
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldExpandItem:(id)item
|
||||
{
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldExpandItem:)]) {
|
||||
return [_secondaryDelegate sourceList:self shouldExpandItem:item];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(id)item
|
||||
{
|
||||
//Make sure the item isn't displayed as always expanded
|
||||
if([self isGroupItem:item])
|
||||
{
|
||||
if([self isGroupAlwaysExpanded:item]) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldCollapseItem:)]) {
|
||||
return [_secondaryDelegate sourceList:self shouldExpandItem:item];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
||||
{
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:dataCellForItem:)]) {
|
||||
return [_secondaryDelegate sourceList:self dataCellForItem:item];
|
||||
}
|
||||
|
||||
NSInteger row = [self rowForItem:item];
|
||||
|
||||
//Return the default table column
|
||||
return [[[self tableColumns] objectAtIndex:0] dataCellForRow:row];
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item
|
||||
{
|
||||
//Make sure that the item isn't a group as they can't be selected
|
||||
if(![self isGroupItem:item]) {
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldSelectItem:)]) {
|
||||
return [_secondaryDelegate sourceList:self shouldSelectItem:item];
|
||||
}
|
||||
}
|
||||
else {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
- (NSIndexSet *)outlineView:(NSOutlineView *)outlineView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes
|
||||
{
|
||||
//The outline view will try to select the first row if -[allowsEmptySelection:] is set to NO – if this is a group row
|
||||
//stop it from doing so and leave it to our implementation of-[reloadData] which will select the first non-group row
|
||||
//for us.
|
||||
if([self numberOfSelectedRows]==0) {
|
||||
if([self isGroupItem:[self itemAtRow:[proposedSelectionIndexes firstIndex]]]) {
|
||||
return [NSIndexSet indexSet];
|
||||
}
|
||||
}
|
||||
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:selectionIndexesForProposedSelection:)]) {
|
||||
return [_secondaryDelegate sourceList:self selectionIndexesForProposedSelection:proposedSelectionIndexes];
|
||||
}
|
||||
|
||||
//Since we implement this method, something must be returned to the outline view
|
||||
return proposedSelectionIndexes;
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
||||
{
|
||||
//Group titles can't be edited
|
||||
if([self isGroupItem:item])
|
||||
return NO;
|
||||
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldEditItem:)]) {
|
||||
return [_secondaryDelegate sourceList:self shouldEditItem:item];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
||||
{
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldTrackCell:forItem:)]) {
|
||||
return [_secondaryDelegate sourceList:self shouldTrackCell:cell forItem:item];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
|
||||
{
|
||||
if([_secondaryDelegate respondsToSelector:@selector(sourceList:heightOfRowByItem:)]) {
|
||||
return [_secondaryDelegate sourceList:self heightOfRowByItem:item];
|
||||
}
|
||||
|
||||
return [self rowHeight];
|
||||
}
|
||||
|
||||
- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item
|
||||
{
|
||||
return [self isGroupItem:item];
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Notification handling
|
||||
|
||||
/* Notification wrappers */
|
||||
- (void)outlineViewSelectionIsChanging:(NSNotification *)notification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PXSLSelectionIsChangingNotification object:self];
|
||||
}
|
||||
|
||||
|
||||
- (void)outlineViewSelectionDidChange:(NSNotification *)notification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PXSLSelectionDidChangeNotification object:self];
|
||||
}
|
||||
|
||||
- (void)outlineViewItemWillExpand:(NSNotification *)notification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemWillExpandNotification
|
||||
object:self
|
||||
userInfo:[notification userInfo]];
|
||||
}
|
||||
|
||||
- (void)outlineViewItemDidExpand:(NSNotification *)notification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemDidExpandNotification
|
||||
object:self
|
||||
userInfo:[notification userInfo]];
|
||||
}
|
||||
|
||||
- (void)outlineViewItemWillCollapse:(NSNotification *)notification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemWillCollapseNotification
|
||||
object:self
|
||||
userInfo:[notification userInfo]];
|
||||
}
|
||||
|
||||
- (void)outlineViewItemDidCollapse:(NSNotification *)notification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemDidCollapseNotification
|
||||
object:self
|
||||
userInfo:[notification userInfo]];
|
||||
}
|
||||
|
||||
- (void)registerDelegateToReceiveNotification:(NSString*)notification withSelector:(SEL)selector
|
||||
{
|
||||
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
|
||||
|
||||
//Set the delegate as a receiver of the notification if it implements the notification method
|
||||
if([_secondaryDelegate respondsToSelector:selector]) {
|
||||
[defaultCenter addObserver:_secondaryDelegate
|
||||
selector:selector
|
||||
name:notification
|
||||
object:self];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//MARK: -
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// PXSourceListDataSource.h
|
||||
// PXViewKit
|
||||
//
|
||||
// Created by Alex Rozanski on 17/10/2009.
|
||||
// Copyright 2009-10 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class PXSourceList;
|
||||
|
||||
@protocol PXSourceListDataSource <NSObject>
|
||||
|
||||
@required
|
||||
- (NSUInteger)sourceList:(PXSourceList*)sourceList numberOfChildrenOfItem:(id)item;
|
||||
- (id)sourceList:(PXSourceList*)aSourceList child:(NSUInteger)index ofItem:(id)item;
|
||||
- (id)sourceList:(PXSourceList*)aSourceList objectValueForItem:(id)item;
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList isItemExpandable:(id)item;
|
||||
|
||||
@optional
|
||||
- (void)sourceList:(PXSourceList*)aSourceList setObjectValue:(id)object forItem:(id)item;
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList itemHasBadge:(id)item;
|
||||
- (NSInteger)sourceList:(PXSourceList*)aSourceList badgeValueForItem:(id)item;
|
||||
- (NSColor*)sourceList:(PXSourceList*)aSourceList badgeTextColorForItem:(id)item;
|
||||
- (NSColor*)sourceList:(PXSourceList*)aSourceList badgeBackgroundColorForItem:(id)item;
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList itemHasIcon:(id)item;
|
||||
- (NSImage*)sourceList:(PXSourceList*)aSourceList iconForItem:(id)item;
|
||||
|
||||
- (NSMenu*)sourceList:(PXSourceList*)aSourceList contextMenuForItem:(id)item;
|
||||
|
||||
//The rest of these methods are basically "wrappers" for the NSOutlineViewDataSource methods
|
||||
- (id)sourceList:(PXSourceList*)aSourceList itemForPersistentObject:(id)object;
|
||||
- (id)sourceList:(PXSourceList*)aSourceList persistentObjectForItem:(id)item;
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard;
|
||||
- (NSDragOperation)sourceList:(PXSourceList*)sourceList validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index;
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index;
|
||||
- (NSArray *)sourceList:(PXSourceList*)aSourceList namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination forDraggedItems:(NSArray *)items;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// PXSourceListDelegate.h
|
||||
// PXViewKit
|
||||
//
|
||||
// Created by Alex Rozanski on 17/10/2009.
|
||||
// Copyright 2009-10 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class PXSourceList;
|
||||
|
||||
@protocol PXSourceListDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
//Extra methods
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList isGroupAlwaysExpanded:(id)group;
|
||||
|
||||
//Basically NSOutlineViewDelegate wrapper methods
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList shouldSelectItem:(id)item;
|
||||
- (NSIndexSet*)sourceList:(PXSourceList*)aSourceList selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes;
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList shouldEditItem:(id)item;
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList shouldTrackCell:(NSCell *)cell forItem:(id)item;
|
||||
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList shouldExpandItem:(id)item;
|
||||
- (BOOL)sourceList:(PXSourceList*)aSourceList shouldCollapseItem:(id)item;
|
||||
|
||||
- (CGFloat)sourceList:(PXSourceList*)aSourceList heightOfRowByItem:(id)item;
|
||||
|
||||
- (NSCell*)sourceList:(PXSourceList*)aSourceList dataCellForItem:(id)item;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSObject (PXSourceListNotifications)
|
||||
|
||||
//Selection
|
||||
- (void)sourceListSelectionIsChanging:(NSNotification *)notification;
|
||||
- (void)sourceListSelectionDidChange:(NSNotification *)notification;
|
||||
|
||||
//Item expanding/collapsing
|
||||
- (void)sourceListItemWillExpand:(NSNotification *)notification;
|
||||
- (void)sourceListItemDidExpand:(NSNotification *)notification;
|
||||
- (void)sourceListItemWillCollapse:(NSNotification *)notification;
|
||||
- (void)sourceListItemDidCollapse:(NSNotification *)notification;
|
||||
|
||||
- (void)sourceListDeleteKeyPressedOnRows:(NSNotification *)notification;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
//PXSourceList delegate notifications
|
||||
extern NSString * const PXSLSelectionIsChangingNotification;
|
||||
extern NSString * const PXSLSelectionDidChangeNotification;
|
||||
extern NSString * const PXSLItemWillExpandNotification;
|
||||
extern NSString * const PXSLItemDidExpandNotification;
|
||||
extern NSString * const PXSLItemWillCollapseNotification;
|
||||
extern NSString * const PXSLItemDidCollapseNotification;
|
||||
extern NSString * const PXSLDeleteKeyPressedOnRowsNotification;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// SourceListItem.h
|
||||
// PXSourceList
|
||||
//
|
||||
// Created by Alex Rozanski on 08/01/2010.
|
||||
// Copyright 2010 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
/*An example of a class that could be used to represent a Source List Item
|
||||
|
||||
Provides a title, an identifier, and an icon to be shown, as well as a badge value and a property to determine
|
||||
whether the current item has a badge or not (`badgeValue` is set to -1 if no badge is shown)
|
||||
|
||||
Used to form a hierarchical model of SourceListItem instances – similar to the Source List tree structure
|
||||
and easily accessible by the data source with the "children" property
|
||||
|
||||
SourceListItem *parent
|
||||
- SourceListItem *child1;
|
||||
- SourceListItem *child2;
|
||||
- SourceListItem *childOfChild2;
|
||||
- SourceListItem *anotherChildOfChild2;
|
||||
- SourceListItem *child3;
|
||||
|
||||
*/
|
||||
|
||||
@interface SourceListItem : NSObject {
|
||||
NSString *title;
|
||||
NSString *identifier;
|
||||
NSImage *icon;
|
||||
NSInteger badgeValue;
|
||||
|
||||
NSArray *children;
|
||||
}
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *identifier;
|
||||
@property (nonatomic, retain) NSImage *icon;
|
||||
@property NSInteger badgeValue;
|
||||
|
||||
@property (nonatomic, copy) NSArray *children;
|
||||
|
||||
//Convenience methods
|
||||
+ (id)itemWithTitle:(NSString*)aTitle identifier:(NSString*)anIdentifier;
|
||||
+ (id)itemWithTitle:(NSString*)aTitle identifier:(NSString*)anIdentifier icon:(NSImage*)anIcon;
|
||||
|
||||
|
||||
- (BOOL)hasBadge;
|
||||
- (BOOL)hasChildren;
|
||||
- (BOOL)hasIcon;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// SourceListItem.m
|
||||
// PXSourceList
|
||||
//
|
||||
// Created by Alex Rozanski on 08/01/2010.
|
||||
// Copyright 2010 Alex Rozanski http://perspx.com
|
||||
//
|
||||
|
||||
#import "SourceListItem.h"
|
||||
|
||||
|
||||
@implementation SourceListItem
|
||||
|
||||
@synthesize title;
|
||||
@synthesize identifier;
|
||||
@synthesize icon;
|
||||
@synthesize badgeValue;
|
||||
@synthesize children;
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Init/Dealloc
|
||||
|
||||
- (id)init
|
||||
{
|
||||
if(self=[super init])
|
||||
{
|
||||
badgeValue = -1; //We don't want a badge value by default
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
+ (id)itemWithTitle:(NSString*)aTitle identifier:(NSString*)anIdentifier
|
||||
{
|
||||
SourceListItem *item = [SourceListItem itemWithTitle:aTitle identifier:anIdentifier icon:nil];
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
+ (id)itemWithTitle:(NSString*)aTitle identifier:(NSString*)anIdentifier icon:(NSImage*)anIcon
|
||||
{
|
||||
SourceListItem *item = [[[SourceListItem alloc] init] autorelease];
|
||||
|
||||
[item setTitle:aTitle];
|
||||
[item setIdentifier:anIdentifier];
|
||||
[item setIcon:anIcon];
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[title release];
|
||||
[identifier release];
|
||||
[icon release];
|
||||
[children release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
//MARK: -
|
||||
|
||||
#pragma mark Custom Accessors
|
||||
|
||||
- (BOOL)hasBadge
|
||||
{
|
||||
return badgeValue!=-1;
|
||||
}
|
||||
|
||||
- (BOOL)hasChildren
|
||||
{
|
||||
return [children count]>0;
|
||||
}
|
||||
|
||||
- (BOOL)hasIcon
|
||||
{
|
||||
return icon!=nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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:rfc1034identifier}</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>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,353 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 45;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
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 */; };
|
||||
D205EDF010F7A609004C6545 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D205EDEF10F7A609004C6545 /* MainMenu.xib */; };
|
||||
D205EDF710F7A693004C6545 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D205EDF610F7A693004C6545 /* AppDelegate.m */; };
|
||||
D2456CAA10F8F7F500B8045C /* audiobooks.png in Resources */ = {isa = PBXBuildFile; fileRef = D2456CA410F8F7F500B8045C /* audiobooks.png */; };
|
||||
D2456CAB10F8F7F500B8045C /* movies.png in Resources */ = {isa = PBXBuildFile; fileRef = D2456CA510F8F7F500B8045C /* movies.png */; };
|
||||
D2456CAC10F8F7F500B8045C /* music.png in Resources */ = {isa = PBXBuildFile; fileRef = D2456CA610F8F7F500B8045C /* music.png */; };
|
||||
D2456CAD10F8F7F500B8045C /* playlist.png in Resources */ = {isa = PBXBuildFile; fileRef = D2456CA710F8F7F500B8045C /* playlist.png */; };
|
||||
D2456CAE10F8F7F500B8045C /* playlistFolder.png in Resources */ = {isa = PBXBuildFile; fileRef = D2456CA810F8F7F500B8045C /* playlistFolder.png */; };
|
||||
D2456CAF10F8F7F500B8045C /* podcasts.png in Resources */ = {isa = PBXBuildFile; fileRef = D2456CA910F8F7F500B8045C /* podcasts.png */; };
|
||||
D2456D1A10F9093800B8045C /* PXSourceList.m in Sources */ = {isa = PBXBuildFile; fileRef = D2456D1910F9093800B8045C /* PXSourceList.m */; };
|
||||
D265C2D710E3814B00A1E824 /* TODO.rtf in Resources */ = {isa = PBXBuildFile; fileRef = D265C2D610E3814B00A1E824 /* TODO.rtf */; };
|
||||
D2EA6EC210D9205A00402758 /* PXSourceList.m in Sources */ = {isa = PBXBuildFile; fileRef = D2EA6EC110D9205A00402758 /* PXSourceList.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
089C165DFE840E0CC02AAC07 /* InfoPlist.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = InfoPlist.strings; path = Resources/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>"; };
|
||||
256AC3F00F4B6AF500CF3369 /* PXSourceList_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PXSourceList_Prefix.pch; 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>"; };
|
||||
8D1107310486CEB800E47090 /* PXSourceList-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "PXSourceList-Info.plist"; sourceTree = "<group>"; };
|
||||
8D1107320486CEB800E47090 /* PXSourceList.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PXSourceList.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D205EDDD10F7A1EE004C6545 /* README.markdown */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.markdown; sourceTree = "<group>"; };
|
||||
D205EDEF10F7A609004C6545 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = Resources/MainMenu.xib; sourceTree = "<group>"; };
|
||||
D205EDF510F7A693004C6545 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Classes/AppDelegate.h; sourceTree = "<group>"; };
|
||||
D205EDF610F7A693004C6545 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Classes/AppDelegate.m; sourceTree = "<group>"; };
|
||||
D205EE7D10F7B5AB004C6545 /* SourceListItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SourceListItem.h; path = Classes/SourceListItem.h; sourceTree = "<group>"; };
|
||||
D2456CA410F8F7F500B8045C /* audiobooks.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = audiobooks.png; path = Resources/audiobooks.png; sourceTree = "<group>"; };
|
||||
D2456CA510F8F7F500B8045C /* movies.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = movies.png; path = Resources/movies.png; sourceTree = "<group>"; };
|
||||
D2456CA610F8F7F500B8045C /* music.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = music.png; path = Resources/music.png; sourceTree = "<group>"; };
|
||||
D2456CA710F8F7F500B8045C /* playlist.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = playlist.png; path = Resources/playlist.png; sourceTree = "<group>"; };
|
||||
D2456CA810F8F7F500B8045C /* playlistFolder.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = playlistFolder.png; path = Resources/playlistFolder.png; sourceTree = "<group>"; };
|
||||
D2456CA910F8F7F500B8045C /* podcasts.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = podcasts.png; path = Resources/podcasts.png; sourceTree = "<group>"; };
|
||||
D2456D1910F9093800B8045C /* PXSourceList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PXSourceList.m; path = Classes/PXSourceList.m; sourceTree = "<group>"; };
|
||||
D265C2D610E3814B00A1E824 /* TODO.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = TODO.rtf; sourceTree = "<group>"; };
|
||||
D2EA6EC010D9205A00402758 /* PXSourceList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PXSourceList.h; path = Classes/PXSourceList.h; sourceTree = "<group>"; };
|
||||
D2EA6EC110D9205A00402758 /* PXSourceList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PXSourceList.m; path = Classes/PXSourceList.m; sourceTree = "<group>"; };
|
||||
D2EA6EFF10D925AA00402758 /* PXSourceListDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PXSourceListDataSource.h; path = Classes/PXSourceListDataSource.h; sourceTree = "<group>"; };
|
||||
D2EA6F0010D925AA00402758 /* PXSourceListDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PXSourceListDelegate.h; path = Classes/PXSourceListDelegate.h; 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 = (
|
||||
D205EDF510F7A693004C6545 /* AppDelegate.h */,
|
||||
D205EDF610F7A693004C6545 /* AppDelegate.m */,
|
||||
D205EE7D10F7B5AB004C6545 /* SourceListItem.h */,
|
||||
D2456D1910F9093800B8045C /* PXSourceList.m */,
|
||||
D2EA702910D92EB300402758 /* PXSourceList */,
|
||||
);
|
||||
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 /* PXSourceList.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97314FDCFA39411CA2CEA /* PXSourceList */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D265C2D610E3814B00A1E824 /* TODO.rtf */,
|
||||
D205EDDD10F7A1EE004C6545 /* README.markdown */,
|
||||
080E96DDFE201D6D7F000001 /* Classes */,
|
||||
29B97315FDCFA39411CA2CEA /* Other Sources */,
|
||||
29B97317FDCFA39411CA2CEA /* Resources */,
|
||||
29B97323FDCFA39411CA2CEA /* Frameworks */,
|
||||
19C28FACFE9D520D11CA2CBB /* Products */,
|
||||
);
|
||||
name = PXSourceList;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97315FDCFA39411CA2CEA /* Other Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
256AC3F00F4B6AF500CF3369 /* PXSourceList_Prefix.pch */,
|
||||
29B97316FDCFA39411CA2CEA /* main.m */,
|
||||
);
|
||||
name = "Other Sources";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97317FDCFA39411CA2CEA /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D27B7D2710E0C925004D7DBC /* Icons */,
|
||||
8D1107310486CEB800E47090 /* PXSourceList-Info.plist */,
|
||||
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
|
||||
D205EDEF10F7A609004C6545 /* MainMenu.xib */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97323FDCFA39411CA2CEA /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */,
|
||||
1058C7A2FEA54F0111CA2CBB /* Other Frameworks */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D27B7D2710E0C925004D7DBC /* Icons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D2456CA410F8F7F500B8045C /* audiobooks.png */,
|
||||
D2456CA510F8F7F500B8045C /* movies.png */,
|
||||
D2456CA610F8F7F500B8045C /* music.png */,
|
||||
D2456CA710F8F7F500B8045C /* playlist.png */,
|
||||
D2456CA810F8F7F500B8045C /* playlistFolder.png */,
|
||||
D2456CA910F8F7F500B8045C /* podcasts.png */,
|
||||
);
|
||||
name = Icons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D2EA702910D92EB300402758 /* PXSourceList */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D2EA6EC010D9205A00402758 /* PXSourceList.h */,
|
||||
D2EA6EC110D9205A00402758 /* PXSourceList.m */,
|
||||
D2EA6EFF10D925AA00402758 /* PXSourceListDataSource.h */,
|
||||
D2EA6F0010D925AA00402758 /* PXSourceListDelegate.h */,
|
||||
);
|
||||
name = PXSourceList;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
8D1107260486CEB800E47090 /* PXSourceList */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "PXSourceList" */;
|
||||
buildPhases = (
|
||||
8D1107290486CEB800E47090 /* Resources */,
|
||||
8D11072C0486CEB800E47090 /* Sources */,
|
||||
8D11072E0486CEB800E47090 /* Frameworks */,
|
||||
D205F09310F7D40E004C6545 /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = PXSourceList;
|
||||
productInstallPath = "$(HOME)/Applications";
|
||||
productName = PXSourceList;
|
||||
productReference = 8D1107320486CEB800E47090 /* PXSourceList.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 "PXSourceList" */;
|
||||
compatibilityVersion = "Xcode 3.1";
|
||||
hasScannedForEncodings = 1;
|
||||
mainGroup = 29B97314FDCFA39411CA2CEA /* PXSourceList */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
8D1107260486CEB800E47090 /* PXSourceList */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
8D1107290486CEB800E47090 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
|
||||
D265C2D710E3814B00A1E824 /* TODO.rtf in Resources */,
|
||||
D205EDF010F7A609004C6545 /* MainMenu.xib in Resources */,
|
||||
D2456CAA10F8F7F500B8045C /* audiobooks.png in Resources */,
|
||||
D2456CAB10F8F7F500B8045C /* movies.png in Resources */,
|
||||
D2456CAC10F8F7F500B8045C /* music.png in Resources */,
|
||||
D2456CAD10F8F7F500B8045C /* playlist.png in Resources */,
|
||||
D2456CAE10F8F7F500B8045C /* playlistFolder.png in Resources */,
|
||||
D2456CAF10F8F7F500B8045C /* podcasts.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
D205F09310F7D40E004C6545 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "DOXYGEN=/Users/alexrozanski/Development/Xcode/OSX/Documentation/doxygen\nDOXYFILE=$SOURCE_ROOT/Doxyfile\n\nAPPLEDOC=/Users/alexrozanski/Development/Xcode/osx/Documentation/appledoc\nSTRIPDTAGS=/Users/alexrozanski/Development/Xcode/osx/Documentation/StripDoxygenTags\n\n#THE OUTPUT DIRECTORY HAS ITS CONTENTS REMOVED ON BUILD!\n\n#$APPLEDOC -p \"$PROJECT_NAME\" -i $SOURCE_ROOT -o $SOURCE_ROOT/documentation/output -t \"/Library/Application Support/appledoc\" -c $SOURCE_ROOT/Doxyfile --date-time-template \"Last updated: %Y-%m-%d\" --xhtml\n\n#Strip the Doxygen tags\n\n#$STRIPDTAGS -i $SOURCE_ROOT -o /Users/alexrozanski/Development/Xcode/osx/Github/PXSourceList\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
8D11072C0486CEB800E47090 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8D11072D0486CEB800E47090 /* main.m in Sources */,
|
||||
D2EA6EC210D9205A00402758 /* PXSourceList.m in Sources */,
|
||||
D205EDF710F7A693004C6545 /* AppDelegate.m in Sources */,
|
||||
D2456D1A10F9093800B8045C /* PXSourceList.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
089C165DFE840E0CC02AAC07 /* InfoPlist.strings */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
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 = PXSourceList_Prefix.pch;
|
||||
INFOPLIST_FILE = "PXSourceList-Info.plist";
|
||||
INSTALL_PATH = "$(HOME)/Applications";
|
||||
PRODUCT_NAME = PXSourceList;
|
||||
};
|
||||
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 = PXSourceList_Prefix.pch;
|
||||
INFOPLIST_FILE = "PXSourceList-Info.plist";
|
||||
INSTALL_PATH = "$(HOME)/Applications";
|
||||
PRODUCT_NAME = PXSourceList;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C01FCF4F08A954540054247B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD_32_64_BIT)";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PREBINDING = NO;
|
||||
SDKROOT = macosx10.6;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C01FCF5008A954540054247B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD_32_64_BIT)";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
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 "PXSourceList" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C01FCF4B08A954540054247B /* Debug */,
|
||||
C01FCF4C08A954540054247B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "PXSourceList" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C01FCF4F08A954540054247B /* Debug */,
|
||||
C01FCF5008A954540054247B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 29B97313FDCFA39411CA2CEA /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//
|
||||
// Prefix header for all source files of the 'PXSourceList' target in the 'PXSourceList' project
|
||||
//
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#endif
|
||||
@@ -0,0 +1,60 @@
|
||||
#PXSourceList
|
||||
|
||||
A Source List control for use with the Mac OS X 10.5 SDK or above.
|
||||
|
||||
[Download the documentation][1]
|
||||
|
||||
##Intention
|
||||
[Source Lists][2] are used in a lot of Mac OS X applications, but the support for such controls is quite primitive – at best you create an Outline View with Source List highlighting, but none of the features such as badging are built in by default. `PXSourceList` is a reusable control – within the context of Source Lists – which makes creating applications with Source Lists a much easier process.
|
||||
|
||||
##Using the code
|
||||
There are only a few steps involved:
|
||||
|
||||
1. Download the source, and copy `PXSourceList.h`, `PXSourceList.m`, `PXSourceListDelegate.h` and `PXSourceListDataSource.h` into your Xcode project.
|
||||
2. To create the control in Interface Builder, drag an `NSOutlineView` object over to a window and in the Identity Inspector for the Outline View, change the class to `PXSourceList`. In the Attributes Inspector, set it to have only 1 column, uncheck "Headers" in the "Columns" section and set "Highlight" to "Source List" – there is an NIB in the example project bundled with the source.
|
||||
3. Make sure to `#import "PXSourceList.h"` for files that require it (the delegate and data source protocol files are imported in this main header), and ensure that your class(es) that are the `delegate` and/or `dataSource` for the Source List conform to the `PXSourceListDelegate` and `PXSourceListDataSource` protocols respectively.
|
||||
|
||||
**Note:** If you intend to use PXSourceList with the 10.5 SDK, you will need to remove some of the protocols that PXSourceList conforms to which do not exist – in `PXSourceList.h`, remove the `<NSOutlineViewDelegate, NSOutlineViewDataSource, NSMenuDelegate>` from the interface declaration.
|
||||
|
||||
There is also an example project bundled with the source to see how the control is used.
|
||||
|
||||
##How the control works
|
||||
I have tried to structure PXSourceList in a way such that it fits common Cocoa design patterns and therefore makes it easier to use.
|
||||
|
||||
PXSourceList adapts the delegate and data source design patterns, and extends those of the `NSOutlineViewDelegate` and `NSOutlineViewDataSource`, much in the way that these extend the appropriate `NSTableView` protocols.
|
||||
|
||||
If you want more information have a look at the [Outline View Programming Topics for Cocoa][3] – the Source List delegate and data source implementation work in much the same way, but with methods added and removed, as detailed in the documentation.
|
||||
|
||||
##Documentation
|
||||
Documentation is available for PXSourceList, downloadable [here][4]. Provided in the ZIP file is a folder containing HTML documentation, or a docset which can be opened in Xcode and which is then searchable from the Xcode Developer Documentation.
|
||||
|
||||
If you feel that any areas of the documentation are lacking or missing, please feel free to [let me know][5], which will be much appreciated.
|
||||
|
||||
**Note:** the documentation for the Source List notifications can be found in the `NSObject(PXSourceListNotifications)` reference, which is linked to from the documentation index page.
|
||||
|
||||
##Known Issues
|
||||
|
||||
- Calling `delegate` or `dataSource` on the Source List returns the Source List instance. This is, unfortunately, a side effect of how delegate and data source methods are handled within the Source List – I hope to work around it in the future.
|
||||
- No bindings implementation (yet).
|
||||
|
||||
##Attribution
|
||||
|
||||
I was spurred along the way by many sources, but in particular [BWToolkit][6] by Brandon Walkin and Fraser Kuyvenhoven, which gave me the idea of how to handle the Source List delegate and data source methods.
|
||||
|
||||
Also Brian Dunagan's post on [Source List badging][7] and determining state for the various colours was a great help when I came to the drawing code for that.
|
||||
|
||||
The documentation was created using [Doxygen][8] and [appledoc][9], thanks of which go to the developers of both.
|
||||
|
||||
The icons used in the demo project bundled with the source code are from the [Fugue icon set][10] by Yusuke Kamiyamane.
|
||||
|
||||
|
||||
[1]: http://perspx.com/wp-content/uploads/2010/01/PXSourceList_0-7_Documentation.zip
|
||||
[2]: http://developer.apple.com/Mac/library/documentation/UserExperience/Conceptual/AppleHIGuidelines/XHIGWindows/XHIGWindows.html#//apple_ref/doc/uid/20000961-CHDDIGDE
|
||||
[3]: http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/OutlineView/Articles/UsingOutlineDataSource.html
|
||||
[4]: http://perspx.com/wp-content/uploads/2010/01/PXSourceList_0-7_Documentation.zip
|
||||
[5]: http://perspx.com/contact
|
||||
[6]: http://brandonwalkin.com/bwtoolkit/
|
||||
[7]: http://www.bdunagan.com/2008/11/10/cocoa-tutorial-source-list-badges-part-2/
|
||||
[8]: http://www.doxygen.org/
|
||||
[9]: http://www.gentlebytes.com/freeware/appledoc/
|
||||
[10]: http://www.pinvoke.com/
|
||||
@@ -0,0 +1,12 @@
|
||||
<?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>CFBundleIdentifier</key>
|
||||
<string>com.custom.PXSourceList.docset</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>PXSourceList</string>
|
||||
<key>DocSetFeedName</key>
|
||||
<string>Custom documentation</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 415 B |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 654 B |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 721 B |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 671 B |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 511 B |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 702 B |
@@ -0,0 +1,13 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf250
|
||||
{\fonttbl\f0\fnil\fcharset0 Monaco;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural
|
||||
|
||||
\f0\fs20 \cf0 \strike \strikec0 1. Source List badge colours for Graphite theme set in system preferences\strike0\striked0 \
|
||||
2. Implement custom drawing for drag and drop\
|
||||
\strike \strikec0 3. Sort out way that Source List returns badge values \'96 use NSNumber?\strike0\striked0 \
|
||||
- No, return NSNotFound for -badgeValueForItem: instead if item doesn't have badge \
|
||||
4. Provide a bindings interface\
|
||||
5. Cross-group keyboard implementation?\
|
||||
6. Provide an isGroupItem: delegate method?\
|
||||
7. Change the way the Source List handles drag and drop to something more like iTunes/Finder?}
|
||||
Reference in New Issue
Block a user