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