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