]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: add file size column to playlist table (close #9497)
[vlc] / modules / gui / macosx / playlist.m
1 /*****************************************************************************
2  * playlist.m: MacOS X interface module
3  *****************************************************************************
4 * Copyright (C) 2002-2013 VLC authors and VideoLAN
5  * $Id$
6  *
7  * Authors: Jon Lech Johansen <jon-vl@nanocrew.net>
8  *          Derk-Jan Hartman <hartman at videola/n dot org>
9  *          Benjamin Pracht <bigben at videolab dot org>
10  *          Felix Paul Kühne <fkuehne at videolan dot org>
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
25  *****************************************************************************/
26
27 /* TODO
28  * add 'icons' for different types of nodes? (http://www.cocoadev.com/index.pl?IconAndTextInTableCell)
29  * reimplement enable/disable item
30  */
31
32
33 /*****************************************************************************
34  * Preamble
35  *****************************************************************************/
36 #include <stdlib.h>                                      /* malloc(), free() */
37 #include <sys/param.h>                                    /* for MAXPATHLEN */
38 #include <string.h>
39 #include <math.h>
40 #include <sys/mount.h>
41
42 #import "intf.h"
43 #import "wizard.h"
44 #import "bookmarks.h"
45 #import "playlistinfo.h"
46 #import "playlist.h"
47 #import "controls.h"
48 #import "misc.h"
49 #import "open.h"
50 #import "MainMenu.h"
51
52 #include <vlc_keys.h>
53 #import <vlc_interface.h>
54 #include <vlc_url.h>
55
56 /*****************************************************************************
57  * VLCPlaylistView implementation
58  *****************************************************************************/
59 @implementation VLCPlaylistView
60
61 - (NSMenu *)menuForEvent:(NSEvent *)o_event
62 {
63     return([(VLCPlaylist *)[self delegate] menuForEvent: o_event]);
64 }
65
66 - (void)keyDown:(NSEvent *)o_event
67 {
68     unichar key = 0;
69
70     if ([[o_event characters] length])
71         key = [[o_event characters] characterAtIndex: 0];
72
73     switch(key) {
74         case NSDeleteCharacter:
75         case NSDeleteFunctionKey:
76         case NSDeleteCharFunctionKey:
77         case NSBackspaceCharacter:
78             [(VLCPlaylist *)[self delegate] deleteItem:self];
79             break;
80
81         case NSEnterCharacter:
82         case NSCarriageReturnCharacter:
83             [(VLCPlaylist *)[[VLCMain sharedInstance] playlist] playItem:nil];
84             break;
85
86         default:
87             [super keyDown: o_event];
88             break;
89     }
90 }
91
92 - (BOOL)validateMenuItem:(NSMenuItem *)item
93 {
94     if (([self numberOfSelectedRows] >= 1 && [item action] == @selector(delete:)) || [item action] == @selector(selectAll:))
95         return YES;
96
97     return NO;
98 }
99
100 - (BOOL)acceptsFirstResponder
101 {
102     return YES;
103 }
104
105 - (BOOL)becomeFirstResponder
106 {
107     [self setNeedsDisplay:YES];
108     return YES;
109 }
110
111 - (BOOL)resignFirstResponder
112 {
113     [self setNeedsDisplay:YES];
114     return YES;
115 }
116
117 - (IBAction)delete:(id)sender
118 {
119     [[[VLCMain sharedInstance] playlist] deleteItem: sender];
120 }
121
122 @end
123
124 /*****************************************************************************
125  * VLCPlaylistCommon implementation
126  *
127  * This class the superclass of the VLCPlaylist and VLCPlaylistWizard.
128  * It contains the common methods and elements of these 2 entities.
129  *****************************************************************************/
130 @interface VLCPlaylistCommon ()
131 {
132     playlist_item_t * p_current_root_item;
133 }
134 @end
135
136 @implementation VLCPlaylistCommon
137
138 - (id)init
139 {
140     playlist_t * p_playlist = pl_Get(VLCIntf);
141     p_current_root_item = p_playlist->p_local_category;
142
143     self = [super init];
144     if (self != nil)
145         o_outline_dict = [[NSMutableDictionary alloc] init];
146
147     return self;
148 }
149
150 - (void)awakeFromNib
151 {
152     playlist_t * p_playlist = pl_Get(VLCIntf);
153     [o_outline_view setTarget: self];
154     [o_outline_view setDelegate: self];
155     [o_outline_view setDataSource: self];
156     [o_outline_view setAllowsEmptySelection: NO];
157     [o_outline_view expandItem: [o_outline_view itemAtRow:0]];
158
159     [o_outline_view_other setTarget: self];
160     [o_outline_view_other setDelegate: self];
161     [o_outline_view_other setDataSource: self];
162     [o_outline_view_other setAllowsEmptySelection: NO];
163
164     [[o_tc_name_other headerCell] setStringValue:_NS("Name")];
165     [[o_tc_author_other headerCell] setStringValue:_NS("Author")];
166     [[o_tc_duration_other headerCell] setStringValue:_NS("Duration")];
167 }
168
169 - (void)setPlaylistRoot: (playlist_item_t *)root_item
170 {
171     p_current_root_item = root_item;
172     [o_outline_view reloadData];
173     [o_outline_view_other reloadData];
174 }
175
176 - (playlist_item_t *)currentPlaylistRoot
177 {
178     return p_current_root_item;
179 }
180
181 - (NSOutlineView *)outlineView
182 {
183     return o_outline_view;
184 }
185
186 - (playlist_item_t *)selectedPlaylistItem
187 {
188     return [[o_outline_view itemAtRow: [o_outline_view selectedRow]]
189                                                                 pointerValue];
190 }
191
192 @end
193
194 @implementation VLCPlaylistCommon (NSOutlineViewDataSource)
195 /* return the number of children for Obj-C pointer item */ /* DONE */
196 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
197 {
198     int i_return = 0;
199     playlist_item_t *p_item = NULL;
200     playlist_t * p_playlist = pl_Get(VLCIntf);
201     //assert(outlineView == o_outline_view);
202
203     PL_LOCK;
204     if (!item)
205         p_item = p_current_root_item;
206     else
207         p_item = (playlist_item_t *)[item pointerValue];
208
209     if (p_item)
210         i_return = p_item->i_children;
211     PL_UNLOCK;
212
213     return i_return > 0 ? i_return : 0;
214 }
215
216 /* return the child at index for the Obj-C pointer item */ /* DONE */
217 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
218 {
219     playlist_item_t *p_return = NULL, *p_item = NULL;
220     NSValue *o_value;
221     playlist_t * p_playlist = pl_Get(VLCIntf);
222
223     PL_LOCK;
224     if (item == nil)
225         p_item = p_current_root_item; /* root object */
226     else
227         p_item = (playlist_item_t *)[item pointerValue];
228
229     if (p_item && index < p_item->i_children && index >= 0)
230         p_return = p_item->pp_children[index];
231     PL_UNLOCK;
232
233     o_value = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_return]];
234
235     if (o_value == nil) {
236         /* FIXME: Why is there a warning if that happens all the time and seems
237          * to be normal? Add an assert and fix it.
238          * msg_Warn(VLCIntf, "playlist item misses pointer value, adding one"); */
239         o_value = [[NSValue valueWithPointer: p_return] retain];
240     }
241     return o_value;
242 }
243
244 /* is the item expandable */
245 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
246 {
247     int i_return = 0;
248     playlist_t *p_playlist = pl_Get(VLCIntf);
249
250     PL_LOCK;
251     if (item == nil) {
252         /* root object */
253         if (p_current_root_item) {
254             i_return = p_current_root_item->i_children;
255         }
256     } else {
257         playlist_item_t *p_item = (playlist_item_t *)[item pointerValue];
258         if (p_item)
259             i_return = p_item->i_children;
260     }
261     PL_UNLOCK;
262
263     return (i_return >= 0);
264 }
265
266 /* retrieve the string values for the cells */
267 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)o_tc byItem:(id)item
268 {
269     id o_value = nil;
270     char * psz_value;
271     playlist_item_t *p_item;
272
273     /* For error handling */
274     static BOOL attempted_reload = NO;
275
276     if (item == nil || ![item isKindOfClass: [NSValue class]]) {
277         /* Attempt to fix the error by asking for a data redisplay
278          * This might cause infinite loop, so add a small check */
279         if (!attempted_reload) {
280             attempted_reload = YES;
281             [outlineView reloadData];
282         }
283         return @"error" ;
284     }
285
286     p_item = (playlist_item_t *)[item pointerValue];
287     if (!p_item || !p_item->p_input) {
288         /* Attempt to fix the error by asking for a data redisplay
289          * This might cause infinite loop, so add a small check */
290         if (!attempted_reload) {
291             attempted_reload = YES;
292             [outlineView reloadData];
293         }
294         return @"error";
295     }
296
297     attempted_reload = NO;
298     NSString * o_identifier = [o_tc identifier];
299
300     if ([o_identifier isEqualToString:TRACKNUM_COLUMN]) {
301         psz_value = input_item_GetTrackNumber(p_item->p_input);
302         if (psz_value) {
303             o_value = [NSString stringWithUTF8String:psz_value];
304             free(psz_value);
305         }
306     } else if ([o_identifier isEqualToString:TITLE_COLUMN]) {
307         /* sanity check to prevent the NSString class from crashing */
308         char *psz_title =  input_item_GetTitleFbName(p_item->p_input);
309         if (psz_title) {
310             o_value = [NSString stringWithUTF8String:psz_title];
311             free(psz_title);
312         }
313     } else if ([o_identifier isEqualToString:ARTIST_COLUMN]) {
314         psz_value = input_item_GetArtist(p_item->p_input);
315         if (psz_value) {
316             o_value = [NSString stringWithUTF8String:psz_value];
317             free(psz_value);
318         }
319     } else if ([o_identifier isEqualToString:@"duration"]) {
320         char psz_duration[MSTRTIME_MAX_SIZE];
321         mtime_t dur = input_item_GetDuration(p_item->p_input);
322         if (dur != -1) {
323             secstotimestr(psz_duration, dur/1000000);
324             o_value = [NSString stringWithUTF8String:psz_duration];
325         }
326         else
327             o_value = @"--:--";
328     } else if ([o_identifier isEqualToString:GENRE_COLUMN]) {
329         psz_value = input_item_GetGenre(p_item->p_input);
330         if (psz_value) {
331             o_value = [NSString stringWithUTF8String:psz_value];
332             free(psz_value);
333         }
334     } else if ([o_identifier isEqualToString:ALBUM_COLUMN]) {
335         psz_value = input_item_GetAlbum(p_item->p_input);
336         if (psz_value) {
337             o_value = [NSString stringWithUTF8String:psz_value];
338             free(psz_value);
339         }
340     } else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN]) {
341         psz_value = input_item_GetDescription(p_item->p_input);
342         if (psz_value) {
343             o_value = [NSString stringWithUTF8String:psz_value];
344             free(psz_value);
345         }
346     } else if ([o_identifier isEqualToString:DATE_COLUMN]) {
347         psz_value = input_item_GetDate(p_item->p_input);
348         if (psz_value) {
349             o_value = [NSString stringWithUTF8String:psz_value];
350             free(psz_value);
351         }
352     } else if ([o_identifier isEqualToString:LANGUAGE_COLUMN]) {
353         psz_value = input_item_GetLanguage(p_item->p_input);
354         if (psz_value) {
355             o_value = [NSString stringWithUTF8String:psz_value];
356             free(psz_value);
357         }
358     }
359     else if ([o_identifier isEqualToString:URI_COLUMN]) {
360         psz_value = decode_URI(input_item_GetURI(p_item->p_input));
361         if (psz_value) {
362             o_value = [NSString stringWithUTF8String:psz_value];
363             free(psz_value);
364         }
365     }
366     else if ([o_identifier isEqualToString:FILESIZE_COLUMN]) {
367         psz_value = input_item_GetURI(p_item->p_input);
368         o_value = @"";
369         if (psz_value) {
370             NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:psz_value]];
371             if ([url isFileURL]) {
372                 NSFileManager *fileManager = [NSFileManager defaultManager];
373                 if ([fileManager fileExistsAtPath:[url path]]) {
374                     NSError *error;
375                     NSDictionary *attributes = [fileManager attributesOfItemAtPath:[url path] error:&error];
376                     o_value = [NSByteCountFormatter stringFromByteCount:[attributes fileSize] countStyle:NSByteCountFormatterCountStyleDecimal];
377                 }
378             }
379             free(psz_value);
380         }
381     }
382     else if ([o_identifier isEqualToString:@"status"]) {
383         if (input_item_HasErrorWhenReading(p_item->p_input)) {
384             o_value = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kAlertCautionIcon)];
385             [o_value setSize: NSMakeSize(16,16)];
386         }
387     }
388
389     return o_value;
390 }
391
392 @end
393
394 /*****************************************************************************
395  * VLCPlaylistWizard implementation
396  *****************************************************************************/
397 @implementation VLCPlaylistWizard
398
399 - (IBAction)reloadOutlineView
400 {
401     /* Only reload the outlineview if the wizard window is open since this can
402        be quite long on big playlists */
403     if ([[o_outline_view window] isVisible])
404         [o_outline_view reloadData];
405 }
406
407 @end
408
409 /*****************************************************************************
410  * An extension to NSOutlineView's interface to fix compilation warnings
411  * and let us access these 2 functions properly.
412  * This uses a private API, but works fine on all current OSX releases.
413  * Radar ID 11739459 request a public API for this. However, it is probably
414  * easier and faster to recreate similar looking bitmaps ourselves.
415  *****************************************************************************/
416
417 @interface NSOutlineView (UndocumentedSortImages)
418 + (NSImage *)_defaultTableHeaderSortImage;
419 + (NSImage *)_defaultTableHeaderReverseSortImage;
420 @end
421
422
423 /*****************************************************************************
424  * VLCPlaylist implementation
425  *****************************************************************************/
426 @interface VLCPlaylist ()
427 {
428     NSImage *o_descendingSortingImage;
429     NSImage *o_ascendingSortingImage;
430
431     NSMutableArray *o_nodes_array;
432     NSMutableArray *o_items_array;
433
434     BOOL b_selected_item_met;
435     BOOL b_isSortDescending;
436     id o_tc_sortColumn;
437     NSInteger retainedRowSelection;
438
439     BOOL b_playlistmenu_nib_loaded;
440     BOOL b_view_setup;
441 }
442
443 - (void)saveTableColumns;
444 @end
445
446 @implementation VLCPlaylist
447
448 + (void)initialize{
449     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
450     NSMutableArray * o_columnArray = [[NSMutableArray alloc] init];
451     [o_columnArray addObject: [NSArray arrayWithObjects:TITLE_COLUMN, [NSNumber numberWithFloat:190.], nil]];
452     [o_columnArray addObject: [NSArray arrayWithObjects:ARTIST_COLUMN, [NSNumber numberWithFloat:95.], nil]];
453     [o_columnArray addObject: [NSArray arrayWithObjects:DURATION_COLUMN, [NSNumber numberWithFloat:95.], nil]];
454     NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:[NSArray arrayWithArray:o_columnArray] forKey: @"PlaylistColumnSelection"];
455
456     [defaults registerDefaults:appDefaults];
457     [o_columnArray release];
458 }
459
460 - (id)init
461 {
462     self = [super init];
463     if (self != nil) {
464         o_nodes_array = [[NSMutableArray alloc] init];
465         o_items_array = [[NSMutableArray alloc] init];
466     }
467     return self;
468 }
469
470 - (void)dealloc
471 {
472     [o_nodes_array release];
473     [o_items_array release];
474     [super dealloc];
475 }
476
477 - (void)awakeFromNib
478 {
479     if (b_view_setup)
480         return;
481
482     playlist_t * p_playlist = pl_Get(VLCIntf);
483
484     [super awakeFromNib];
485     [self initStrings];
486
487     [o_outline_view setDoubleAction: @selector(playItem:)];
488     [o_outline_view_other setDoubleAction: @selector(playItem:)];
489
490     [o_outline_view registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
491     [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
492
493     [o_outline_view_other registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
494     [o_outline_view_other setIntercellSpacing: NSMakeSize (0.0, 1.0)];
495
496     /* This uses a private API, but works fine on all current OSX releases.
497      * Radar ID 11739459 request a public API for this. However, it is probably
498      * easier and faster to recreate similar looking bitmaps ourselves. */
499     o_ascendingSortingImage = [[NSOutlineView class] _defaultTableHeaderSortImage];
500     o_descendingSortingImage = [[NSOutlineView class] _defaultTableHeaderReverseSortImage];
501
502     o_tc_sortColumn = nil;
503
504     NSArray * o_columnArray = [[NSUserDefaults standardUserDefaults] arrayForKey:@"PlaylistColumnSelection"];
505     NSUInteger count = [o_columnArray count];
506
507     id o_menu = [[VLCMain sharedInstance] mainMenu];
508     NSString * o_column;
509
510     NSMenu *o_context_menu = [o_menu setupPlaylistTableColumnsMenu];
511     [o_playlist_header setMenu: o_context_menu];
512
513     for (NSUInteger i = 0; i < count; i++) {
514         o_column = [[o_columnArray objectAtIndex:i] objectAtIndex:0];
515         if ([o_column isEqualToString:@"status"])
516             continue;
517
518         [o_menu setPlaylistColumnTableState: NSOnState forColumn: o_column];
519         [[o_outline_view tableColumnWithIdentifier: o_column] setWidth: [[[o_columnArray objectAtIndex:i] objectAtIndex:1] floatValue]];
520     }
521
522     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];
523
524     b_view_setup = YES;
525 }
526
527 - (void)applicationWillTerminate:(NSNotification *)notification
528 {
529     /* let's make sure we save the correct widths and positions, since this likely changed since the last time the user played with the column selection */
530     [self saveTableColumns];
531 }
532
533 - (void)searchfieldChanged:(NSNotification *)o_notification
534 {
535     [o_search_field setStringValue:[[o_notification object] stringValue]];
536 }
537
538 - (void)initStrings
539 {
540     [o_mi_play setTitle: _NS("Play")];
541     [o_mi_delete setTitle: _NS("Delete")];
542     [o_mi_recursive_expand setTitle: _NS("Expand Node")];
543     [o_mi_selectall setTitle: _NS("Select All")];
544     [o_mi_info setTitle: _NS("Media Information...")];
545     [o_mi_dl_cover_art setTitle: _NS("Download Cover Art")];
546     [o_mi_preparse setTitle: _NS("Fetch Meta Data")];
547     [o_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
548     [o_mm_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
549     [[o_mm_mi_revealInFinder menu] setAutoenablesItems: NO];
550     [o_mi_sort_name setTitle: _NS("Sort Node by Name")];
551     [o_mi_sort_author setTitle: _NS("Sort Node by Author")];
552
553     [o_search_field setToolTip: _NS("Search in Playlist")];
554     [o_search_field_other setToolTip: _NS("Search in Playlist")];
555 }
556
557 - (void)playlistUpdated
558 {
559     /* Clear indications of any existing column sorting */
560     NSUInteger count = [[o_outline_view tableColumns] count];
561     for (NSUInteger i = 0 ; i < count ; i++)
562         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
563
564     [o_outline_view setHighlightedTableColumn:nil];
565     o_tc_sortColumn = nil;
566     // TODO Find a way to keep the dict size to a minimum
567     //[o_outline_dict removeAllObjects];
568     [o_outline_view reloadData];
569     [[[[VLCMain sharedInstance] wizard] playlistWizard] reloadOutlineView];
570     [[[[VLCMain sharedInstance] bookmarks] dataTable] reloadData];
571
572     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:retainedRowSelection] byExtendingSelection:NO];
573
574     [self outlineViewSelectionDidChange: nil];
575     [[VLCMain sharedInstance] updateMainWindow];
576 }
577
578 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
579 {
580     // FIXME: unsafe
581     playlist_item_t * p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
582
583     if (p_item) {
584         /* update the state of our Reveal-in-Finder menu items */
585         NSMutableString *o_mrl;
586         char *psz_uri = input_item_GetURI(p_item->p_input);
587
588         [o_mi_revealInFinder setEnabled: NO];
589         [o_mm_mi_revealInFinder setEnabled: NO];
590         if (psz_uri) {
591             o_mrl = [NSMutableString stringWithUTF8String: psz_uri];
592
593             /* perform some checks whether it is a file and if it is local at all... */
594             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
595             if (prefix_range.location != NSNotFound)
596                 [o_mrl deleteCharactersInRange: prefix_range];
597
598             if ([o_mrl characterAtIndex:0] == '/') {
599                 [o_mi_revealInFinder setEnabled: YES];
600                 [o_mm_mi_revealInFinder setEnabled: YES];
601             }
602             free(psz_uri);
603         }
604
605         /* update our info-panel to reflect the new item */
606         [[[VLCMain sharedInstance] info] updatePanelWithItem:p_item->p_input];
607     }
608 }
609
610 - (BOOL)isSelectionEmpty
611 {
612     return [o_outline_view selectedRow] == -1;
613 }
614
615 - (void)updateRowSelection
616 {
617     // FIXME: unsafe
618     playlist_t *p_playlist = pl_Get(VLCIntf);
619     playlist_item_t *p_item, *p_temp_item;
620     NSMutableArray *o_array = [NSMutableArray array];
621
622     PL_LOCK;
623     p_item = playlist_CurrentPlayingItem(p_playlist);
624     if (p_item == NULL) {
625         PL_UNLOCK;
626         return;
627     }
628
629     p_temp_item = p_item;
630     while(p_temp_item->p_parent) {
631         [o_array insertObject: [NSValue valueWithPointer: p_temp_item] atIndex: 0];
632         p_temp_item = p_temp_item->p_parent;
633     }
634     PL_UNLOCK;
635
636     NSUInteger count = [o_array count];
637     for (NSUInteger j = 0; j < count - 1; j++) {
638         id o_item;
639         if ((o_item = [o_outline_dict objectForKey:
640                             [NSString stringWithFormat: @"%p",
641                             [[o_array objectAtIndex:j] pointerValue]]]) != nil) {
642             [o_outline_view expandItem: o_item];
643         }
644     }
645
646     id o_item = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_item]];
647     NSInteger i_index = [o_outline_view rowForItem:o_item];
648     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_index] byExtendingSelection:NO];
649     [o_outline_view setNeedsDisplay:YES];
650 }
651
652 /* Check if p_item is a child of p_node recursively. We need to check the item
653    existence first since OSX sometimes tries to redraw items that have been
654    deleted. We don't do it when not required since this verification takes
655    quite a long time on big playlists (yes, pretty hacky). */
656
657 - (BOOL)isItem: (playlist_item_t *)p_item inNode: (playlist_item_t *)p_node checkItemExistence:(BOOL)b_check locked:(BOOL)b_locked
658 {
659     playlist_t * p_playlist = pl_Get(VLCIntf);
660     playlist_item_t *p_temp_item = p_item;
661
662     if (!p_node)
663         return NO;
664
665     if (p_node == p_item)
666         return YES;
667
668     if (p_node->i_children < 1)
669         return NO;
670
671     if (p_temp_item) {
672         int i;
673         if (!b_locked) PL_LOCK;
674
675         if (b_check) {
676         /* Since outlineView: willDisplayCell:... may call this function with
677            p_items that don't exist anymore, first check if the item is still
678            in the playlist. Any cleaner solution welcomed. */
679             for (i = 0; i < p_playlist->all_items.i_size; i++) {
680                 if (ARRAY_VAL(p_playlist->all_items, i) == p_item)
681                     break;
682                 else if (i == p_playlist->all_items.i_size - 1)
683                 {
684                     if (!b_locked) PL_UNLOCK;
685                     return NO;
686                 }
687             }
688         }
689
690         while(p_temp_item) {
691             p_temp_item = p_temp_item->p_parent;
692             if (p_temp_item == p_node) {
693                 if (!b_locked) PL_UNLOCK;
694                 return YES;
695             }
696         }
697         if (!b_locked) PL_UNLOCK;
698     }
699     return NO;
700 }
701
702 /* This method is useful for instance to remove the selected children of an
703    already selected node */
704 - (void)removeItemsFrom:(id)o_items ifChildrenOf:(id)o_nodes
705 {
706     NSUInteger itemCount = [o_items count];
707     NSUInteger nodeCount = [o_nodes count];
708     for (NSUInteger i = 0 ; i < itemCount ; i++) {
709         for (NSUInteger j = 0 ; j < nodeCount ; j++) {
710             if (o_items == o_nodes) {
711                 if (j == i) continue;
712             }
713             if ([self isItem: [[o_items objectAtIndex:i] pointerValue]
714                     inNode: [[o_nodes objectAtIndex:j] pointerValue]
715                     checkItemExistence: NO locked:NO]) {
716                 [o_items removeObjectAtIndex:i];
717                 /* We need to execute the next iteration with the same index
718                    since the current item has been deleted */
719                 i--;
720                 break;
721             }
722         }
723     }
724 }
725
726 - (IBAction)savePlaylist:(id)sender
727 {
728     playlist_t * p_playlist = pl_Get(VLCIntf);
729
730     NSSavePanel *o_save_panel = [NSSavePanel savePanel];
731     NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
732
733     [NSBundle loadNibNamed:@"PlaylistAccessoryView" owner:self];
734
735     [o_save_accessory_text setStringValue: _NS("File Format:")];
736     [[o_save_accessory_popup itemAtIndex:0] setTitle: _NS("Extended M3U")];
737     [[o_save_accessory_popup itemAtIndex:1] setTitle: _NS("XML Shareable Playlist Format (XSPF)")];
738     [[o_save_accessory_popup itemAtIndex:2] setTitle: _NS("HTML playlist")];
739
740     [o_save_panel setTitle: _NS("Save Playlist")];
741     [o_save_panel setPrompt: _NS("Save")];
742     [o_save_panel setAccessoryView: o_save_accessory_view];
743     [o_save_panel setNameFieldStringValue: o_name];
744
745     if ([o_save_panel runModal] == NSFileHandlingPanelOKButton) {
746         NSString *o_filename = [[o_save_panel URL] path];
747
748         if ([o_save_accessory_popup indexOfSelectedItem] == 0) {
749             NSString * o_real_filename;
750             NSRange range;
751             range.location = [o_filename length] - [@".m3u" length];
752             range.length = [@".m3u" length];
753
754             if ([o_filename compare:@".m3u" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
755                 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
756             else
757                 o_real_filename = o_filename;
758
759             playlist_Export(p_playlist,
760                 [o_real_filename fileSystemRepresentation],
761                 p_playlist->p_local_category, "export-m3u");
762         } else if ([o_save_accessory_popup indexOfSelectedItem] == 1) {
763             NSString * o_real_filename;
764             NSRange range;
765             range.location = [o_filename length] - [@".xspf" length];
766             range.length = [@".xspf" length];
767
768             if ([o_filename compare:@".xspf" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
769                 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
770             else
771                 o_real_filename = o_filename;
772
773             playlist_Export(p_playlist,
774                 [o_real_filename fileSystemRepresentation],
775                 p_playlist->p_local_category, "export-xspf");
776         } else {
777             NSString * o_real_filename;
778             NSRange range;
779             range.location = [o_filename length] - [@".html" length];
780             range.length = [@".html" length];
781
782             if ([o_filename compare:@".html" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
783                 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
784             else
785                 o_real_filename = o_filename;
786
787             playlist_Export(p_playlist,
788                 [o_real_filename fileSystemRepresentation],
789                 p_playlist->p_local_category, "export-html");
790         }
791     }
792 }
793
794 /* When called retrieves the selected outlineview row and plays that node or item */
795 - (IBAction)playItem:(id)sender
796 {
797     intf_thread_t * p_intf = VLCIntf;
798     playlist_t * p_playlist = pl_Get(p_intf);
799
800     playlist_item_t *p_item;
801     playlist_item_t *p_node = NULL;
802
803     // ignore clicks on column header when handling double action
804     if (sender != nil && [o_outline_view clickedRow] == -1 && sender != o_mi_play)
805         return;
806
807     p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
808
809     PL_LOCK;
810     if (p_item) {
811         if (p_item->i_children == -1) {
812             p_node = p_item->p_parent;
813         } else {
814             p_node = p_item;
815             if (p_node->i_children > 0 && p_node->pp_children[0]->i_children == -1)
816                 p_item = p_node->pp_children[0];
817             else
818                 p_item = NULL;
819         }
820         playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
821     }
822     PL_UNLOCK;
823 }
824
825 - (IBAction)revealItemInFinder:(id)sender
826 {
827     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
828     NSUInteger count = [selectedRows count];
829     NSUInteger indexes[count];
830     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
831
832     NSMutableString * o_mrl;
833     playlist_item_t *p_item;
834     for (NSUInteger i = 0; i < count; i++) {
835         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
836
837         if (! p_item || !p_item->p_input)
838             continue;
839
840         char * psz_url = decode_URI(input_item_GetURI(p_item->p_input));
841         o_mrl = [[NSMutableString alloc] initWithString: [NSString stringWithUTF8String:psz_url ? psz_url : ""]];
842         if (psz_url != NULL)
843             free( psz_url );
844
845         /* perform some checks whether it is a file and if it is local at all... */
846         if ([o_mrl length] > 0) {
847             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
848             if (prefix_range.location != NSNotFound)
849                 [o_mrl deleteCharactersInRange: prefix_range];
850
851             if ([o_mrl characterAtIndex:0] == '/')
852                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
853         }
854
855         [o_mrl release];
856     }
857 }
858
859 /* When called retrieves the selected outlineview row and plays that node or item */
860 - (IBAction)preparseItem:(id)sender
861 {
862     int i_count;
863     NSIndexSet *o_selected_indexes;
864     intf_thread_t * p_intf = VLCIntf;
865     playlist_t * p_playlist = pl_Get(p_intf);
866     playlist_item_t *p_item = NULL;
867
868     o_selected_indexes = [o_outline_view selectedRowIndexes];
869     i_count = [o_selected_indexes count];
870
871     NSUInteger indexes[i_count];
872     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
873     for (int i = 0; i < i_count; i++) {
874         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
875         [o_outline_view deselectRow: indexes[i]];
876
877         if (p_item) {
878             if (p_item->i_children == -1)
879                 libvlc_MetaRequest(p_intf->p_libvlc, p_item->p_input);
880             else
881                 msg_Dbg(p_intf, "preparsing nodes not implemented");
882         }
883     }
884     [self playlistUpdated];
885 }
886
887 - (IBAction)downloadCoverArt:(id)sender
888 {
889     int i_count;
890     NSIndexSet *o_selected_indexes;
891     intf_thread_t * p_intf = VLCIntf;
892     playlist_t * p_playlist = pl_Get(p_intf);
893     playlist_item_t *p_item = NULL;
894
895     o_selected_indexes = [o_outline_view selectedRowIndexes];
896     i_count = [o_selected_indexes count];
897
898     NSUInteger indexes[i_count];
899     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
900     for (int i = 0; i < i_count; i++) {
901         p_item = [[o_outline_view itemAtRow: indexes[i]] pointerValue];
902
903         if (p_item && p_item->i_children == -1)
904             libvlc_ArtRequest(p_intf->p_libvlc, p_item->p_input);
905     }
906     [self playlistUpdated];
907 }
908
909 - (IBAction)selectAll:(id)sender
910 {
911     [o_outline_view selectAll: nil];
912 }
913
914 - (IBAction)showInfoPanel:(id)sender
915 {
916     [[[VLCMain sharedInstance] info] initPanel];
917 }
918
919 - (IBAction)deleteItem:(id)sender
920 {
921     int i_count;
922     NSIndexSet *o_selected_indexes;
923     playlist_t * p_playlist;
924     intf_thread_t * p_intf = VLCIntf;
925
926     o_selected_indexes = [o_outline_view selectedRowIndexes];
927     i_count = [o_selected_indexes count];
928     retainedRowSelection = [o_selected_indexes firstIndex];
929
930     p_playlist = pl_Get(p_intf);
931
932     NSUInteger indexes[i_count];
933     if (i_count == [o_outline_view numberOfRows]) {
934         PL_LOCK;
935         playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
936         PL_UNLOCK;
937         [self playlistUpdated];
938         return;
939     }
940     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
941     for (int i = 0; i < i_count; i++) {
942         id o_item = [o_outline_view itemAtRow: indexes[i]];
943         [o_outline_view deselectRow: indexes[i]];
944
945         PL_LOCK;
946         playlist_item_t *p_item = [o_item pointerValue];
947         if (!p_item || !p_item->p_input) {
948             PL_UNLOCK;
949             continue;
950         }
951
952         if (p_item->i_children != -1) {
953         //is a node and not an item
954             if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
955                 [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
956                         checkItemExistence: NO locked:YES] == YES)
957                 // if current item is in selected node and is playing then stop playlist
958                 playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
959
960                 playlist_NodeDelete(p_playlist, p_item, true, false);
961         } else
962             playlist_DeleteFromInput(p_playlist, p_item->p_input, pl_Locked);
963
964         PL_UNLOCK;
965         [o_outline_dict removeObjectForKey:[NSString stringWithFormat:@"%p", [o_item pointerValue]]];
966         [o_item release];
967     }
968
969     [self playlistUpdated];
970 }
971
972 - (IBAction)sortNodeByName:(id)sender
973 {
974     [self sortNode: SORT_TITLE];
975 }
976
977 - (IBAction)sortNodeByAuthor:(id)sender
978 {
979     [self sortNode: SORT_ARTIST];
980 }
981
982 - (void)sortNode:(int)i_mode
983 {
984     playlist_t * p_playlist = pl_Get(VLCIntf);
985     playlist_item_t * p_item;
986
987     if ([o_outline_view selectedRow] > -1) {
988         p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
989         if (!p_item)
990             return;
991     } else
992         p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
993
994     PL_LOCK;
995     if (p_item->i_children > -1) // the item is a node
996         playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
997     else
998         playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
999
1000     PL_UNLOCK;
1001     [self playlistUpdated];
1002 }
1003
1004 - (input_item_t *)createItem:(NSDictionary *)o_one_item
1005 {
1006     intf_thread_t * p_intf = VLCIntf;
1007     playlist_t * p_playlist = pl_Get(p_intf);
1008
1009     input_item_t *p_input;
1010     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
1011     NSString *o_uri, *o_name, *o_path;
1012     NSURL * o_nsurl;
1013     NSArray *o_options;
1014     NSURL *o_true_file;
1015
1016     /* Get the item */
1017     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
1018     o_nsurl = [NSURL URLWithString: o_uri];
1019     o_path = [o_nsurl path];
1020     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
1021     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
1022
1023     if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
1024         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
1025                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
1026         id o_vlc_open = [[VLCMain sharedInstance] open];
1027
1028         NSString *diskType = [o_vlc_open getVolumeTypeFromMountPath: o_path];
1029         msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
1030
1031         if ([diskType isEqualToString: kVLCMediaDVD])
1032             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1033         else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
1034             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
1035         else if ([diskType isEqualToString: kVLCMediaAudioCD])
1036             o_uri = [NSString stringWithFormat: @"cdda://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1037         else if ([diskType isEqualToString: kVLCMediaVCD])
1038             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1039         else if ([diskType isEqualToString: kVLCMediaSVCD])
1040             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1041         else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
1042             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
1043         else
1044             msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
1045
1046         p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
1047     }
1048     else
1049         p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
1050
1051     if (!p_input)
1052         return NULL;
1053
1054     if (o_options) {
1055         NSUInteger count = [o_options count];
1056         for (NSUInteger i = 0; i < count; i++)
1057             input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
1058     }
1059
1060     /* Recent documents menu */
1061     if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
1062         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
1063
1064     return p_input;
1065 }
1066
1067 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
1068 {
1069     playlist_t * p_playlist = pl_Get(VLCIntf);
1070     NSUInteger count = [o_array count];
1071     BOOL b_usingPlaylist;
1072     if ([self currentPlaylistRoot] == p_playlist->p_ml_category)
1073         b_usingPlaylist = NO;
1074     else
1075         b_usingPlaylist = YES;
1076
1077     PL_LOCK;
1078     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1079         input_item_t *p_input;
1080         NSDictionary *o_one_item;
1081
1082         /* Get the item */
1083         o_one_item = [o_array objectAtIndex:i_item];
1084         p_input = [self createItem: o_one_item];
1085         if (!p_input)
1086             continue;
1087
1088         /* Add the item */
1089         int returnValue = playlist_AddInput(p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist, pl_Locked);
1090         if (returnValue != VLC_SUCCESS) {
1091             vlc_gc_decref(p_input);
1092             continue;
1093         }
1094
1095         if (i_item == 0 && !b_enqueue) {
1096             playlist_item_t *p_item = playlist_ItemGetByInput(p_playlist, p_input);
1097             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item);
1098         }
1099
1100         vlc_gc_decref(p_input);
1101     }
1102     PL_UNLOCK;
1103     [self playlistUpdated];
1104 }
1105
1106 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
1107 {
1108     playlist_t * p_playlist = pl_Get(VLCIntf);
1109     NSUInteger count = [o_array count];
1110
1111     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1112         input_item_t *p_input;
1113         NSDictionary *o_one_item;
1114
1115         /* Get the item */
1116         o_one_item = [o_array objectAtIndex:i_item];
1117         p_input = [self createItem: o_one_item];
1118
1119         if (!p_input)
1120             continue;
1121
1122         /* Add the item */
1123         PL_LOCK;
1124         playlist_NodeAddInput(p_playlist, p_input, p_node,
1125                                       PLAYLIST_INSERT,
1126                                       i_position == -1 ?
1127                                       PLAYLIST_END : i_position + i_item,
1128                                       pl_Locked);
1129
1130
1131         if (i_item == 0 && !b_enqueue) {
1132             playlist_item_t *p_item;
1133             p_item = playlist_ItemGetByInput(p_playlist, p_input);
1134             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
1135         }
1136         PL_UNLOCK;
1137         vlc_gc_decref(p_input);
1138     }
1139     [self playlistUpdated];
1140 }
1141
1142 - (NSMutableArray *)subSearchItem:(playlist_item_t *)p_item
1143 {
1144     playlist_t *p_playlist = pl_Get(VLCIntf);
1145     playlist_item_t *p_selected_item;
1146     int i_selected_row;
1147
1148     i_selected_row = [o_outline_view selectedRow];
1149     if (i_selected_row < 0)
1150         i_selected_row = 0;
1151
1152     p_selected_item = (playlist_item_t *)[[o_outline_view itemAtRow: i_selected_row] pointerValue];
1153
1154     for (NSUInteger i_current = 0; i_current < p_item->i_children ; i_current++) {
1155         char *psz_temp;
1156         NSString *o_current_name, *o_current_author;
1157
1158         PL_LOCK;
1159         o_current_name = [NSString stringWithUTF8String:p_item->pp_children[i_current]->p_input->psz_name];
1160         psz_temp = input_item_GetInfo(p_item->p_input, _("Meta-information"),_("Artist"));
1161         o_current_author = [NSString stringWithUTF8String:psz_temp];
1162         free(psz_temp);
1163         PL_UNLOCK;
1164
1165         if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == NO)
1166             b_selected_item_met = YES;
1167         else if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == YES)
1168             return NULL;
1169         else if (b_selected_item_met == YES &&
1170                     ([o_current_name rangeOfString:[o_search_field
1171                         stringValue] options:NSCaseInsensitiveSearch].length ||
1172                       [o_current_author rangeOfString:[o_search_field
1173                         stringValue] options:NSCaseInsensitiveSearch].length))
1174             /*Adds the parent items in the result array as well, so that we can
1175             expand the tree*/
1176             return [NSMutableArray arrayWithObject: [NSValue valueWithPointer: p_item->pp_children[i_current]]];
1177
1178         if (p_item->pp_children[i_current]->i_children > 0) {
1179             id o_result = [self subSearchItem:
1180                                             p_item->pp_children[i_current]];
1181             if (o_result != NULL) {
1182                 [o_result insertObject: [NSValue valueWithPointer:
1183                                 p_item->pp_children[i_current]] atIndex:0];
1184                 return o_result;
1185             }
1186         }
1187     }
1188     return NULL;
1189 }
1190
1191 - (IBAction)searchItem:(id)sender
1192 {
1193     playlist_t * p_playlist = pl_Get(VLCIntf);
1194     id o_result;
1195
1196     int i_row = -1;
1197
1198     b_selected_item_met = NO;
1199
1200     /* First, only search after the selected item:
1201      * (b_selected_item_met = NO) */
1202     o_result = [self subSearchItem:[self currentPlaylistRoot]];
1203     if (o_result == NULL)
1204         /* If the first search failed, search again from the beginning */
1205         o_result = [self subSearchItem:[self currentPlaylistRoot]];
1206
1207     if (o_result != NULL) {
1208         int i_start;
1209         if ([[o_result objectAtIndex:0] pointerValue] == p_playlist->p_local_category)
1210             i_start = 1;
1211         else
1212             i_start = 0;
1213         NSUInteger count = [o_result count];
1214
1215         for (NSUInteger i = i_start ; i < count - 1 ; i++) {
1216             [o_outline_view expandItem: [o_outline_dict objectForKey:
1217                         [NSString stringWithFormat: @"%p",
1218                         [[o_result objectAtIndex:i] pointerValue]]]];
1219         }
1220         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1221                         [NSString stringWithFormat: @"%p",
1222                         [[o_result objectAtIndex:count - 1 ]
1223                         pointerValue]]]];
1224     }
1225     if (i_row > -1) {
1226         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1227         [o_outline_view scrollRowToVisible: i_row];
1228     }
1229 }
1230
1231 - (IBAction)recursiveExpandNode:(id)sender
1232 {
1233     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1234     NSUInteger count = [selectedRows count];
1235     NSUInteger indexes[count];
1236     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1237
1238     id o_item;
1239     playlist_item_t *p_item;
1240     for (NSUInteger i = 0; i < count; i++) {
1241         o_item = [o_outline_view itemAtRow: indexes[i]];
1242         p_item = (playlist_item_t *)[o_item pointerValue];
1243
1244         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
1245             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1246
1247         /* We need to collapse the node first, since OSX refuses to recursively
1248          expand an already expanded node, even if children nodes are collapsed. */
1249         [o_outline_view collapseItem: o_item collapseChildren: YES];
1250         [o_outline_view expandItem: o_item expandChildren: YES];
1251
1252         selectedRows = [o_outline_view selectedRowIndexes];
1253         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1254     }
1255 }
1256
1257 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1258 {
1259     if (!b_playlistmenu_nib_loaded)
1260         b_playlistmenu_nib_loaded = [NSBundle loadNibNamed:@"PlaylistMenu" owner:self];
1261
1262     NSPoint pt;
1263     bool b_rows;
1264     bool b_item_sel;
1265
1266     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1267     int row = [o_outline_view rowAtPoint:pt];
1268     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
1269         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1270
1271     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
1272     b_rows = [o_outline_view numberOfRows] != 0;
1273
1274     [o_mi_play setEnabled: b_item_sel];
1275     [o_mi_delete setEnabled: b_item_sel];
1276     [o_mi_selectall setEnabled: b_rows];
1277     [o_mi_info setEnabled: b_item_sel];
1278     [o_mi_preparse setEnabled: b_item_sel];
1279     [o_mi_recursive_expand setEnabled: b_item_sel];
1280     [o_mi_sort_name setEnabled: b_item_sel];
1281     [o_mi_sort_author setEnabled: b_item_sel];
1282
1283     return o_ctx_menu;
1284 }
1285
1286 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1287 {
1288     int i_mode, i_type = 0;
1289     intf_thread_t *p_intf = VLCIntf;
1290     NSString * o_identifier = [o_tc identifier];
1291
1292     playlist_t *p_playlist = pl_Get(p_intf);
1293
1294     if ([o_identifier isEqualToString:TRACKNUM_COLUMN])
1295         i_mode = SORT_TRACK_NUMBER;
1296     else if ([o_identifier isEqualToString:TITLE_COLUMN])
1297         i_mode = SORT_TITLE;
1298     else if ([o_identifier isEqualToString:ARTIST_COLUMN])
1299         i_mode = SORT_ARTIST;
1300     else if ([o_identifier isEqualToString:GENRE_COLUMN])
1301         i_mode = SORT_GENRE;
1302     else if ([o_identifier isEqualToString:DURATION_COLUMN])
1303         i_mode = SORT_DURATION;
1304     else if ([o_identifier isEqualToString:ALBUM_COLUMN])
1305         i_mode = SORT_ALBUM;
1306     else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN])
1307         i_mode = SORT_DESCRIPTION;
1308     else if ([o_identifier isEqualToString:URI_COLUMN])
1309         i_mode = SORT_URI;
1310     else
1311         return;
1312
1313     if (o_tc_sortColumn == o_tc)
1314         b_isSortDescending = !b_isSortDescending;
1315     else
1316         b_isSortDescending = false;
1317
1318     if (b_isSortDescending)
1319         i_type = ORDER_REVERSE;
1320     else
1321         i_type = ORDER_NORMAL;
1322
1323     PL_LOCK;
1324     playlist_RecursiveNodeSort(p_playlist, [self currentPlaylistRoot], i_mode, i_type);
1325     PL_UNLOCK;
1326
1327     [self playlistUpdated];
1328
1329     o_tc_sortColumn = o_tc;
1330     [o_outline_view setHighlightedTableColumn:o_tc];
1331
1332     if (b_isSortDescending)
1333         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1334     else
1335         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1336 }
1337
1338
1339 - (void)outlineView:(NSOutlineView *)outlineView
1340                                 willDisplayCell:(id)cell
1341                                 forTableColumn:(NSTableColumn *)tableColumn
1342                                 item:(id)item
1343 {
1344     /* this method can be called when VLC is already dead, hence the extra checks */
1345     intf_thread_t * p_intf = VLCIntf;
1346     if (!p_intf)
1347         return;
1348     playlist_t *p_playlist = pl_Get(p_intf);
1349     if (!p_playlist)
1350         return;
1351
1352     id o_playing_item;
1353
1354     PL_LOCK;
1355     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1356     PL_UNLOCK;
1357
1358     if ([self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
1359                         || [o_playing_item isEqual: item])
1360         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1361     else
1362         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1363 }
1364
1365 - (id)playingItem
1366 {
1367     playlist_t *p_playlist = pl_Get(VLCIntf);
1368
1369     id o_playing_item;
1370
1371     PL_LOCK;
1372     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1373     PL_UNLOCK;
1374
1375     return o_playing_item;
1376 }
1377
1378 - (NSArray *)draggedItems
1379 {
1380     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1381 }
1382
1383 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1384 {
1385     NSTableColumn * o_work_tc;
1386
1387     if (i_state == NSOnState) {
1388         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1389         [o_work_tc setEditable: NO];
1390         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1391
1392         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1393
1394         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1395             [o_work_tc setWidth: 20.];
1396             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1397             [[o_work_tc headerCell] setStringValue: @"#"];
1398         }
1399
1400         [o_outline_view addTableColumn: o_work_tc];
1401         [o_work_tc release];
1402         [o_outline_view reloadData];
1403         [o_outline_view setNeedsDisplay: YES];
1404     }
1405     else
1406         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1407
1408     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
1409 }
1410
1411 - (void)saveTableColumns
1412 {
1413     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1414     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1415     NSUInteger count = [o_columns count];
1416     NSTableColumn * o_currentColumn;
1417     for (NSUInteger i = 0; i < count; i++) {
1418         o_currentColumn = [o_columns objectAtIndex:i];
1419         [o_arrayToSave addObject:[NSArray arrayWithObjects:[o_currentColumn identifier], [NSNumber numberWithFloat:[o_currentColumn width]], nil]];
1420     }
1421     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1422     [[NSUserDefaults standardUserDefaults] synchronize];
1423     [o_columns release];
1424     [o_arrayToSave release];
1425 }
1426
1427 @end
1428
1429 @implementation VLCPlaylist (NSOutlineViewDataSource)
1430
1431 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1432 {
1433     id o_value = [super outlineView: outlineView child: index ofItem: item];
1434
1435     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1436     return o_value;
1437 }
1438
1439 /* Required for drag & drop and reordering */
1440 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1441 {
1442     playlist_t *p_playlist = pl_Get(VLCIntf);
1443
1444     /* First remove the items that were moved during the last drag & drop
1445        operation */
1446     [o_items_array removeAllObjects];
1447     [o_nodes_array removeAllObjects];
1448
1449     NSUInteger itemCount = [items count];
1450
1451     for (NSUInteger i = 0 ; i < itemCount ; i++) {
1452         id o_item = [items objectAtIndex:i];
1453
1454         /* Fill the items and nodes to move in 2 different arrays */
1455         if (((playlist_item_t *)[o_item pointerValue])->i_children > 0)
1456             [o_nodes_array addObject: o_item];
1457         else
1458             [o_items_array addObject: o_item];
1459     }
1460
1461     /* Now we need to check if there are selected items that are in already
1462        selected nodes. In that case, we only want to move the nodes */
1463     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1464     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1465
1466     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1467        a Drop operation coming from the playlist. */
1468
1469     [pboard declareTypes: [NSArray arrayWithObject:@"VLCPlaylistItemPboardType"] owner: self];
1470     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1471
1472     return YES;
1473 }
1474
1475 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1476 {
1477     playlist_t *p_playlist = pl_Get(VLCIntf);
1478     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1479
1480     if (!p_playlist) return NSDragOperationNone;
1481
1482     /* Dropping ON items is not allowed if item is not a node */
1483     if (item) {
1484         if (index == NSOutlineViewDropOnItemIndex &&
1485                 ((playlist_item_t *)[item pointerValue])->i_children == -1) {
1486             return NSDragOperationNone;
1487         }
1488     }
1489
1490     /* We refuse to drop an item in anything else than a child of the General
1491        Node. We still accept items that would be root nodes of the outlineview
1492        however, to allow drop in an empty playlist. */
1493     if (!(([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO locked: NO] ||
1494             (var_CreateGetBool(p_playlist, "media-library") && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO locked: NO])) || item == nil)) {
1495         return NSDragOperationNone;
1496     }
1497
1498     /* Drop from the Playlist */
1499     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1500         NSUInteger count = [o_nodes_array count];
1501         for (NSUInteger i = 0 ; i < count ; i++) {
1502             /* We refuse to Drop in a child of an item we are moving */
1503             if ([self isItem: [item pointerValue] inNode: [[o_nodes_array objectAtIndex:i] pointerValue] checkItemExistence: NO locked:NO]) {
1504                 return NSDragOperationNone;
1505             }
1506         }
1507         return NSDragOperationMove;
1508     }
1509     /* Drop from the Finder */
1510     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1511         return NSDragOperationGeneric;
1512     }
1513     return NSDragOperationNone;
1514 }
1515
1516 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1517 {
1518     playlist_t * p_playlist =  pl_Get(VLCIntf);
1519     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1520
1521     /* Drag & Drop inside the playlist */
1522     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1523         if (index == -1) // this is no valid target, sanitize to top of table
1524             index = 0;
1525
1526         int i_row = 0;
1527         playlist_item_t *p_new_parent, *p_item = NULL;
1528         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1529         /* If the item is to be dropped as root item of the outline, make it a
1530            child of the respective general node, if is either the pl or the ml
1531            Else, choose the proposed parent as parent. */
1532         if (item == nil) {
1533             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1534                 p_new_parent = [self currentPlaylistRoot];
1535             else
1536                 return NO;
1537         }
1538         else
1539             p_new_parent = [item pointerValue];
1540
1541         /* Make sure the proposed parent is a node.
1542            (This should never be true) */
1543         if (p_new_parent->i_children < 0)
1544             return NO;
1545
1546         NSUInteger count = [o_all_items count];
1547         if (count == 0)
1548             return NO;
1549
1550         playlist_item_t **pp_items = (playlist_item_t **)calloc(count, sizeof(playlist_item_t*));
1551         if (!pp_items)
1552             return NO;
1553
1554         PL_LOCK;
1555         NSUInteger j = 0;
1556         for (NSUInteger i = 0; i < count; i++) {
1557             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1558             if (p_item)
1559                 pp_items[j++] = p_item;
1560         }
1561
1562         if (j == 0 || playlist_TreeMoveMany(p_playlist, j, pp_items, p_new_parent, index) != VLC_SUCCESS) {
1563             PL_UNLOCK;
1564             free(pp_items);
1565             return NO;
1566         }
1567
1568         PL_UNLOCK;
1569         free(pp_items);
1570
1571         [self playlistUpdated];
1572         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex:0] pointerValue]]]];
1573
1574         if (i_row == -1)
1575             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1576
1577         [o_outline_view deselectAll: self];
1578         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1579         [o_outline_view scrollRowToVisible: i_row];
1580
1581         return YES;
1582     }
1583
1584     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1585         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1586             return NO;
1587
1588         playlist_item_t *p_node = [item pointerValue];
1589
1590         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1591                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1592         NSUInteger count = [o_values count];
1593         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1594         input_thread_t * p_input = pl_CurrentInput(VLCIntf);
1595         BOOL b_returned = NO;
1596
1597         if (count == 1 && p_input) {
1598             b_returned = input_AddSubtitle(p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true);
1599             vlc_object_release(p_input);
1600             if (!b_returned)
1601                 return YES;
1602         }
1603         else if (p_input)
1604             vlc_object_release(p_input);
1605
1606         for (NSUInteger i = 0; i < count; i++) {
1607             NSDictionary *o_dic;
1608             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1609             if (!psz_uri)
1610                 continue;
1611
1612             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1613
1614             free(psz_uri);
1615
1616             [o_array addObject: o_dic];
1617         }
1618
1619         if (item == nil)
1620             [self appendArray:o_array atPos:index enqueue: YES];
1621         else {
1622             assert(p_node->i_children != -1);
1623             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1624         }
1625         return YES;
1626     }
1627     return NO;
1628 }
1629
1630 @end