1/******************************************************************************
2 * Copyright (c) 2008-2012 Transmission authors and contributors
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *****************************************************************************/
22
23#import <Quartz/Quartz.h>
24
25#import "FileOutlineController.h"
26#import "Torrent.h"
27#import "FileListNode.h"
28#import "FileOutlineView.h"
29#import "FilePriorityCell.h"
30#import "FileRenameSheetController.h"
31#import "NSApplicationAdditions.h"
32#import "NSMutableArrayAdditions.h"
33#import "NSStringAdditions.h"
34
35#define ROW_SMALL_HEIGHT 18.0
36
37typedef enum
38{
39    FILE_CHECK_TAG,
40    FILE_UNCHECK_TAG
41} fileCheckMenuTag;
42
43typedef enum
44{
45    FILE_PRIORITY_HIGH_TAG,
46    FILE_PRIORITY_NORMAL_TAG,
47    FILE_PRIORITY_LOW_TAG
48} filePriorityMenuTag;
49
50@interface FileOutlineController (Private)
51
52- (NSMenu *) menu;
53
54- (NSUInteger) findFileNode: (FileListNode *) node inList: (NSArray *) list atIndexes: (NSIndexSet *) range currentParent: (FileListNode *) currentParent finalParent: (FileListNode **) parent;
55
56@end
57
58@implementation FileOutlineController
59
60- (void) awakeFromNib
61{
62    fFileList = [[NSMutableArray alloc] init];
63
64    [fOutline setDoubleAction: @selector(revealFile:)];
65    [fOutline setTarget: self];
66
67    //set table header tool tips
68    [[fOutline tableColumnWithIdentifier: @"Check"] setHeaderToolTip: NSLocalizedString(@"Download", "file table -> header tool tip")];
69    [[fOutline tableColumnWithIdentifier: @"Priority"] setHeaderToolTip: NSLocalizedString(@"Priority", "file table -> header tool tip")];
70
71    [fOutline setMenu: [self menu]];
72
73    [self setTorrent: nil];
74}
75
76
77- (FileOutlineView *) outlineView
78{
79    return fOutline;
80}
81
82- (void) setTorrent: (Torrent *) torrent
83{
84    fTorrent = torrent;
85
86    [fFileList setArray: [fTorrent fileList]];
87
88    fFilterText = nil;
89
90    [fOutline reloadData];
91    [fOutline deselectAll: nil]; //do this after reloading the data #4575
92}
93
94- (void) setFilterText: (NSString *) text
95{
96    NSArray * components = [text betterComponentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
97    if (!components || [components count] == 0)
98    {
99        text = nil;
100        components = nil;
101    }
102
103    if ((!text && !fFilterText) || (text && fFilterText && [text isEqualToString: fFilterText]))
104        return;
105
106    [fOutline beginUpdates];
107
108    NSUInteger currentIndex = 0, totalCount = 0;
109    NSMutableArray * itemsToAdd = [NSMutableArray array];
110    NSMutableIndexSet * itemsToAddIndexes = [NSMutableIndexSet indexSet];
111
112    NSMutableDictionary * removedIndexesForParents = nil; //ugly, but we can't modify the actual file nodes
113
114    NSArray * tempList = !text ? [fTorrent fileList] : [fTorrent flatFileList];
115    for (FileListNode * item in tempList)
116    {
117        __block BOOL filter = NO;
118        if (components)
119        {
120            [components enumerateObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(id obj, NSUInteger idx, BOOL * stop) {
121                if ([[item name] rangeOfString: (NSString *)obj options: (NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch)].location == NSNotFound)
122                {
123                    filter = YES;
124                    *stop = YES;
125                }
126            }];
127        }
128
129        if (!filter)
130        {
131            FileListNode * parent = nil;
132            NSUInteger previousIndex = ![item isFolder] ? [self findFileNode: item inList: fFileList atIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(currentIndex, [fFileList count]-currentIndex)] currentParent: nil finalParent: &parent] : NSNotFound;
133
134            if (previousIndex == NSNotFound)
135            {
136                [itemsToAdd addObject: item];
137                [itemsToAddIndexes addIndex: totalCount];
138            }
139            else
140            {
141                BOOL move = YES;
142                if (!parent)
143                {
144                    if (previousIndex != currentIndex)
145                        [fFileList moveObjectAtIndex: previousIndex toIndex: currentIndex];
146                    else
147                        move = NO;
148                }
149                else
150                {
151                    [fFileList insertObject: item atIndex: currentIndex];
152
153                    //figure out the index within the semi-edited table - UGLY
154                    if (!removedIndexesForParents)
155                        removedIndexesForParents = [NSMutableDictionary dictionary];
156
157                    NSMutableIndexSet * removedIndexes = removedIndexesForParents[parent];
158                    if (!removedIndexes)
159                    {
160                        removedIndexes = [NSMutableIndexSet indexSetWithIndex: previousIndex];
161                        removedIndexesForParents[parent] = removedIndexes;
162                    }
163                    else
164                    {
165                        [removedIndexes addIndex: previousIndex];
166                        previousIndex -= [removedIndexes countOfIndexesInRange: NSMakeRange(0, previousIndex)];
167                    }
168                }
169
170                if (move)
171                    [fOutline moveItemAtIndex: previousIndex inParent: parent toIndex: currentIndex inParent: nil];
172
173                ++currentIndex;
174            }
175
176            ++totalCount;
177        }
178    }
179
180    //remove trailing items - those are the unused
181    if (currentIndex  < [fFileList count])
182    {
183        const NSRange removeRange = NSMakeRange(currentIndex, [fFileList count]-currentIndex);
184        [fFileList removeObjectsInRange: removeRange];
185        [fOutline removeItemsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: removeRange] inParent: nil withAnimation: NSTableViewAnimationSlideDown];
186    }
187
188    //add new items
189    [fFileList insertObjects: itemsToAdd atIndexes: itemsToAddIndexes];
190    [fOutline insertItemsAtIndexes: itemsToAddIndexes inParent: nil withAnimation: NSTableViewAnimationSlideUp];
191
192    [fOutline endUpdates];
193
194    fFilterText = text;
195}
196
197- (void) refresh
198{
199    [fTorrent updateFileStat];
200
201    [fOutline setNeedsDisplay: YES];
202}
203
204- (void) outlineViewSelectionDidChange: (NSNotification *) notification
205{
206    if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible])
207        [[QLPreviewPanel sharedPreviewPanel] reloadData];
208}
209
210- (NSInteger) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item
211{
212    if (!item)
213        return fFileList ? [fFileList count] : 0;
214    else
215    {
216        FileListNode * node = (FileListNode *)item;
217        return [node isFolder] ? [[node children] count] : 0;
218    }
219}
220
221- (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item
222{
223    return [(FileListNode *)item isFolder];
224}
225
226- (id) outlineView: (NSOutlineView *) outlineView child: (NSInteger) index ofItem: (id) item
227{
228    return (item ? [(FileListNode *)item children] : fFileList)[index];
229}
230
231- (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item
232{
233    if ([[tableColumn identifier] isEqualToString: @"Check"])
234        return @([fTorrent checkForFiles: [(FileListNode *)item indexes]]);
235    else
236        return item;
237}
238
239- (void) outlineView: (NSOutlineView *) outlineView willDisplayCell: (id) cell forTableColumn: (NSTableColumn *) tableColumn item: (id) item
240{
241    NSString * identifier = [tableColumn identifier];
242    if ([identifier isEqualToString: @"Check"])
243        [cell setEnabled: [fTorrent canChangeDownloadCheckForFiles: [(FileListNode *)item indexes]]];
244    else if ([identifier isEqualToString: @"Priority"])
245    {
246        [cell setRepresentedObject: item];
247
248        NSInteger hoveredRow = [fOutline hoveredRow];
249        [(FilePriorityCell *)cell setHovered: hoveredRow != -1 && hoveredRow == [fOutline rowForItem: item]];
250    }
251    else;
252}
253
254- (void) outlineView: (NSOutlineView *) outlineView setObjectValue: (id) object forTableColumn: (NSTableColumn *) tableColumn byItem: (id) item
255{
256    NSString * identifier = [tableColumn identifier];
257    if ([identifier isEqualToString: @"Check"])
258    {
259        NSIndexSet * indexSet;
260        if ([NSEvent modifierFlags] & NSAlternateKeyMask)
261            indexSet = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTorrent fileCount])];
262        else
263            indexSet = [(FileListNode *)item indexes];
264
265        [fTorrent setFileCheckState: [object intValue] != NSOffState ? NSOnState : NSOffState forIndexes: indexSet];
266        [fOutline setNeedsDisplay: YES];
267
268        [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateUI" object: nil];
269    }
270}
271
272- (NSString *) outlineView: (NSOutlineView *) outlineView typeSelectStringForTableColumn: (NSTableColumn *) tableColumn item: (id) item
273{
274    return [(FileListNode *)item name];
275}
276
277- (NSString *) outlineView: (NSOutlineView *) outlineView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect
278        tableColumn: (NSTableColumn *) tableColumn item: (id) item mouseLocation: (NSPoint) mouseLocation
279{
280    NSString * ident = [tableColumn identifier];
281    if ([ident isEqualToString: @"Name"])
282    {
283        NSString * path = [fTorrent fileLocation: item];
284        if (!path)
285            path = [[(FileListNode *)item path] stringByAppendingPathComponent: [(FileListNode *)item name]];
286        return path;
287    }
288    else if ([ident isEqualToString: @"Check"])
289    {
290        switch ([cell state])
291        {
292            case NSOffState:
293                return NSLocalizedString(@"Don't Download", "files tab -> tooltip");
294            case NSOnState:
295                return NSLocalizedString(@"Download", "files tab -> tooltip");
296            case NSMixedState:
297                return NSLocalizedString(@"Download Some", "files tab -> tooltip");
298        }
299    }
300    else if ([ident isEqualToString: @"Priority"])
301    {
302        NSSet * priorities = [fTorrent filePrioritiesForIndexes: [(FileListNode *)item indexes]];
303        switch ([priorities count])
304        {
305            case 0:
306                return NSLocalizedString(@"Priority Not Available", "files tab -> tooltip");
307            case 1:
308                switch ([[priorities anyObject] intValue])
309                {
310                    case TR_PRI_LOW:
311                        return NSLocalizedString(@"Low Priority", "files tab -> tooltip");
312                    case TR_PRI_HIGH:
313                        return NSLocalizedString(@"High Priority", "files tab -> tooltip");
314                    case TR_PRI_NORMAL:
315                        return NSLocalizedString(@"Normal Priority", "files tab -> tooltip");
316                }
317                break;
318            default:
319                return NSLocalizedString(@"Multiple Priorities", "files tab -> tooltip");
320        }
321    }
322    else;
323
324    return nil;
325}
326
327- (CGFloat) outlineView: (NSOutlineView *) outlineView heightOfRowByItem: (id) item
328{
329    if ([(FileListNode *)item isFolder])
330        return ROW_SMALL_HEIGHT;
331    else
332        return [outlineView rowHeight];
333}
334
335- (void) setCheck: (id) sender
336{
337    NSInteger state = [sender tag] == FILE_UNCHECK_TAG ? NSOffState : NSOnState;
338
339    NSIndexSet * indexSet = [fOutline selectedRowIndexes];
340    NSMutableIndexSet * itemIndexes = [NSMutableIndexSet indexSet];
341    for (NSInteger i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
342        [itemIndexes addIndexes: [[fOutline itemAtRow: i] indexes]];
343
344    [fTorrent setFileCheckState: state forIndexes: itemIndexes];
345    [fOutline setNeedsDisplay: YES];
346}
347
348- (void) setOnlySelectedCheck: (id) sender
349{
350    NSIndexSet * indexSet = [fOutline selectedRowIndexes];
351    NSMutableIndexSet * itemIndexes = [NSMutableIndexSet indexSet];
352    for (NSInteger i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
353        [itemIndexes addIndexes: [[fOutline itemAtRow: i] indexes]];
354
355    [fTorrent setFileCheckState: NSOnState forIndexes: itemIndexes];
356
357    NSMutableIndexSet * remainingItemIndexes = [NSMutableIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTorrent fileCount])];
358    [remainingItemIndexes removeIndexes: itemIndexes];
359    [fTorrent setFileCheckState: NSOffState forIndexes: remainingItemIndexes];
360
361    [fOutline setNeedsDisplay: YES];
362}
363
364- (void) checkAll
365{
366    NSIndexSet * indexSet = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTorrent fileCount])];
367    [fTorrent setFileCheckState: NSOnState forIndexes: indexSet];
368    [fOutline setNeedsDisplay: YES];
369}
370
371- (void) uncheckAll
372{
373    NSIndexSet * indexSet = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fTorrent fileCount])];
374    [fTorrent setFileCheckState: NSOffState forIndexes: indexSet];
375    [fOutline setNeedsDisplay: YES];
376}
377
378- (void) setPriority: (id) sender
379{
380    tr_priority_t priority;
381    switch ([sender tag])
382    {
383        case FILE_PRIORITY_HIGH_TAG:
384            priority = TR_PRI_HIGH;
385            break;
386        case FILE_PRIORITY_NORMAL_TAG:
387            priority = TR_PRI_NORMAL;
388            break;
389        case FILE_PRIORITY_LOW_TAG:
390            priority = TR_PRI_LOW;
391    }
392
393    NSIndexSet * indexSet = [fOutline selectedRowIndexes];
394    NSMutableIndexSet * itemIndexes = [NSMutableIndexSet indexSet];
395    for (NSInteger i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
396        [itemIndexes addIndexes: [[fOutline itemAtRow: i] indexes]];
397
398    [fTorrent setFilePriority: priority forIndexes: itemIndexes];
399    [fOutline setNeedsDisplay: YES];
400}
401
402- (void) revealFile: (id) sender
403{
404    NSIndexSet * indexes = [fOutline selectedRowIndexes];
405    NSMutableArray * paths = [NSMutableArray arrayWithCapacity: [indexes count]];
406    for (NSUInteger i = [indexes firstIndex]; i != NSNotFound; i = [indexes indexGreaterThanIndex: i])
407    {
408        NSString * path = [fTorrent fileLocation: [fOutline itemAtRow: i]];
409        if (path)
410            [paths addObject: [NSURL fileURLWithPath: path]];
411    }
412
413    if ([paths count] > 0)
414        [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs: paths];
415}
416
417- (void) renameSelected: (id) sender
418{
419    NSIndexSet * indexes = [fOutline selectedRowIndexes];
420    NSAssert([indexes count] == 1, @"1 file needs to be selected to rename, but %ld are selected", [indexes count]);
421
422    FileListNode * node = [fOutline itemAtRow: [indexes firstIndex]];
423    Torrent * torrent = [node torrent];
424    if (![torrent isFolder])
425    {
426        [FileRenameSheetController presentSheetForTorrent: torrent modalForWindow: [fOutline window] completionHandler: ^(BOOL didRename) {
427            if (didRename)
428            {
429                [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateQueue" object: self];
430                [[NSNotificationCenter defaultCenter] postNotificationName: @"ResetInspector" object: self userInfo: @{ @"Torrent" : torrent }];
431            }
432        }];
433    }
434    else
435    {
436        [FileRenameSheetController presentSheetForFileListNode: node modalForWindow: [fOutline window] completionHandler: ^(BOOL didRename) {
437            #warning instead of calling reset inspector, just resort?
438            if (didRename)
439                [[NSNotificationCenter defaultCenter] postNotificationName: @"ResetInspector" object: self userInfo: @{ @"Torrent" : torrent }];
440        }];
441    }
442}
443
444#warning make real view controller (Leopard-only) so that Command-R will work
445- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
446{
447    if (!fTorrent)
448        return NO;
449
450    SEL action = [menuItem action];
451
452    if (action == @selector(revealFile:))
453    {
454        NSIndexSet * indexSet = [fOutline selectedRowIndexes];
455        for (NSInteger i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
456            if ([fTorrent fileLocation: [fOutline itemAtRow: i]] != nil)
457                return YES;
458        return NO;
459    }
460
461    if (action == @selector(setCheck:))
462    {
463        if ([fOutline numberOfSelectedRows] == 0)
464            return NO;
465
466        NSIndexSet * indexSet = [fOutline selectedRowIndexes];
467        NSMutableIndexSet * itemIndexes = [NSMutableIndexSet indexSet];
468        for (NSInteger i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
469            [itemIndexes addIndexes: [[fOutline itemAtRow: i] indexes]];
470
471        NSInteger state = ([menuItem tag] == FILE_CHECK_TAG) ? NSOnState : NSOffState;
472        return [fTorrent checkForFiles: itemIndexes] != state && [fTorrent canChangeDownloadCheckForFiles: itemIndexes];
473    }
474
475    if (action == @selector(setOnlySelectedCheck:))
476    {
477        if ([fOutline numberOfSelectedRows] == 0)
478            return NO;
479
480        NSIndexSet * indexSet = [fOutline selectedRowIndexes];
481        NSMutableIndexSet * itemIndexes = [NSMutableIndexSet indexSet];
482        for (NSInteger i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
483            [itemIndexes addIndexes: [[fOutline itemAtRow: i] indexes]];
484
485        return [fTorrent canChangeDownloadCheckForFiles: itemIndexes];
486    }
487
488    if (action == @selector(setPriority:))
489    {
490        if ([fOutline numberOfSelectedRows] == 0)
491        {
492            [menuItem setState: NSOffState];
493            return NO;
494        }
495
496        //determine which priorities are checked
497        NSIndexSet * indexSet = [fOutline selectedRowIndexes];
498        tr_priority_t priority;
499        switch ([menuItem tag])
500        {
501            case FILE_PRIORITY_HIGH_TAG:
502                priority = TR_PRI_HIGH;
503                break;
504            case FILE_PRIORITY_NORMAL_TAG:
505                priority = TR_PRI_NORMAL;
506                break;
507            case FILE_PRIORITY_LOW_TAG:
508                priority = TR_PRI_LOW;
509                break;
510        }
511
512        BOOL current = NO, canChange = NO;
513        for (NSInteger i = [indexSet firstIndex]; i != NSNotFound; i = [indexSet indexGreaterThanIndex: i])
514        {
515            NSIndexSet * fileIndexSet = [[fOutline itemAtRow: i] indexes];
516            if (![fTorrent canChangeDownloadCheckForFiles: fileIndexSet])
517                continue;
518
519            canChange = YES;
520            if ([fTorrent hasFilePriority: priority forIndexes: fileIndexSet])
521            {
522                current = YES;
523                break;
524            }
525        }
526
527        [menuItem setState: current ? NSOnState : NSOffState];
528        return canChange;
529    }
530
531    if (action == @selector(renameSelected:))
532    {
533        return [fOutline numberOfSelectedRows] == 1;
534    }
535
536    return YES;
537}
538
539@end
540
541@implementation FileOutlineController (Private)
542
543- (NSMenu *) menu
544{
545    NSMenu * menu = [[NSMenu alloc] initWithTitle: @"File Outline Menu"];
546
547    //check and uncheck
548    NSMenuItem * item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"Check Selected", "File Outline -> Menu")
549            action: @selector(setCheck:) keyEquivalent: @""];
550    [item setTarget: self];
551    [item setTag: FILE_CHECK_TAG];
552    [menu addItem: item];
553
554    item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"Uncheck Selected", "File Outline -> Menu")
555            action: @selector(setCheck:) keyEquivalent: @""];
556    [item setTarget: self];
557    [item setTag: FILE_UNCHECK_TAG];
558    [menu addItem: item];
559
560    //only check selected
561    item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"Only Check Selected", "File Outline -> Menu")
562            action: @selector(setOnlySelectedCheck:) keyEquivalent: @""];
563    [item setTarget: self];
564    [menu addItem: item];
565
566    [menu addItem: [NSMenuItem separatorItem]];
567
568    //priority
569    item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"Priority", "File Outline -> Menu") action: NULL keyEquivalent: @""];
570    NSMenu * priorityMenu = [[NSMenu alloc] initWithTitle: @"File Priority Menu"];
571    [item setSubmenu: priorityMenu];
572    [menu addItem: item];
573
574    item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"High", "File Outline -> Priority Menu")
575            action: @selector(setPriority:) keyEquivalent: @""];
576    [item setTarget: self];
577    [item setTag: FILE_PRIORITY_HIGH_TAG];
578    [item setImage: [NSImage imageNamed: @"PriorityHighTemplate"]];
579    [priorityMenu addItem: item];
580
581    item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"Normal", "File Outline -> Priority Menu")
582            action: @selector(setPriority:) keyEquivalent: @""];
583    [item setTarget: self];
584    [item setTag: FILE_PRIORITY_NORMAL_TAG];
585    [item setImage: [NSImage imageNamed: @"PriorityNormalTemplate"]];
586    [priorityMenu addItem: item];
587
588    item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"Low", "File Outline -> Priority Menu")
589            action: @selector(setPriority:) keyEquivalent: @""];
590    [item setTarget: self];
591    [item setTag: FILE_PRIORITY_LOW_TAG];
592    [item setImage: [NSImage imageNamed: @"PriorityLowTemplate"]];
593    [priorityMenu addItem: item];
594
595
596    [menu addItem: [NSMenuItem separatorItem]];
597
598    //reveal in finder
599    item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"Show in Finder", "File Outline -> Menu")
600                                      action: @selector(revealFile:) keyEquivalent: @""];
601    [item setTarget: self];
602    [menu addItem: item];
603
604    [menu addItem: [NSMenuItem separatorItem]];
605
606    //rename
607    item = [[NSMenuItem alloc] initWithTitle: [NSLocalizedString(@"Rename File", "File Outline -> Menu") stringByAppendingEllipsis]
608                                                   action: @selector(renameSelected:) keyEquivalent: @""];
609    [item setTarget: self];
610    [menu addItem: item];
611
612    return menu;
613}
614
615- (NSUInteger) findFileNode: (FileListNode *) node inList: (NSArray *) list atIndexes: (NSIndexSet *) indexes currentParent: (FileListNode *) currentParent finalParent: (FileListNode **) parent
616{
617    NSAssert(![node isFolder], @"Looking up folder node!");
618
619    __block NSUInteger retIndex = NSNotFound;
620
621    [list enumerateObjectsAtIndexes: indexes options: NSEnumerationConcurrent usingBlock: ^(FileListNode * checkNode, NSUInteger index, BOOL * stop) {
622        if ([[checkNode indexes] containsIndex: [[node indexes] firstIndex]])
623        {
624            if (![checkNode isFolder])
625            {
626                NSAssert2([checkNode isEqualTo: node], @"Expected file nodes to be equal: %@ %@", checkNode, node);
627
628                *parent = currentParent;
629                retIndex = index;
630            }
631            else
632            {
633                const NSUInteger subIndex = [self findFileNode: node inList: [checkNode children] atIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [[checkNode children] count])] currentParent: checkNode finalParent: parent];
634                NSAssert(subIndex != NSNotFound, @"We didn't find an expected file node.");
635                retIndex = subIndex;
636            }
637
638            *stop = YES;
639        }
640    }];
641
642    return retIndex;
643}
644
645@end
646