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