]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx/playlist: added missing sanity check
[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
676                     inNode: (playlist_item_t *)p_node
677                     checkItemExistence:(BOOL)b_check
678                     locked:(BOOL)b_locked
679
680 {
681     playlist_t * p_playlist = pl_Get( VLCIntf );
682     playlist_item_t *p_temp_item = p_item;
683
684     if( p_node == p_item )
685         return YES;
686
687     if( p_node->i_children < 1)
688         return NO;
689
690     if ( p_temp_item )
691     {
692         int i;
693         if(!b_locked) PL_LOCK;
694
695         if( b_check )
696         {
697         /* Since outlineView: willDisplayCell:... may call this function with
698            p_items that don't exist anymore, first check if the item is still
699            in the playlist. Any cleaner solution welcomed. */
700             for( i = 0; i < p_playlist->all_items.i_size; i++ )
701             {
702                 if( ARRAY_VAL( p_playlist->all_items, i) == p_item ) 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 - (BOOL)isItem: (playlist_item_t *)p_item
726                     inNode: (playlist_item_t *)p_node
727                     checkItemExistence:(BOOL)b_check
728 {
729     return [self isItem:p_item inNode:p_node checkItemExistence:b_check locked:NO];
730 }
731
732 /* This method is useful for instance to remove the selected children of an
733    already selected node */
734 - (void)removeItemsFrom:(id)o_items ifChildrenOf:(id)o_nodes
735 {
736     NSUInteger itemCount = [o_items count];
737     NSUInteger nodeCount = [o_nodes count];
738     for( NSUInteger i = 0 ; i < itemCount ; i++ )
739     {
740         for ( NSUInteger j = 0 ; j < nodeCount ; j++ )
741         {
742             if( o_items == o_nodes)
743             {
744                 if( j == i ) continue;
745             }
746             if( [self isItem: [[o_items objectAtIndex:i] pointerValue]
747                     inNode: [[o_nodes objectAtIndex:j] pointerValue]
748                     checkItemExistence: NO locked:NO] )
749             {
750                 [o_items removeObjectAtIndex:i];
751                 /* We need to execute the next iteration with the same index
752                    since the current item has been deleted */
753                 i--;
754                 break;
755             }
756         }
757     }
758 }
759
760 - (IBAction)savePlaylist:(id)sender
761 {
762     playlist_t * p_playlist = pl_Get( VLCIntf );
763
764     NSSavePanel *o_save_panel = [NSSavePanel savePanel];
765     NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
766
767     [o_save_panel setTitle: _NS("Save Playlist")];
768     [o_save_panel setPrompt: _NS("Save")];
769     [o_save_panel setAccessoryView: o_save_accessory_view];
770
771     if( [o_save_panel runModalForDirectory: nil
772             file: o_name] == NSOKButton )
773     {
774         NSString *o_filename = [[o_save_panel URL] path];
775
776         if( [o_save_accessory_popup indexOfSelectedItem] == 0 )
777         {
778             NSString * o_real_filename;
779             NSRange range;
780             range.location = [o_filename length] - [@".m3u" length];
781             range.length = [@".m3u" length];
782
783             if( [o_filename compare:@".m3u" options: NSCaseInsensitiveSearch
784                                              range: range] != NSOrderedSame )
785             {
786                 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
787             }
788             else
789             {
790                 o_real_filename = o_filename;
791             }
792             playlist_Export( p_playlist,
793                 [o_real_filename fileSystemRepresentation],
794                 p_playlist->p_local_category, "export-m3u" );
795         }
796         else if( [o_save_accessory_popup indexOfSelectedItem] == 1 )
797         {
798             NSString * o_real_filename;
799             NSRange range;
800             range.location = [o_filename length] - [@".xspf" length];
801             range.length = [@".xspf" length];
802
803             if( [o_filename compare:@".xspf" options: NSCaseInsensitiveSearch
804                                              range: range] != NSOrderedSame )
805             {
806                 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
807             }
808             else
809             {
810                 o_real_filename = o_filename;
811             }
812             playlist_Export( p_playlist,
813                 [o_real_filename fileSystemRepresentation],
814                 p_playlist->p_local_category, "export-xspf" );
815         }
816         else
817         {
818             NSString * o_real_filename;
819             NSRange range;
820             range.location = [o_filename length] - [@".html" length];
821             range.length = [@".html" length];
822
823             if( [o_filename compare:@".html" options: NSCaseInsensitiveSearch
824                                              range: range] != NSOrderedSame )
825             {
826                 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
827             }
828             else
829             {
830                 o_real_filename = o_filename;
831             }
832             playlist_Export( p_playlist,
833                 [o_real_filename fileSystemRepresentation],
834                 p_playlist->p_local_category, "export-html" );
835         }
836     }
837 }
838
839 /* When called retrieves the selected outlineview row and plays that node or item */
840 - (IBAction)playItem:(id)sender
841 {
842     intf_thread_t * p_intf = VLCIntf;
843     playlist_t * p_playlist = pl_Get( p_intf );
844
845     playlist_item_t *p_item;
846     playlist_item_t *p_node = NULL;
847
848     // ignore clicks on column header when handling double action
849     if( sender != nil && [o_outline_view clickedRow] == -1 && sender != o_mi_play)
850         return;
851
852     p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
853
854     PL_LOCK;
855     if( p_item )
856     {
857         if( p_item->i_children == -1 )
858         {
859             p_node = p_item->p_parent;
860         }
861         else
862         {
863             p_node = p_item;
864             if( p_node->i_children > 0 && p_node->pp_children[0]->i_children == -1 )
865             {
866                 p_item = p_node->pp_children[0];
867             }
868             else
869             {
870                 p_item = NULL;
871             }
872         }
873         playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item );
874     }
875     PL_UNLOCK;
876 }
877
878 - (IBAction)revealItemInFinder:(id)sender
879 {
880     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
881     NSUInteger count = [selectedRows count];
882     NSUInteger indexes[count];
883     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
884
885     NSMutableString * o_mrl;
886     playlist_item_t *p_item;
887     for (NSUInteger i = 0; i < count; i++) {
888         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
889
890         if(! p_item || !p_item->p_input )
891             continue;
892
893         o_mrl = [[NSMutableString alloc] initWithFormat: @"%s", decode_URI( input_item_GetURI( p_item->p_input ))];
894
895         /* perform some checks whether it is a file and if it is local at all... */
896         if ([o_mrl length] > 0)
897         {
898             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
899             if( prefix_range.location != NSNotFound )
900                 [o_mrl deleteCharactersInRange: prefix_range];
901
902             if( [o_mrl characterAtIndex:0] == '/' )
903                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
904         }
905
906         [o_mrl release];
907     }
908 }
909
910 /* When called retrieves the selected outlineview row and plays that node or item */
911 - (IBAction)preparseItem:(id)sender
912 {
913     int i_count;
914     NSIndexSet *o_selected_indexes;
915     intf_thread_t * p_intf = VLCIntf;
916     playlist_t * p_playlist = pl_Get( p_intf );
917     playlist_item_t *p_item = NULL;
918
919     o_selected_indexes = [o_outline_view selectedRowIndexes];
920     i_count = [o_selected_indexes count];
921
922     NSUInteger indexes[i_count];
923     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
924     for (int i = 0; i < i_count; i++)
925     {
926         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
927         [o_outline_view deselectRow: indexes[i]];
928
929         if( p_item )
930         {
931             if( p_item->i_children == -1 )
932                 playlist_PreparseEnqueue( p_playlist, p_item->p_input );
933             else
934                 msg_Dbg( p_intf, "preparsing nodes not implemented" );
935         }
936     }
937     [self playlistUpdated];
938 }
939
940 - (IBAction)downloadCoverArt:(id)sender
941 {
942     int i_count;
943     NSIndexSet *o_selected_indexes;
944     intf_thread_t * p_intf = VLCIntf;
945     playlist_t * p_playlist = pl_Get( p_intf );
946     playlist_item_t *p_item = NULL;
947
948     o_selected_indexes = [o_outline_view selectedRowIndexes];
949     i_count = [o_selected_indexes count];
950
951     NSUInteger indexes[i_count];
952     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
953     for (int i = 0; i < i_count; i++)
954     {
955         p_item = [[o_outline_view itemAtRow: indexes[i]] pointerValue];
956
957         if( p_item && p_item->i_children == -1 )
958             playlist_AskForArtEnqueue( p_playlist, p_item->p_input );
959     }
960     [self playlistUpdated];
961 }
962
963 - (IBAction)selectAll:(id)sender
964 {
965     [o_outline_view selectAll: nil];
966 }
967
968 - (IBAction)deleteItem:(id)sender
969 {
970     int i_count;
971     NSIndexSet *o_selected_indexes;
972     playlist_t * p_playlist;
973     intf_thread_t * p_intf = VLCIntf;
974
975     o_selected_indexes = [o_outline_view selectedRowIndexes];
976     i_count = [o_selected_indexes count];
977
978     p_playlist = pl_Get( p_intf );
979
980     NSUInteger indexes[i_count];
981     if (i_count == [o_outline_view numberOfRows])
982     {
983 #ifndef NDEBUG
984         msg_Dbg( p_intf, "user selected entire list, deleting current playlist root instead of individual items" );
985 #endif
986         PL_LOCK;
987         playlist_NodeDelete( p_playlist, [self currentPlaylistRoot], true, false );
988         PL_UNLOCK;
989         [self playlistUpdated];
990         return;
991     }
992     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
993     for (int i = 0; i < i_count; i++)
994     {
995         id o_item = [o_outline_view itemAtRow: indexes[i]];
996         [o_outline_view deselectRow: indexes[i]];
997
998         PL_LOCK;
999         playlist_item_t *p_item = [o_item pointerValue];
1000 #ifndef NDEBUG
1001         msg_Dbg( p_intf, "deleting item %i (of %i) with id \"%i\", pointerValue \"%p\" and %i children", i+1, i_count,
1002                 p_item->p_input->i_id, [o_item pointerValue], p_item->i_children +1 );
1003 #endif
1004
1005         if( p_item->i_children != -1 )
1006         //is a node and not an item
1007         {
1008             if( playlist_Status( p_playlist ) != PLAYLIST_STOPPED &&
1009                 [self isItem: playlist_CurrentPlayingItem( p_playlist ) inNode: ((playlist_item_t *)[o_item pointerValue])
1010                         checkItemExistence: NO locked:YES] == YES )
1011                 // if current item is in selected node and is playing then stop playlist
1012                 playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked );
1013
1014                 playlist_NodeDelete( p_playlist, p_item, true, false );
1015         }
1016         else
1017             playlist_DeleteFromInput( p_playlist, p_item->p_input, pl_Locked );
1018
1019         PL_UNLOCK;
1020         [o_outline_dict removeObjectForKey:[NSString stringWithFormat:@"%p", [o_item pointerValue]]];
1021         [o_item release];
1022     }
1023
1024     [self playlistUpdated];
1025 }
1026
1027 - (IBAction)sortNodeByName:(id)sender
1028 {
1029     [self sortNode: SORT_TITLE];
1030 }
1031
1032 - (IBAction)sortNodeByAuthor:(id)sender
1033 {
1034     [self sortNode: SORT_ARTIST];
1035 }
1036
1037 - (void)sortNode:(int)i_mode
1038 {
1039     playlist_t * p_playlist = pl_Get( VLCIntf );
1040     playlist_item_t * p_item;
1041
1042     if( [o_outline_view selectedRow] > -1 )
1043     {
1044         p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
1045         if (!p_item)
1046             return;
1047     }
1048     else
1049         p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
1050
1051     PL_LOCK;
1052     if( p_item->i_children > -1 ) // the item is a node
1053     {
1054         playlist_RecursiveNodeSort( p_playlist, p_item, i_mode, ORDER_NORMAL );
1055     }
1056     else
1057     {
1058         playlist_RecursiveNodeSort( p_playlist,
1059                 p_item->p_parent, i_mode, ORDER_NORMAL );
1060     }
1061     PL_UNLOCK;
1062     [self playlistUpdated];
1063 }
1064
1065 - (input_item_t *)createItem:(NSDictionary *)o_one_item
1066 {
1067     intf_thread_t * p_intf = VLCIntf;
1068     playlist_t * p_playlist = pl_Get( p_intf );
1069
1070     input_item_t *p_input;
1071     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
1072     NSString *o_uri, *o_name, *o_path;
1073     NSURL * o_nsurl;
1074     NSArray *o_options;
1075     NSURL *o_true_file;
1076
1077     /* Get the item */
1078     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
1079     o_nsurl = [NSURL URLWithString: o_uri];
1080     o_path = [o_nsurl path];
1081     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
1082     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
1083
1084     if( [[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
1085         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
1086                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL] )
1087     {
1088
1089         id o_vlc_open = [[VLCMain sharedInstance] open];
1090
1091         char *diskType = [o_vlc_open getVolumeTypeFromMountPath: o_path];
1092         msg_Dbg( p_intf, "detected optical media of type '%s' in the file input", diskType );
1093
1094         if (diskType == kVLCMediaDVD)
1095         {
1096             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1097         }
1098         else if (diskType == kVLCMediaVideoTSFolder)
1099         {
1100             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
1101         }
1102         else if (diskType == kVLCMediaAudioCD)
1103         {
1104             o_uri = [NSString stringWithFormat: @"cdda://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1105         }
1106         else if (diskType == kVLCMediaVCD)
1107         {
1108             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1109         }
1110         else if (diskType == kVLCMediaSVCD)
1111         {
1112             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1113         }
1114         else if (diskType == kVLCMediaBD || diskType == kVLCMediaBDMVFolder)
1115         {
1116             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
1117         }
1118         else
1119         {
1120             msg_Warn( VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String] );
1121         }
1122
1123         p_input = input_item_New( [o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String] );
1124     }
1125     else
1126         p_input = input_item_New( [o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL );
1127
1128     if( !p_input )
1129         return NULL;
1130
1131     if( o_options )
1132     {
1133         NSUInteger count = [o_options count];
1134         for( NSUInteger i = 0; i < count; i++ )
1135         {
1136             input_item_AddOption( p_input, [[o_options objectAtIndex:i] UTF8String],
1137                                   VLC_INPUT_OPTION_TRUSTED );
1138         }
1139     }
1140
1141     /* Recent documents menu */
1142     if( o_nsurl != nil && (BOOL)config_GetInt( p_playlist, "macosx-recentitems" ) == YES )
1143     {
1144         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
1145     }
1146     return p_input;
1147 }
1148
1149 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
1150 {
1151     playlist_t * p_playlist = pl_Get( VLCIntf );
1152     NSUInteger count = [o_array count];
1153     BOOL b_usingPlaylist;
1154     if ([self currentPlaylistRoot] == p_playlist->p_ml_category)
1155         b_usingPlaylist = NO;
1156     else
1157         b_usingPlaylist = YES;
1158
1159     PL_LOCK;
1160     for( NSUInteger i_item = 0; i_item < count; i_item++ )
1161     {
1162         input_item_t *p_input;
1163         NSDictionary *o_one_item;
1164
1165         /* Get the item */
1166         o_one_item = [o_array objectAtIndex: i_item];
1167         p_input = [self createItem: o_one_item];
1168         if( !p_input )
1169         {
1170             continue;
1171         }
1172
1173         /* Add the item */
1174         /* FIXME: playlist_AddInput() can fail */
1175
1176         playlist_AddInput( p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist,
1177          pl_Locked );
1178
1179         if( i_item == 0 && !b_enqueue )
1180         {
1181             playlist_item_t *p_item = playlist_ItemGetByInput( p_playlist, p_input );
1182             playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item );
1183         }
1184
1185         vlc_gc_decref( p_input );
1186     }
1187     PL_UNLOCK;
1188     [self playlistUpdated];
1189 }
1190
1191 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
1192 {
1193     playlist_t * p_playlist = pl_Get( VLCIntf );
1194     NSUInteger count = [o_array count];
1195
1196     for( NSUInteger i_item = 0; i_item < count; i_item++ )
1197     {
1198         input_item_t *p_input;
1199         NSDictionary *o_one_item;
1200
1201         /* Get the item */
1202         o_one_item = [o_array objectAtIndex: i_item];
1203         p_input = [self createItem: o_one_item];
1204
1205         if( !p_input ) continue;
1206
1207         /* Add the item */
1208         PL_LOCK;
1209         playlist_NodeAddInput( p_playlist, p_input, p_node,
1210                                       PLAYLIST_INSERT,
1211                                       i_position == -1 ?
1212                                       PLAYLIST_END : i_position + i_item,
1213                                       pl_Locked );
1214
1215
1216         if( i_item == 0 && !b_enqueue )
1217         {
1218             playlist_item_t *p_item;
1219             p_item = playlist_ItemGetByInput( p_playlist, p_input );
1220             playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item );
1221         }
1222         PL_UNLOCK;
1223         vlc_gc_decref( p_input );
1224     }
1225     [self playlistUpdated];
1226 }
1227
1228 - (NSMutableArray *)subSearchItem:(playlist_item_t *)p_item
1229 {
1230     playlist_t *p_playlist = pl_Get( VLCIntf );
1231     playlist_item_t *p_selected_item;
1232     int i_selected_row;
1233
1234     i_selected_row = [o_outline_view selectedRow];
1235     if (i_selected_row < 0)
1236         i_selected_row = 0;
1237
1238     p_selected_item = (playlist_item_t *)[[o_outline_view itemAtRow: i_selected_row] pointerValue];
1239
1240     for( NSUInteger i_current = 0; i_current < p_item->i_children ; i_current++ )
1241     {
1242         char *psz_temp;
1243         NSString *o_current_name, *o_current_author;
1244
1245         PL_LOCK;
1246         o_current_name = [NSString stringWithUTF8String:
1247             p_item->pp_children[i_current]->p_input->psz_name];
1248         psz_temp = input_item_GetInfo( p_item->p_input ,
1249                    _("Meta-information"),_("Artist") );
1250         o_current_author = [NSString stringWithUTF8String: psz_temp];
1251         free( psz_temp);
1252         PL_UNLOCK;
1253
1254         if( p_selected_item == p_item->pp_children[i_current] &&
1255                     b_selected_item_met == NO )
1256         {
1257             b_selected_item_met = YES;
1258         }
1259         else if( p_selected_item == p_item->pp_children[i_current] &&
1260                     b_selected_item_met == YES )
1261         {
1262             return NULL;
1263         }
1264         else if( b_selected_item_met == YES &&
1265                     ( [o_current_name rangeOfString:[o_search_field
1266                         stringValue] options:NSCaseInsensitiveSearch].length ||
1267                       [o_current_author rangeOfString:[o_search_field
1268                         stringValue] options:NSCaseInsensitiveSearch].length ) )
1269         {
1270             /*Adds the parent items in the result array as well, so that we can
1271             expand the tree*/
1272             return [NSMutableArray arrayWithObject: [NSValue
1273                             valueWithPointer: p_item->pp_children[i_current]]];
1274         }
1275         if( p_item->pp_children[i_current]->i_children > 0 )
1276         {
1277             id o_result = [self subSearchItem:
1278                                             p_item->pp_children[i_current]];
1279             if( o_result != NULL )
1280             {
1281                 [o_result insertObject: [NSValue valueWithPointer:
1282                                 p_item->pp_children[i_current]] atIndex:0];
1283                 return o_result;
1284             }
1285         }
1286     }
1287     return NULL;
1288 }
1289
1290 - (IBAction)searchItem:(id)sender
1291 {
1292     playlist_t * p_playlist = pl_Get( VLCIntf );
1293     id o_result;
1294
1295     int i_row = -1;
1296
1297     b_selected_item_met = NO;
1298
1299         /*First, only search after the selected item:*
1300          *(b_selected_item_met = NO)                 */
1301     o_result = [self subSearchItem:[self currentPlaylistRoot]];
1302     if( o_result == NULL )
1303     {
1304         /* If the first search failed, search again from the beginning */
1305         o_result = [self subSearchItem:[self currentPlaylistRoot]];
1306     }
1307     if( o_result != NULL )
1308     {
1309         int i_start;
1310         if( [[o_result objectAtIndex: 0] pointerValue] == p_playlist->p_local_category )
1311             i_start = 1;
1312         else
1313             i_start = 0;
1314         NSUInteger count = [o_result count];
1315
1316         for( NSUInteger i = i_start ; i < count - 1 ; i++ )
1317         {
1318             [o_outline_view expandItem: [o_outline_dict objectForKey:
1319                         [NSString stringWithFormat: @"%p",
1320                         [[o_result objectAtIndex: i] pointerValue]]]];
1321         }
1322         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1323                         [NSString stringWithFormat: @"%p",
1324                         [[o_result objectAtIndex: count - 1 ]
1325                         pointerValue]]]];
1326     }
1327     if( i_row > -1 )
1328     {
1329         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1330         [o_outline_view scrollRowToVisible: i_row];
1331     }
1332 }
1333
1334 - (IBAction)recursiveExpandNode:(id)sender
1335 {
1336     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1337     NSUInteger count = [selectedRows count];
1338     NSUInteger indexes[count];
1339     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1340
1341     id o_item;
1342     playlist_item_t *p_item;
1343     for (NSUInteger i = 0; i < count; i++) {
1344         o_item = [o_outline_view itemAtRow: indexes[i]];
1345         p_item = (playlist_item_t *)[o_item pointerValue];
1346
1347         if( ![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item] )
1348             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1349
1350         /* We need to collapse the node first, since OSX refuses to recursively
1351          expand an already expanded node, even if children nodes are collapsed. */
1352         [o_outline_view collapseItem: o_item collapseChildren: YES];
1353         [o_outline_view expandItem: o_item expandChildren: YES];
1354
1355         selectedRows = [o_outline_view selectedRowIndexes];
1356         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1357     }
1358 }
1359
1360 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1361 {
1362     NSPoint pt;
1363     bool b_rows;
1364     bool b_item_sel;
1365
1366     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1367     int row = [o_outline_view rowAtPoint:pt];
1368     if( row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row] )
1369         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1370
1371     b_item_sel = ( row != -1 && [o_outline_view selectedRow] != -1 );
1372     b_rows = [o_outline_view numberOfRows] != 0;
1373
1374     [o_mi_play setEnabled: b_item_sel];
1375     [o_mi_delete setEnabled: b_item_sel];
1376     [o_mi_selectall setEnabled: b_rows];
1377     [o_mi_info setEnabled: b_item_sel];
1378     [o_mi_preparse setEnabled: b_item_sel];
1379     [o_mi_recursive_expand setEnabled: b_item_sel];
1380     [o_mi_sort_name setEnabled: b_item_sel];
1381     [o_mi_sort_author setEnabled: b_item_sel];
1382
1383     return( o_ctx_menu );
1384 }
1385
1386 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1387 {
1388     int i_mode, i_type = 0;
1389     intf_thread_t *p_intf = VLCIntf;
1390     NSString * o_identifier = [o_tc identifier];
1391
1392     playlist_t *p_playlist = pl_Get( p_intf );
1393
1394     if( [o_identifier isEqualToString:TRACKNUM_COLUMN] )
1395         i_mode = SORT_TRACK_NUMBER;
1396     else if( [o_identifier isEqualToString:TITLE_COLUMN] )
1397         i_mode = SORT_TITLE;
1398     else if( [o_identifier isEqualToString:ARTIST_COLUMN] )
1399         i_mode = SORT_ARTIST;
1400     else if( [o_identifier isEqualToString:GENRE_COLUMN] )
1401         i_mode = SORT_GENRE;
1402     else if( [o_identifier isEqualToString:DURATION_COLUMN] )
1403         i_mode = SORT_DURATION;
1404     else if( [o_identifier isEqualToString:ALBUM_COLUMN] )
1405         i_mode = SORT_ALBUM;
1406     else if( [o_identifier isEqualToString:DESCRIPTION_COLUMN] )
1407         i_mode = SORT_DESCRIPTION;
1408     else if( [o_identifier isEqualToString:URI_COLUMN] )
1409         i_mode = SORT_URI;
1410     else
1411         return;
1412
1413     if( o_tc_sortColumn == o_tc )
1414         b_isSortDescending = !b_isSortDescending;
1415     else
1416         b_isSortDescending = false;
1417
1418     if( b_isSortDescending )
1419         i_type = ORDER_REVERSE;
1420     else
1421         i_type = ORDER_NORMAL;
1422
1423     PL_LOCK;
1424     playlist_RecursiveNodeSort( p_playlist, [self currentPlaylistRoot], i_mode, i_type );
1425     PL_UNLOCK;
1426
1427     [self playlistUpdated];
1428
1429     o_tc_sortColumn = o_tc;
1430     [o_outline_view setHighlightedTableColumn:o_tc];
1431
1432     if( b_isSortDescending )
1433         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1434     else
1435         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1436 }
1437
1438
1439 - (void)outlineView:(NSOutlineView *)outlineView
1440                                 willDisplayCell:(id)cell
1441                                 forTableColumn:(NSTableColumn *)tableColumn
1442                                 item:(id)item
1443 {
1444     /* this method can be called when VLC is already dead, hence the extra checks */
1445     intf_thread_t * p_intf = VLCIntf;
1446     if (!p_intf)
1447         return;
1448     playlist_t *p_playlist = pl_Get( p_intf );
1449     if (!p_playlist)
1450         return;
1451
1452     id o_playing_item;
1453
1454     PL_LOCK;
1455     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem( p_playlist )]];
1456     PL_UNLOCK;
1457
1458     if( [self isItem: [o_playing_item pointerValue] inNode:
1459                         [item pointerValue] checkItemExistence: YES]
1460                         || [o_playing_item isEqual: item] )
1461     {
1462         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1463     }
1464     else
1465     {
1466         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1467     }
1468 }
1469
1470 - (id)playingItem
1471 {
1472     playlist_t *p_playlist = pl_Get( VLCIntf );
1473
1474     id o_playing_item;
1475
1476     PL_LOCK;
1477     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem( p_playlist )]];
1478     PL_UNLOCK;
1479
1480     return o_playing_item;
1481 }
1482
1483 - (NSArray *)draggedItems
1484 {
1485     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1486 }
1487
1488 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1489 {
1490     NSTableColumn * o_work_tc;
1491
1492     if (i_state == NSOnState)
1493     {
1494         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1495         [o_work_tc setEditable: NO];
1496         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1497
1498         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1499
1500         if ([o_column isEqualToString: TRACKNUM_COLUMN])
1501         {
1502             [o_work_tc setWidth: 20.];
1503             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1504             [[o_work_tc headerCell] setStringValue: @"#"];
1505         }
1506
1507         [o_outline_view addTableColumn: o_work_tc];
1508         [o_work_tc release];
1509         [o_outline_view reloadData];
1510         [o_outline_view setNeedsDisplay: YES];
1511     }
1512     else
1513         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1514 }
1515
1516 - (void)saveTableColumns
1517 {
1518     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1519     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1520     NSUInteger count = [o_columns count];
1521     NSTableColumn * o_currentColumn;
1522     for (NSUInteger i = 0; i < count; i++)
1523     {
1524         o_currentColumn = [o_columns objectAtIndex: i];
1525         [o_arrayToSave addObject: [NSArray arrayWithObjects: [o_currentColumn identifier], [NSNumber numberWithFloat: [o_currentColumn width]], nil]];
1526     }
1527     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1528     [[NSUserDefaults standardUserDefaults] synchronize];
1529     [o_columns release];
1530     [o_arrayToSave release];
1531 }
1532
1533 @end
1534
1535 @implementation VLCPlaylist (NSOutlineViewDataSource)
1536
1537 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1538 {
1539     id o_value = [super outlineView: outlineView child: index ofItem: item];
1540
1541     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1542     return o_value;
1543 }
1544
1545 /* Required for drag & drop and reordering */
1546 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1547 {
1548     playlist_t *p_playlist = pl_Get( VLCIntf );
1549
1550     /* First remove the items that were moved during the last drag & drop
1551        operation */
1552     [o_items_array removeAllObjects];
1553     [o_nodes_array removeAllObjects];
1554
1555     NSUInteger itemCount = [items count];
1556
1557     for( NSUInteger i = 0 ; i < itemCount ; i++ )
1558     {
1559         id o_item = [items objectAtIndex: i];
1560
1561         /* Fill the items and nodes to move in 2 different arrays */
1562         if( ((playlist_item_t *)[o_item pointerValue])->i_children > 0 )
1563             [o_nodes_array addObject: o_item];
1564         else
1565             [o_items_array addObject: o_item];
1566     }
1567
1568     /* Now we need to check if there are selected items that are in already
1569        selected nodes. In that case, we only want to move the nodes */
1570     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1571     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1572
1573     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1574        a Drop operation coming from the playlist. */
1575
1576     [pboard declareTypes: [NSArray arrayWithObjects:
1577         @"VLCPlaylistItemPboardType", nil] owner: self];
1578     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1579
1580     return YES;
1581 }
1582
1583 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1584 {
1585     playlist_t *p_playlist = pl_Get( VLCIntf );
1586     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1587
1588     if( !p_playlist ) return NSDragOperationNone;
1589
1590     /* Dropping ON items is not allowed if item is not a node */
1591     if( item )
1592     {
1593         if( index == NSOutlineViewDropOnItemIndex &&
1594                 ((playlist_item_t *)[item pointerValue])->i_children == -1 )
1595         {
1596             return NSDragOperationNone;
1597         }
1598     }
1599
1600     /* We refuse to drop an item in anything else than a child of the General
1601        Node. We still accept items that would be root nodes of the outlineview
1602        however, to allow drop in an empty playlist. */
1603     if( !( ([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO] ||
1604         ( var_CreateGetBool( p_playlist, "media-library" ) && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO] ) ) || item == nil ) )
1605     {
1606         return NSDragOperationNone;
1607     }
1608
1609     /* Drop from the Playlist */
1610     if( [[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"] )
1611     {
1612         NSUInteger count = [o_nodes_array count];
1613         for( NSUInteger i = 0 ; i < count ; i++ )
1614         {
1615             /* We refuse to Drop in a child of an item we are moving */
1616             if( [self isItem: [item pointerValue] inNode:
1617                     [[o_nodes_array objectAtIndex: i] pointerValue]
1618                     checkItemExistence: NO] )
1619             {
1620                 return NSDragOperationNone;
1621             }
1622         }
1623         return NSDragOperationMove;
1624     }
1625
1626     /* Drop from the Finder */
1627     else if( [[o_pasteboard types] containsObject: NSFilenamesPboardType] )
1628     {
1629         return NSDragOperationGeneric;
1630     }
1631     return NSDragOperationNone;
1632 }
1633
1634 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1635 {
1636     playlist_t * p_playlist =  pl_Get( VLCIntf );
1637     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1638
1639     /* Drag & Drop inside the playlist */
1640     if( [[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"] )
1641     {
1642         int i_row = 0;
1643         playlist_item_t *p_new_parent, *p_item = NULL;
1644         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1645         /* If the item is to be dropped as root item of the outline, make it a
1646            child of the respective general node, if is either the pl or the ml
1647            Else, choose the proposed parent as parent. */
1648         if( item == nil )
1649         {
1650             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1651                 p_new_parent = [self currentPlaylistRoot];
1652             else
1653                 return NO;
1654         }
1655         else
1656             p_new_parent = [item pointerValue];
1657
1658         /* Make sure the proposed parent is a node.
1659            (This should never be true) */
1660         if( p_new_parent->i_children < 0 )
1661         {
1662             return NO;
1663         }
1664
1665         NSUInteger count = [o_all_items count];
1666         if( count == 0 )
1667             return NO;
1668
1669         playlist_item_t **pp_items = (playlist_item_t **)calloc( count, sizeof( playlist_item_t* ) );
1670         if ( !pp_items )
1671             return NO;
1672
1673         PL_LOCK;
1674         NSUInteger j = 0;
1675         for( NSUInteger i = 0; i < count; i++ )
1676         {
1677             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1678             if( p_item )
1679                 pp_items[j++] = p_item;
1680         }
1681
1682         if( j == 0 || playlist_TreeMoveMany( p_playlist, j, pp_items, p_new_parent, index ) != VLC_SUCCESS )
1683         {
1684             PL_UNLOCK;
1685             free( pp_items );
1686             return NO;
1687         }
1688
1689         PL_UNLOCK;
1690         free( pp_items );
1691
1692         [self playlistUpdated];
1693         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex: 0] pointerValue]]]];
1694
1695         if( i_row == -1 )
1696         {
1697             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1698         }
1699
1700         [o_outline_view deselectAll: self];
1701         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1702         [o_outline_view scrollRowToVisible: i_row];
1703
1704         return YES;
1705     }
1706
1707     else if( [[o_pasteboard types] containsObject: NSFilenamesPboardType] )
1708     {
1709         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1710             return NO;
1711
1712         playlist_item_t *p_node = [item pointerValue];
1713
1714         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1715                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1716         NSUInteger count = [o_values count];
1717         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1718         input_thread_t * p_input = pl_CurrentInput( VLCIntf );
1719         BOOL b_returned = NO;
1720
1721         if (count == 1 && p_input)
1722         {
1723             b_returned = input_AddSubtitle( p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true );
1724             vlc_object_release( p_input );
1725             if(!b_returned)
1726                 return YES;
1727         }
1728         else if( p_input )
1729             vlc_object_release( p_input );
1730
1731         for( NSUInteger i = 0; i < count; i++)
1732         {
1733             NSDictionary *o_dic;
1734             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1735             if( !psz_uri )
1736                 continue;
1737
1738             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1739
1740             free( psz_uri );
1741
1742             [o_array addObject: o_dic];
1743         }
1744
1745         if ( item == nil )
1746         {
1747             [self appendArray:o_array atPos:index enqueue: YES];
1748         }
1749         else
1750         {
1751             assert( p_node->i_children != -1 );
1752             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1753         }
1754         return YES;
1755     }
1756     return NO;
1757 }
1758
1759 @end