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