]> git.sesse.net Git - vlc/blob - modules/gui/macosx/PXSourceList.m
macosx: added 'subsdelay' to the track synchro panel and removed the bogus, non HIG...
[vlc] / modules / gui / macosx / PXSourceList.m
1 //
2 //  PXSourceList.m
3 //  PXSourceList
4 //
5 //  Created by Alex Rozanski on 05/09/2009.
6 //  Copyright 2009-10 Alex Rozanski http://perspx.com
7 //
8 //  GC-enabled code revised by Stefan Vogt http://byteproject.net
9 //
10
11 #import "PXSourceList.h"
12 #import "SideBarItem.h"
13
14 //Layout constants
15 #define MIN_BADGE_WIDTH     22.0            //The minimum badge width for each item (default 22.0)
16 #define BADGE_HEIGHT        14.0            //The badge height for each item (default 14.0)
17 #define BADGE_MARGIN        5.0             //The spacing between the badge and the cell for that row
18 #define ROW_RIGHT_MARGIN    5.0             //The spacing between the right edge of the badge and the edge of the table column
19 #define ICON_SPACING        2.0             //The spacing between the icon and it's adjacent cell
20 #define DISCLOSURE_TRIANGLE_SPACE   18.0    //The indentation reserved for disclosure triangles for non-group items
21
22 //Drawing constants
23 #define BADGE_BACKGROUND_COLOR              [NSColor colorWithCalibratedRed:(152/255.0) green:(168/255.0) blue:(202/255.0) alpha:1]
24 #define BADGE_HIDDEN_BACKGROUND_COLOR       [NSColor colorWithDeviceWhite:(180/255.0) alpha:1]
25 #define BADGE_SELECTED_TEXT_COLOR           [NSColor keyboardFocusIndicatorColor]
26 #define BADGE_SELECTED_UNFOCUSED_TEXT_COLOR [NSColor colorWithCalibratedRed:(153/255.0) green:(169/255.0) blue:(203/255.0) alpha:1]
27 #define BADGE_SELECTED_HIDDEN_TEXT_COLOR    [NSColor colorWithCalibratedWhite:(170/255.0) alpha:1]
28 #define BADGE_FONT                          [NSFont boldSystemFontOfSize:11]
29
30 //Delegate notification constants
31 NSString * const PXSLSelectionIsChangingNotification = @"PXSourceListSelectionIsChanging";
32 NSString * const PXSLSelectionDidChangeNotification = @"PXSourceListSelectionDidChange";
33 NSString * const PXSLItemWillExpandNotification = @"PXSourceListItemWillExpand";
34 NSString * const PXSLItemDidExpandNotification = @"PXSourceListItemDidExpand";
35 NSString * const PXSLItemWillCollapseNotification = @"PXSourceListItemWillCollapse";
36 NSString * const PXSLItemDidCollapseNotification = @"PXSourceListItemDidCollapse";
37 NSString * const PXSLDeleteKeyPressedOnRowsNotification = @"PXSourceListDeleteKeyPressedOnRows";
38
39 #pragma mark -
40 @interface PXSourceList ()
41
42 - (NSSize)sizeOfBadgeAtRow:(NSInteger)rowIndex;
43 - (void)drawBadgeForRow:(NSInteger)rowIndex inRect:(NSRect)badgeFrame;
44 - (void)registerDelegateToReceiveNotification:(NSString*)notification withSelector:(SEL)selector;
45
46 @end
47
48 #pragma mark -
49 @implementation PXSourceList
50
51 @synthesize iconSize = _iconSize;
52 @dynamic dataSource;
53 @dynamic delegate;
54
55 #pragma mark Init/Dealloc/Finalize
56
57 - (id)initWithCoder:(NSCoder*)decoder
58 {
59     if(self=[super initWithCoder:decoder])
60     {
61         [self setDelegate:(id<PXSourceListDelegate>)[super delegate]];
62         [super setDelegate:self];
63         [self setDataSource:(id<PXSourceListDataSource>)[super dataSource]];
64         [super setDataSource:self];
65
66         _iconSize = NSMakeSize(16,16);
67     }
68
69     return self;
70 }
71
72 - (void)dealloc
73 {
74     //Remove ourselves as the delegate and data source to be safe
75     [super setDataSource:nil];
76     [super setDelegate:nil];
77
78     //Unregister the delegate from receiving notifications
79     [[NSNotificationCenter defaultCenter] removeObserver:_secondaryDelegate name:nil object:self];
80
81     [super dealloc];
82 }
83
84 - (void)finalize
85 {
86     //Remove ourselves as the delegate and data source to be safe
87     [super setDataSource:nil];
88     [super setDelegate:nil];
89
90     //Unregister the delegate from receiving notifications
91     [[NSNotificationCenter defaultCenter] removeObserver:_secondaryDelegate name:nil object:self];
92
93     [super finalize];
94 }
95
96 #pragma mark -
97 #pragma mark Custom Accessors
98
99 - (void)setDelegate:(id<PXSourceListDelegate>)aDelegate
100 {
101     //Unregister the old delegate from receiving notifications
102     [[NSNotificationCenter defaultCenter] removeObserver:_secondaryDelegate name:nil object:self];
103
104     _secondaryDelegate = aDelegate;
105     //Register the new delegate to receive notifications
106     [self registerDelegateToReceiveNotification:PXSLSelectionIsChangingNotification withSelector:@selector(sourceListSelectionIsChanging:)];
107     [self registerDelegateToReceiveNotification:PXSLSelectionDidChangeNotification withSelector:@selector(sourceListSelectionDidChange:)];
108     [self registerDelegateToReceiveNotification:PXSLItemWillExpandNotification withSelector:@selector(sourceListItemWillExpand:)];
109     [self registerDelegateToReceiveNotification:PXSLItemDidExpandNotification withSelector:@selector(sourceListItemDidExpand:)];
110     [self registerDelegateToReceiveNotification:PXSLItemWillCollapseNotification withSelector:@selector(sourceListItemWillCollapse:)];
111     [self registerDelegateToReceiveNotification:PXSLItemDidCollapseNotification withSelector:@selector(sourceListItemDidCollapse:)];
112     [self registerDelegateToReceiveNotification:PXSLDeleteKeyPressedOnRowsNotification withSelector:@selector(sourceListDeleteKeyPressedOnRows:)];
113 }
114
115
116 - (void)setDataSource:(id<PXSourceListDataSource>)aDataSource
117 {
118     _secondaryDataSource = aDataSource;
119
120     [self reloadData];
121 }
122
123 - (void)setIconSize:(NSSize)newIconSize
124 {
125     _iconSize = newIconSize;
126     CGFloat rowHeight = [self rowHeight];
127
128     //Make sure icon height does not exceed row height; if so constrain, keeping width and height in proportion
129     if(_iconSize.height>rowHeight)
130     {
131         _iconSize.width = _iconSize.width * (rowHeight/_iconSize.height);
132         _iconSize.height = rowHeight;
133     }
134 }
135
136 #pragma mark -
137 #pragma mark Data Management
138
139 - (void)reloadData
140 {
141     [super reloadData];
142
143     //Expand items that are displayed as always expanded
144     if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)] &&
145        [_secondaryDelegate respondsToSelector:@selector(sourceList:isGroupAlwaysExpanded:)])
146     {
147         for(NSUInteger i=0;i<[self numberOfGroups];i++)
148         {
149             id item = [_secondaryDataSource sourceList:self child:i ofItem:nil];
150
151             if([self isGroupAlwaysExpanded:item]) {
152                 [self expandItem:item expandChildren:NO];
153             }
154         }
155     }
156
157     //If there are selected rows and the item hierarchy has changed, make sure a Group row isn't
158     //selected
159     if([self numberOfSelectedRows]>0) {
160         NSIndexSet *selectedIndexes = [self selectedRowIndexes];
161         NSUInteger firstSelectedRow = [selectedIndexes firstIndex];
162
163         //Is a group item selected?
164         if([self isGroupItem:[self itemAtRow:firstSelectedRow]]) {
165             //Work backwards to find the first non-group row
166             BOOL foundRow = NO;
167             for(NSUInteger i=firstSelectedRow;i>0;i--)
168             {
169                 if(![self isGroupItem:[self itemAtRow:i]]) {
170                     [self selectRowIndexes:[NSIndexSet indexSetWithIndex:i] byExtendingSelection:NO];
171                     foundRow = YES;
172                     break;
173                 }
174             }
175
176             //If there is no non-group row preceding the currently selected group item, remove the selection
177             //from the Source List
178             if(!foundRow) {
179                 [self deselectAll:self];
180             }
181         }
182     }
183     else if(![self allowsEmptySelection]&&[self numberOfSelectedRows]==0)
184     {
185         //Select the first non-group row if no rows are selected, and empty selection is disallowed
186         for(NSUInteger i=0;i<[self numberOfRows];i++)
187         {
188             if(![self isGroupItem:[self itemAtRow:i]]) {
189                 [self selectRowIndexes:[NSIndexSet indexSetWithIndex:i] byExtendingSelection:NO];
190                 break;
191             }
192         }
193     }
194 }
195
196 - (NSUInteger)numberOfGroups
197 {
198     if([_secondaryDataSource respondsToSelector:@selector(sourceList:numberOfChildrenOfItem:)]) {
199         return [_secondaryDataSource sourceList:self numberOfChildrenOfItem:nil];
200     }
201     return 0;
202 }
203
204
205 - (BOOL)isGroupItem:(id)item
206 {
207     //Groups are defined as root items (at level 0)
208     return 0==[self levelForItem:item];
209 }
210
211
212 - (BOOL)isGroupAlwaysExpanded:(id)group
213 {
214     //Make sure that the item IS a group to prevent unwanted queries sent to the data source
215     if([self isGroupItem:group]) {
216         //Query the data source
217         if([_secondaryDelegate respondsToSelector:@selector(sourceList:isGroupAlwaysExpanded:)]) {
218             return [_secondaryDelegate sourceList:self isGroupAlwaysExpanded:group];
219         }
220     }
221
222     return NO;
223 }
224
225
226 - (BOOL)itemHasBadge:(id)item
227 {
228     if([_secondaryDataSource respondsToSelector:@selector(sourceList:itemHasBadge:)]) {
229         return [_secondaryDataSource sourceList:self itemHasBadge:item];
230     }
231
232     return NO;
233 }
234
235 - (NSInteger)badgeValueForItem:(id)item
236 {
237     //Make sure that the item has a badge
238     if(![self itemHasBadge:item]) {
239         return NSNotFound;
240     }
241
242     if([_secondaryDataSource respondsToSelector:@selector(sourceList:badgeValueForItem:)]) {
243         return [_secondaryDataSource sourceList:self badgeValueForItem:item];
244     }
245
246     return NSNotFound;
247 }
248
249 #pragma mark -
250 #pragma mark Selection Handling
251
252 - (void)selectRowIndexes:(NSIndexSet*)indexes byExtendingSelection:(BOOL)extend
253 {
254     NSUInteger numberOfIndexes = [indexes count];
255
256     //Prevent empty selection if we don't want it
257     if(![self allowsEmptySelection]&&0==numberOfIndexes) {
258         return;
259     }
260
261     //Would use blocks but we're also targeting 10.5...
262     //Get the selected indexes
263     NSUInteger *selectedIndexes = malloc(sizeof(NSUInteger)*numberOfIndexes);
264     [indexes getIndexes:selectedIndexes maxCount:numberOfIndexes inIndexRange:nil];
265
266     //Loop through the indexes and only add non-group row indexes
267     //Allows selection across groups without selecting the group rows
268     NSMutableIndexSet *newSelectionIndexes = [NSMutableIndexSet indexSet];
269     for(NSInteger i=0;i<numberOfIndexes;i++)
270     {
271         if(![self isGroupItem:[self itemAtRow:selectedIndexes[i]]]) {
272             [newSelectionIndexes addIndex:selectedIndexes[i]];
273         }
274     }
275
276     //If there are any non-group rows selected
277     if([newSelectionIndexes count]>0) {
278         [super selectRowIndexes:newSelectionIndexes byExtendingSelection:extend];
279     }
280
281     //C memory management... *sigh*
282     free(selectedIndexes);
283 }
284
285 #pragma mark -
286 #pragma mark Layout
287
288 - (NSRect)frameOfOutlineCellAtRow:(NSInteger)row
289 {
290     //Return a zero-rect if the item is always expanded (a disclosure triangle will not be drawn)
291     if([self isGroupAlwaysExpanded:[self itemAtRow:row]]) {
292         return NSZeroRect;
293     }
294
295     return [super frameOfOutlineCellAtRow:row];
296 }
297
298
299 - (NSRect)frameOfCellAtColumn:(NSInteger)column row:(NSInteger)row
300 {
301     id item = [self itemAtRow:row];
302
303     NSCell *cell = [self preparedCellAtColumn:column row:row];
304     NSSize cellSize = [cell cellSize];
305     if (!([cell type] == NSImageCellType) && !([cell type] == NSTextCellType))
306         cellSize = [cell cellSizeForBounds:[super frameOfCellAtColumn:column row:row]];
307
308     NSRect cellFrame = [super frameOfCellAtColumn:column row:row];
309     NSRect rowRect = [self rectOfRow:row];
310
311     if([self isGroupItem:item])
312     {
313         CGFloat minX = NSMinX(cellFrame);
314
315         //Set the origin x-coord; if there are no children of the group at current, there will still be a
316         //margin to the left of the cell (in cellFrame), which we don't want
317         if([self isGroupAlwaysExpanded:[self itemAtRow:row]]) {
318             minX = 7;
319         }
320
321     return NSMakeRect(minX, NSMidY(cellFrame)-(cellSize.height/2.0), NSWidth(rowRect)-minX, cellSize.height);
322     }
323     else
324     {
325         CGFloat leftIndent = ([self levelForRow:row] -1)*[self indentationPerLevel]+DISCLOSURE_TRIANGLE_SPACE;
326
327         //Calculate space left for a badge if need be
328         CGFloat rightIndent = [self sizeOfBadgeAtRow:row].width+ROW_RIGHT_MARGIN;
329
330         //Allow space for an icon if need be
331         if(![self isGroupItem:item]&&[_secondaryDataSource respondsToSelector:@selector(sourceList:itemHasIcon:)])
332         {
333             if([_secondaryDataSource sourceList:self itemHasIcon:item]) {
334                 leftIndent += [self iconSize].width+(ICON_SPACING*2);
335             }
336         }
337     return NSMakeRect(leftIndent, NSMidY(rowRect)-(cellSize.height/2.0), NSWidth(rowRect)-rightIndent-leftIndent, cellSize.height);
338     }
339 }
340
341
342 //This method calculates and returns the size of the badge for the row index passed to the method. If the
343 //row for the row index passed to the method does not have a badge, then NSZeroSize is returned.
344 - (NSSize)sizeOfBadgeAtRow:(NSInteger)rowIndex
345 {
346     id rowItem = [self itemAtRow:rowIndex];
347
348     //Make sure that the item has a badge
349     if(![self itemHasBadge:rowItem]) {
350         return NSZeroSize;
351     }
352
353     NSAttributedString *badgeAttrString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d", [self badgeValueForItem:rowItem]] attributes:[NSDictionary dictionaryWithObjectsAndKeys:BADGE_FONT, NSFontAttributeName, nil]];
354     NSSize stringSize = [badgeAttrString size];
355
356     //Calculate the width needed to display the text or the minimum width if it's smaller
357     CGFloat width = stringSize.width+(2*BADGE_MARGIN);
358
359     if(width<MIN_BADGE_WIDTH) {
360         width = MIN_BADGE_WIDTH;
361     }
362
363     [badgeAttrString release];
364
365     return NSMakeSize(width, BADGE_HEIGHT);
366 }
367
368
369 #pragma mark -
370 #pragma mark Drawing
371
372 - (void)drawRow:(NSInteger)rowIndex clipRect:(NSRect)clipRect
373 {
374     [super drawRow:rowIndex clipRect:clipRect];
375     id item = [self itemAtRow:rowIndex];
376
377     //Draw an icon if the item has one
378     if(![self isGroupItem:item]&&[_secondaryDataSource respondsToSelector:@selector(sourceList:itemHasIcon:)])
379     {
380         if([_secondaryDataSource sourceList:self itemHasIcon:item])
381         {
382             NSRect cellFrame = [self frameOfCellAtColumn:0 row:rowIndex];
383             NSSize iconSize = [self iconSize];
384             NSRect iconRect = NSMakeRect(NSMinX(cellFrame)-iconSize.width-ICON_SPACING, NSMidY(cellFrame)-(iconSize.width/2.0f), iconSize.width, iconSize.height);
385
386             if([_secondaryDataSource respondsToSelector:@selector(sourceList:iconForItem:)])
387             {
388                 NSImage *icon = [_secondaryDataSource sourceList:self iconForItem:item];
389                 if(icon!=nil)
390                 {
391                     NSSize actualIconSize = [icon size];
392                     //If the icon is *smaller* than the size retrieved from the -iconSize property, make sure we
393                     //reduce the size of the rectangle to draw the icon in, so that it is not stretched.
394                     if((actualIconSize.width<iconSize.width)||(actualIconSize.height<iconSize.height))
395                     {
396                         iconRect = NSMakeRect(NSMidX(iconRect)-(actualIconSize.width/2.0f), NSMidY(iconRect)-(actualIconSize.height/2.0f), actualIconSize.width, actualIconSize.height);
397                     }
398
399                     //Use 10.6 NSImage drawing if we can
400                     if([icon respondsToSelector:@selector(drawInRect:fromRect:operation:fraction:respectFlipped:hints:)]) {
401                         [icon drawInRect:iconRect
402                                 fromRect:NSZeroRect
403                                operation:NSCompositeSourceOver
404                                 fraction:1
405                           respectFlipped:YES hints:nil];
406                     }
407                     else {
408                         [icon setFlipped:[self isFlipped]];
409                         [icon drawInRect:iconRect
410                                 fromRect:NSZeroRect
411                                operation:NSCompositeSourceOver
412                                 fraction:1];
413                     }
414                 }
415             }
416         }
417     }
418
419     //Draw the badge if the item has one
420     if([self itemHasBadge:item])
421     {
422         NSRect rowRect = [self rectOfRow:rowIndex];
423         NSSize badgeSize = [self sizeOfBadgeAtRow:rowIndex];
424
425         NSRect badgeFrame = NSMakeRect(NSMaxX(rowRect)-badgeSize.width-ROW_RIGHT_MARGIN,
426                                        NSMidY(rowRect)-(badgeSize.height/2.0),
427                                        badgeSize.width,
428                                        badgeSize.height);
429         [self drawBadgeForRow:rowIndex inRect:badgeFrame];
430     }
431 }
432
433 - (void)drawBadgeForRow:(NSInteger)rowIndex inRect:(NSRect)badgeFrame
434 {
435     id rowItem = [self itemAtRow:rowIndex];
436     NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeFrame
437                                                               xRadius:(BADGE_HEIGHT/2.0)
438                                                               yRadius:(BADGE_HEIGHT/2.0)];
439
440     //Get window and control state to determine colours used
441     BOOL isVisible = [[NSApp mainWindow] isVisible];
442     BOOL isFocused = [[[self window] firstResponder] isEqual:self];
443     NSInteger rowBeingEdited = [self editedRow];
444
445     //Set the attributes based on the row state
446     NSDictionary *attributes;
447     NSColor *backgroundColor;
448
449     if([[self selectedRowIndexes] containsIndex:rowIndex])
450     {
451         backgroundColor = [NSColor whiteColor];
452         //Set the text color based on window and control state
453         NSColor *textColor;
454         if(isVisible && (isFocused || rowBeingEdited==rowIndex)) {
455             textColor = BADGE_SELECTED_TEXT_COLOR;
456         }
457         else if(isVisible && !isFocused) {
458             textColor = BADGE_SELECTED_UNFOCUSED_TEXT_COLOR;
459         }
460         else {
461             textColor = BADGE_SELECTED_HIDDEN_TEXT_COLOR;
462         }
463
464         attributes = [[NSDictionary alloc] initWithObjectsAndKeys:BADGE_FONT, NSFontAttributeName, textColor, NSForegroundColorAttributeName, nil];
465     }
466     else
467     {
468         //Set the text colour based on window and control state
469         NSColor *badgeColor = [NSColor whiteColor];
470
471         if(isVisible) {
472             //If the data source returns a custom colour..
473             if([_secondaryDataSource respondsToSelector:@selector(sourceList:badgeBackgroundColorForItem:)]) {
474                 backgroundColor = [_secondaryDataSource sourceList:self badgeBackgroundColorForItem:rowItem];
475
476             if(backgroundColor==nil)
477                 backgroundColor = BADGE_BACKGROUND_COLOR;
478         }
479         else { //Otherwise use the default (purple-blue colour)
480             backgroundColor = BADGE_BACKGROUND_COLOR;
481         }
482
483         //If the delegate wants a custom badge text colour..
484         if([_secondaryDataSource respondsToSelector:@selector(sourceList:badgeTextColorForItem:)]) {
485             badgeColor = [_secondaryDataSource sourceList:self badgeTextColorForItem:rowItem];
486
487             if(badgeColor==nil)
488                 badgeColor = [NSColor whiteColor];
489         }
490     }
491     else { //Gray colour
492         backgroundColor = BADGE_HIDDEN_BACKGROUND_COLOR;
493         }
494     attributes = [[NSDictionary alloc] initWithObjectsAndKeys:BADGE_FONT, NSFontAttributeName, badgeColor, NSForegroundColorAttributeName, nil];
495     }
496
497     [backgroundColor set];
498     [badgePath fill];
499
500     //Draw the badge text
501     NSAttributedString *badgeAttrString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d", [self badgeValueForItem:rowItem]] attributes:attributes];
502
503     NSSize stringSize = [badgeAttrString size];
504     NSPoint badgeTextPoint = NSMakePoint(NSMidX(badgeFrame)-(stringSize.width/2.0), //Center in the badge frame
505                                          NSMidY(badgeFrame)-(stringSize.height/2.0)); //Center in the badge frame
506     [badgeAttrString drawAtPoint:badgeTextPoint];
507     [attributes release];
508     [badgeAttrString release];
509 }
510
511 #pragma mark -
512 #pragma mark Keyboard Handling
513
514 - (void)keyDown:(NSEvent *)theEvent
515 {
516     NSIndexSet *selectedIndexes = [self selectedRowIndexes];
517     NSString *keyCharacters = [theEvent characters];
518
519     //Make sure we have a selection
520     if([selectedIndexes count]>0)
521     {
522         if([keyCharacters length]>0)
523         {
524             unichar firstKey = [keyCharacters characterAtIndex:0];
525             if(firstKey==NSUpArrowFunctionKey||firstKey==NSDownArrowFunctionKey)
526             {
527                 //Handle keyboard navigation across groups
528                 if([selectedIndexes count]==1&&!([theEvent modifierFlags] & NSShiftKeyMask))
529                 {
530                     int delta = firstKey==NSDownArrowFunctionKey?1:-1;
531                     //Search "backwards" if up arrow, "forwards" if down
532
533                     NSInteger newRow = [selectedIndexes firstIndex];
534                     //Keep incrementing/decrementing the row until a non-header row is reached
535                     do {
536                         newRow+=delta;
537
538                         //If out of bounds of the number of rows..
539                         if(newRow<0||newRow==[self numberOfRows])
540                             break;
541                     } while([self isGroupItem:[self itemAtRow:newRow]]);
542
543                     [self selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
544
545                     return;
546                 }
547             }
548             else if(firstKey==NSDeleteCharacter||firstKey==NSBackspaceCharacter)
549             {
550                 //Post the notification
551                 [[NSNotificationCenter defaultCenter] postNotificationName:PXSLDeleteKeyPressedOnRowsNotification object:self  userInfo:[NSDictionary dictionaryWithObject:selectedIndexes forKey:@"rows"]];
552
553                 return;
554
555             }
556
557         }
558
559     }
560
561     //We don't care about it
562     [super keyDown:theEvent];
563 }
564
565 #pragma mark -
566 #pragma mark Menu Handling
567
568
569 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
570 {
571     NSMenu * m = nil;
572
573     if([_secondaryDelegate respondsToSelector:@selector(sourceList:menuForEvent:item:)]) {
574         NSPoint clickPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
575         NSInteger row = [self rowAtPoint:clickPoint];
576         id clickedItem = [self itemAtRow:row];
577
578         if ([clickedItem sdtype] > 0)
579             m = [_secondaryDelegate sourceList:self menuForEvent:theEvent item:clickedItem];
580         else
581             m = [super menuForEvent:theEvent];
582     }
583
584     if (m == nil) {
585         m = [super menuForEvent:theEvent];
586     }
587
588     return m;
589 }
590
591 #pragma mark -
592 #pragma mark NSOutlineView Data Source methods
593
594 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
595 {
596     if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
597         return [_secondaryDataSource sourceList:self numberOfChildrenOfItem:item];
598     }
599
600     return 0;
601 }
602
603
604 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
605 {
606     if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
607         return [_secondaryDataSource sourceList:self child:index ofItem:item];
608     }
609
610     return nil;
611 }
612
613
614 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
615 {
616     if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
617         return [_secondaryDataSource sourceList:self isItemExpandable:item];
618     }
619
620     return NO;
621 }
622
623
624 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
625 {
626     if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
627         return [_secondaryDataSource sourceList:self objectValueForItem:item];
628     }
629
630     return nil;
631 }
632
633
634 - (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
635 {
636     if([_secondaryDataSource conformsToProtocol:@protocol(PXSourceListDataSource)]) {
637         [_secondaryDataSource sourceList:self setObjectValue:object forItem:item];
638     }
639 }
640
641
642 - (id)outlineView:(NSOutlineView *)outlineView itemForPersistentObject:(id)object
643 {
644     if([_secondaryDataSource respondsToSelector:@selector(sourceList:itemForPersistentObject:)]) {
645         return [_secondaryDataSource sourceList:self itemForPersistentObject:object];
646     }
647
648     return nil;
649 }
650
651 - (id)outlineView:(NSOutlineView *)outlineView persistentObjectForItem:(id)item
652 {
653     if([_secondaryDataSource respondsToSelector:@selector(sourceList:persistentObjectForItem:)]) {
654         return [_secondaryDataSource sourceList:self persistentObjectForItem:item];
655     }
656
657     return nil;
658 }
659
660 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pasteboard
661 {
662     if([_secondaryDataSource respondsToSelector:@selector(sourceList:writeItems:toPasteboard:)]) {
663         return [_secondaryDataSource sourceList:self writeItems:items toPasteboard:pasteboard];
664     }
665
666     return NO;
667 }
668
669 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
670 {
671     if([_secondaryDataSource respondsToSelector:@selector(sourceList:validateDrop:proposedItem:proposedChildIndex:)]) {
672         return [_secondaryDataSource sourceList:self validateDrop:info proposedItem:item proposedChildIndex:index];
673     }
674
675     return NSDragOperationNone;
676 }
677
678 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
679 {
680     if([_secondaryDataSource respondsToSelector:@selector(sourceList:acceptDrop:item:childIndex:)]) {
681         return [_secondaryDataSource sourceList:self acceptDrop:info item:item childIndex:index];
682     }
683
684     return NO;
685 }
686 - (NSArray *)outlineView:(NSOutlineView *)outlineView namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination forDraggedItems:(NSArray *)items
687 {
688     if([_secondaryDataSource respondsToSelector:@selector(sourceList:namesOfPromisedFilesDroppedAtDestination:forDraggedItems:)]) {
689         return [_secondaryDataSource sourceList:self namesOfPromisedFilesDroppedAtDestination:dropDestination forDraggedItems:items];
690     }
691
692     return nil;
693 }
694
695
696 #pragma mark -
697 #pragma mark NSOutlineView Delegate methods
698
699 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldExpandItem:(id)item
700 {
701     if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldExpandItem:)]) {
702         return [_secondaryDelegate sourceList:self shouldExpandItem:item];
703     }
704
705     return YES;
706 }
707
708 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldCollapseItem:(id)item
709 {
710     //Make sure the item isn't displayed as always expanded
711     if([self isGroupItem:item])
712     {
713         if([self isGroupAlwaysExpanded:item]) {
714             return NO;
715         }
716     }
717
718     if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldCollapseItem:)]) {
719         return [_secondaryDelegate sourceList:self shouldCollapseItem:item];
720     }
721
722     return YES;
723 }
724
725 - (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item
726 {
727     if([_secondaryDelegate respondsToSelector:@selector(sourceList:dataCellForItem:)]) {
728         return [_secondaryDelegate sourceList:self dataCellForItem:item];
729     }
730
731     NSInteger row = [self rowForItem:item];
732
733     //Return the default table column
734     return [[[self tableColumns] objectAtIndex:0] dataCellForRow:row];
735 }
736
737 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
738 {
739     if([_secondaryDelegate respondsToSelector:@selector(sourceList:willDisplayCell:forItem:)]) {
740         [_secondaryDelegate sourceList:self willDisplayCell:cell forItem:item];
741     }
742 }
743
744 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item
745 {
746     //Make sure that the item isn't a group as they can't be selected
747     if(![self isGroupItem:item]) {
748         if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldSelectItem:)]) {
749             return [_secondaryDelegate sourceList:self shouldSelectItem:item];
750         }
751     }
752     else {
753         return NO;
754     }
755
756     return YES;
757 }
758
759
760 - (NSIndexSet *)outlineView:(NSOutlineView *)outlineView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes
761 {
762     //The outline view will try to select the first row if -[allowsEmptySelection:] is set to NO – if this is a group row
763     //stop it from doing so and leave it to our implementation of-[reloadData] which will select the first non-group row
764     //for us.
765     if([self numberOfSelectedRows]==0) {
766         if([self isGroupItem:[self itemAtRow:[proposedSelectionIndexes firstIndex]]]) {
767             return [NSIndexSet indexSet];
768         }
769     }
770
771     if([_secondaryDelegate respondsToSelector:@selector(sourceList:selectionIndexesForProposedSelection:)]) {
772         return [_secondaryDelegate sourceList:self selectionIndexesForProposedSelection:proposedSelectionIndexes];
773     }
774
775     //Since we implement this method, something must be returned to the outline view
776         return proposedSelectionIndexes;
777 }
778
779 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
780 {
781     //Group titles can't be edited
782     if([self isGroupItem:item])
783         return NO;
784
785     if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldEditItem:)]) {
786         return [_secondaryDelegate sourceList:self shouldEditItem:item];
787     }
788
789     return YES;
790 }
791
792
793 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
794 {
795     if([_secondaryDelegate respondsToSelector:@selector(sourceList:shouldTrackCell:forItem:)]) {
796         return [_secondaryDelegate sourceList:self shouldTrackCell:cell forItem:item];
797     }
798
799     return NO;
800 }
801
802 - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
803 {
804     if([_secondaryDelegate respondsToSelector:@selector(sourceList:heightOfRowByItem:)]) {
805         return [_secondaryDelegate sourceList:self heightOfRowByItem:item];
806     }
807
808     return [self rowHeight];
809 }
810
811 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item
812 {
813     return [self isGroupItem:item];
814 }
815
816 #pragma mark -
817 #pragma mark Notification handling
818
819 /* Notification wrappers */
820 - (void)outlineViewSelectionIsChanging:(NSNotification *)notification
821 {
822     [[NSNotificationCenter defaultCenter] postNotificationName:PXSLSelectionIsChangingNotification object:self];
823 }
824
825
826 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
827 {
828     [[NSNotificationCenter defaultCenter] postNotificationName:PXSLSelectionDidChangeNotification object:self];
829 }
830
831 - (void)outlineViewItemWillExpand:(NSNotification *)notification
832 {
833     [[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemWillExpandNotification object:self userInfo:[notification userInfo]];
834 }
835
836 - (void)outlineViewItemDidExpand:(NSNotification *)notification
837 {
838     [[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemDidExpandNotification object:self userInfo:[notification userInfo]];
839 }
840
841 - (void)outlineViewItemWillCollapse:(NSNotification *)notification
842 {
843     [[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemWillCollapseNotification object:self userInfo:[notification userInfo]];
844 }
845
846 - (void)outlineViewItemDidCollapse:(NSNotification *)notification
847 {
848     [[NSNotificationCenter defaultCenter] postNotificationName:PXSLItemDidCollapseNotification object:self userInfo:[notification userInfo]];
849 }
850
851 - (void)registerDelegateToReceiveNotification:(NSString*)notification withSelector:(SEL)selector
852 {
853     NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
854
855     //Set the delegate as a receiver of the notification if it implements the notification method
856     if([_secondaryDelegate respondsToSelector:selector]) {
857         [defaultCenter addObserver:_secondaryDelegate selector:selector name:notification object:self];
858     }
859 }
860
861 @end