]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: expand the documentation for our single (known) private API use
[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     for (NSUInteger i = 0; i < count; i++)
516     {
517         o_column = [[o_columnArray objectAtIndex:i] objectAtIndex:0];
518         if ([o_column isEqualToString:@"status"])
519             continue;
520
521         [self setColumn: o_column state: NSOnState];
522         [o_menu setPlaylistColumnTableState: NSOnState forColumn: o_column];
523         [[o_outline_view tableColumnWithIdentifier: o_column] setWidth: [[[o_columnArray objectAtIndex:i] objectAtIndex:1] floatValue]];
524     }
525
526     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];
527 }
528
529 - (void)applicationWillTerminate:(NSNotification *)notification
530 {
531     /* 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 */
532     [self saveTableColumns];
533 }
534
535 - (void)searchfieldChanged:(NSNotification *)o_notification
536 {
537     [o_search_field setStringValue:[[o_notification object] stringValue]];
538 }
539
540 - (void)initStrings
541 {
542     [o_mi_save_playlist setTitle: _NS("Save Playlist...")];
543     [o_mi_play setTitle: _NS("Play")];
544     [o_mi_delete setTitle: _NS("Delete")];
545     [o_mi_recursive_expand setTitle: _NS("Expand Node")];
546     [o_mi_selectall setTitle: _NS("Select All")];
547     [o_mi_info setTitle: _NS("Media Information...")];
548     [o_mi_dl_cover_art setTitle: _NS("Download Cover Art")];
549     [o_mi_preparse setTitle: _NS("Fetch Meta Data")];
550     [o_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
551     [o_mm_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
552     [[o_mm_mi_revealInFinder menu] setAutoenablesItems: NO];
553     [o_mi_sort_name setTitle: _NS("Sort Node by Name")];
554     [o_mi_sort_author setTitle: _NS("Sort Node by Author")];
555
556     [o_search_field setToolTip: _NS("Search in Playlist")];
557     [o_search_field_other setToolTip: _NS("Search in Playlist")];
558
559     [o_save_accessory_text setStringValue: _NS("File Format:")];
560     [[o_save_accessory_popup itemAtIndex:0] setTitle: _NS("Extended M3U")];
561     [[o_save_accessory_popup itemAtIndex:1] setTitle: _NS("XML Shareable Playlist Format (XSPF)")];
562     [[o_save_accessory_popup itemAtIndex:2] setTitle: _NS("HTML Playlist")];
563 }
564
565 - (void)playlistUpdated
566 {
567     /* Clear indications of any existing column sorting */
568     NSUInteger count = [[o_outline_view tableColumns] count];
569     for( NSUInteger i = 0 ; i < count ; i++ )
570     {
571         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
572     }
573
574     [o_outline_view setHighlightedTableColumn:nil];
575     o_tc_sortColumn = nil;
576     // TODO Find a way to keep the dict size to a minimum
577     //[o_outline_dict removeAllObjects];
578     [o_outline_view reloadData];
579     [[[[VLCMain sharedInstance] wizard] playlistWizard] reloadOutlineView];
580     [[[[VLCMain sharedInstance] bookmarks] dataTable] reloadData];
581
582     [self outlineViewSelectionDidChange: nil];
583     [[VLCMain sharedInstance] updateMainWindow];
584 }
585
586 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
587 {
588     // FIXME: unsafe
589     playlist_item_t * p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
590
591     if( p_item )
592     {
593         /* update the state of our Reveal-in-Finder menu items */
594         NSMutableString *o_mrl;
595         char *psz_uri = input_item_GetURI( p_item->p_input );
596
597         [o_mi_revealInFinder setEnabled: NO];
598         [o_mm_mi_revealInFinder setEnabled: NO];
599         if( psz_uri )
600         {
601             o_mrl = [NSMutableString stringWithUTF8String: psz_uri];
602
603             /* perform some checks whether it is a file and if it is local at all... */
604             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
605             if( prefix_range.location != NSNotFound )
606                 [o_mrl deleteCharactersInRange: prefix_range];
607
608             if( [o_mrl characterAtIndex:0] == '/' )
609             {
610                 [o_mi_revealInFinder setEnabled: YES];
611                 [o_mm_mi_revealInFinder setEnabled: YES];
612             }
613             free( psz_uri );
614         }
615
616         /* update our info-panel to reflect the new item */
617         [[[VLCMain sharedInstance] info] updatePanelWithItem:p_item->p_input];
618     }
619 }
620
621 - (BOOL)isSelectionEmpty
622 {
623     return [o_outline_view selectedRow] == -1;
624 }
625
626 - (void)updateRowSelection
627 {
628     // FIXME: unsafe
629     playlist_t *p_playlist = pl_Get( VLCIntf );
630     playlist_item_t *p_item, *p_temp_item;
631     NSMutableArray *o_array = [NSMutableArray array];
632
633     PL_LOCK;
634     p_item = playlist_CurrentPlayingItem( p_playlist );
635     if( p_item == NULL )
636     {
637         PL_UNLOCK;
638         return;
639     }
640
641     p_temp_item = p_item;
642     while( p_temp_item->p_parent )
643     {
644         [o_array insertObject: [NSValue valueWithPointer: p_temp_item] atIndex: 0];
645         p_temp_item = p_temp_item->p_parent;
646     }
647     PL_UNLOCK;
648
649     NSUInteger count = [o_array count];
650     for( NSUInteger j = 0; j < count - 1; j++ )
651     {
652         id o_item;
653         if( ( o_item = [o_outline_dict objectForKey:
654                             [NSString stringWithFormat: @"%p",
655                             [[o_array objectAtIndex:j] pointerValue]]] ) != nil )
656         {
657             [o_outline_view expandItem: o_item];
658         }
659     }
660
661     id o_item = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_item]];
662     NSInteger i_index = [o_outline_view rowForItem:o_item];
663     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_index] byExtendingSelection:NO];
664     [o_outline_view setNeedsDisplay:YES];
665 }
666
667 /* Check if p_item is a child of p_node recursively. We need to check the item
668    existence first since OSX sometimes tries to redraw items that have been
669    deleted. We don't do it when not required since this verification takes
670    quite a long time on big playlists (yes, pretty hacky). */
671
672 - (BOOL)isItem: (playlist_item_t *)p_item
673                     inNode: (playlist_item_t *)p_node
674                     checkItemExistence:(BOOL)b_check
675                     locked:(BOOL)b_locked
676
677 {
678     playlist_t * p_playlist = pl_Get( VLCIntf );
679     playlist_item_t *p_temp_item = p_item;
680
681     if( p_node == p_item )
682         return YES;
683
684     if( p_node->i_children < 1)
685         return NO;
686
687     if ( p_temp_item )
688     {
689         int i;
690         if(!b_locked) PL_LOCK;
691
692         if( b_check )
693         {
694         /* Since outlineView: willDisplayCell:... may call this function with
695            p_items that don't exist anymore, first check if the item is still
696            in the playlist. Any cleaner solution welcomed. */
697             for( i = 0; i < p_playlist->all_items.i_size; i++ )
698             {
699                 if( ARRAY_VAL( p_playlist->all_items, i) == p_item ) break;
700                 else if ( i == p_playlist->all_items.i_size - 1 )
701                 {
702                     if(!b_locked) PL_UNLOCK;
703                     return NO;
704                 }
705             }
706         }
707
708         while( p_temp_item )
709         {
710             p_temp_item = p_temp_item->p_parent;
711             if( p_temp_item == p_node )
712             {
713                 if(!b_locked) PL_UNLOCK;
714                 return YES;
715             }
716         }
717         if(!b_locked) PL_UNLOCK;
718     }
719     return NO;
720 }
721
722 - (BOOL)isItem: (playlist_item_t *)p_item
723                     inNode: (playlist_item_t *)p_node
724                     checkItemExistence:(BOOL)b_check
725 {
726     return [self isItem:p_item inNode:p_node checkItemExistence:b_check locked:NO];
727 }
728
729 /* This method is useful for instance to remove the selected children of an
730    already selected node */
731 - (void)removeItemsFrom:(id)o_items ifChildrenOf:(id)o_nodes
732 {
733     NSUInteger itemCount = [o_items count];
734     NSUInteger nodeCount = [o_nodes count];
735     for( NSUInteger i = 0 ; i < itemCount ; i++ )
736     {
737         for ( NSUInteger j = 0 ; j < nodeCount ; j++ )
738         {
739             if( o_items == o_nodes)
740             {
741                 if( j == i ) continue;
742             }
743             if( [self isItem: [[o_items objectAtIndex:i] pointerValue]
744                     inNode: [[o_nodes objectAtIndex:j] pointerValue]
745                     checkItemExistence: NO locked:NO] )
746             {
747                 [o_items removeObjectAtIndex:i];
748                 /* We need to execute the next iteration with the same index
749                    since the current item has been deleted */
750                 i--;
751                 break;
752             }
753         }
754     }
755 }
756
757 - (IBAction)savePlaylist:(id)sender
758 {
759     playlist_t * p_playlist = pl_Get( VLCIntf );
760
761     NSSavePanel *o_save_panel = [NSSavePanel savePanel];
762     NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
763
764     [o_save_panel setTitle: _NS("Save Playlist")];
765     [o_save_panel setPrompt: _NS("Save")];
766     [o_save_panel setAccessoryView: o_save_accessory_view];
767
768     if( [o_save_panel runModalForDirectory: nil
769             file: o_name] == NSOKButton )
770     {
771         NSString *o_filename = [[o_save_panel URL] path];
772
773         if( [o_save_accessory_popup indexOfSelectedItem] == 0 )
774         {
775             NSString * o_real_filename;
776             NSRange range;
777             range.location = [o_filename length] - [@".m3u" length];
778             range.length = [@".m3u" length];
779
780             if( [o_filename compare:@".m3u" options: NSCaseInsensitiveSearch
781                                              range: range] != NSOrderedSame )
782             {
783                 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
784             }
785             else
786             {
787                 o_real_filename = o_filename;
788             }
789             playlist_Export( p_playlist,
790                 [o_real_filename fileSystemRepresentation],
791                 p_playlist->p_local_category, "export-m3u" );
792         }
793         else if( [o_save_accessory_popup indexOfSelectedItem] == 1 )
794         {
795             NSString * o_real_filename;
796             NSRange range;
797             range.location = [o_filename length] - [@".xspf" length];
798             range.length = [@".xspf" length];
799
800             if( [o_filename compare:@".xspf" options: NSCaseInsensitiveSearch
801                                              range: range] != NSOrderedSame )
802             {
803                 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
804             }
805             else
806             {
807                 o_real_filename = o_filename;
808             }
809             playlist_Export( p_playlist,
810                 [o_real_filename fileSystemRepresentation],
811                 p_playlist->p_local_category, "export-xspf" );
812         }
813         else
814         {
815             NSString * o_real_filename;
816             NSRange range;
817             range.location = [o_filename length] - [@".html" length];
818             range.length = [@".html" length];
819
820             if( [o_filename compare:@".html" options: NSCaseInsensitiveSearch
821                                              range: range] != NSOrderedSame )
822             {
823                 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
824             }
825             else
826             {
827                 o_real_filename = o_filename;
828             }
829             playlist_Export( p_playlist,
830                 [o_real_filename fileSystemRepresentation],
831                 p_playlist->p_local_category, "export-html" );
832         }
833     }
834 }
835
836 /* When called retrieves the selected outlineview row and plays that node or item */
837 - (IBAction)playItem:(id)sender
838 {
839     intf_thread_t * p_intf = VLCIntf;
840     playlist_t * p_playlist = pl_Get( p_intf );
841
842     playlist_item_t *p_item;
843     playlist_item_t *p_node = NULL;
844
845     // ignore clicks on column header when handling double action
846     if( sender != nil && [o_outline_view clickedRow] == -1 && sender != o_mi_play)
847         return;
848
849     p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
850
851     PL_LOCK;
852     if( p_item )
853     {
854         if( p_item->i_children == -1 )
855         {
856             p_node = p_item->p_parent;
857         }
858         else
859         {
860             p_node = p_item;
861             if( p_node->i_children > 0 && p_node->pp_children[0]->i_children == -1 )
862             {
863                 p_item = p_node->pp_children[0];
864             }
865             else
866             {
867                 p_item = NULL;
868             }
869         }
870         playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item );
871     }
872     PL_UNLOCK;
873 }
874
875 - (IBAction)revealItemInFinder:(id)sender
876 {
877     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
878     NSUInteger count = [selectedRows count];
879     NSUInteger indexes[count];
880     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
881
882     NSMutableString * o_mrl;
883     playlist_item_t *p_item;
884     for (NSUInteger i = 0; i < count; i++) {
885         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
886
887         if(! p_item || !p_item->p_input )
888             continue;
889
890         o_mrl = [[NSMutableString alloc] initWithFormat: @"%s", decode_URI( input_item_GetURI( p_item->p_input ))];
891
892         /* perform some checks whether it is a file and if it is local at all... */
893         if ([o_mrl length] > 0)
894         {
895             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
896             if( prefix_range.location != NSNotFound )
897                 [o_mrl deleteCharactersInRange: prefix_range];
898
899             if( [o_mrl characterAtIndex:0] == '/' )
900                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
901         }
902
903         [o_mrl release];
904     }
905 }
906
907 /* When called retrieves the selected outlineview row and plays that node or item */
908 - (IBAction)preparseItem:(id)sender
909 {
910     int i_count;
911     NSIndexSet *o_selected_indexes;
912     intf_thread_t * p_intf = VLCIntf;
913     playlist_t * p_playlist = pl_Get( p_intf );
914     playlist_item_t *p_item = NULL;
915
916     o_selected_indexes = [o_outline_view selectedRowIndexes];
917     i_count = [o_selected_indexes count];
918
919     NSUInteger indexes[i_count];
920     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
921     for (int i = 0; i < i_count; i++)
922     {
923         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
924         [o_outline_view deselectRow: indexes[i]];
925
926         if( p_item )
927         {
928             if( p_item->i_children == -1 )
929                 playlist_PreparseEnqueue( p_playlist, p_item->p_input );
930             else
931                 msg_Dbg( p_intf, "preparsing nodes not implemented" );
932         }
933     }
934     [self playlistUpdated];
935 }
936
937 - (IBAction)downloadCoverArt:(id)sender
938 {
939     int i_count;
940     NSIndexSet *o_selected_indexes;
941     intf_thread_t * p_intf = VLCIntf;
942     playlist_t * p_playlist = pl_Get( p_intf );
943     playlist_item_t *p_item = NULL;
944
945     o_selected_indexes = [o_outline_view selectedRowIndexes];
946     i_count = [o_selected_indexes count];
947
948     NSUInteger indexes[i_count];
949     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
950     for (int i = 0; i < i_count; i++)
951     {
952         p_item = [[o_outline_view itemAtRow: indexes[i]] pointerValue];
953
954         if( p_item && p_item->i_children == -1 )
955             playlist_AskForArtEnqueue( p_playlist, p_item->p_input );
956     }
957     [self playlistUpdated];
958 }
959
960 - (IBAction)selectAll:(id)sender
961 {
962     [o_outline_view selectAll: nil];
963 }
964
965 - (IBAction)deleteItem:(id)sender
966 {
967     int i_count;
968     NSIndexSet *o_selected_indexes;
969     playlist_t * p_playlist;
970     intf_thread_t * p_intf = VLCIntf;
971
972     o_selected_indexes = [o_outline_view selectedRowIndexes];
973     i_count = [o_selected_indexes count];
974
975     p_playlist = pl_Get( p_intf );
976
977     NSUInteger indexes[i_count];
978     if (i_count == [o_outline_view numberOfRows])
979     {
980 #ifndef NDEBUG
981         msg_Dbg( p_intf, "user selected entire list, deleting current playlist root instead of individual items" );
982 #endif
983         PL_LOCK;
984         playlist_NodeDelete( p_playlist, [self currentPlaylistRoot], true, false );
985         PL_UNLOCK;
986         [self playlistUpdated];
987         return;
988     }
989     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
990     for (int i = 0; i < i_count; i++)
991     {
992         id o_item = [o_outline_view itemAtRow: indexes[i]];
993         [o_outline_view deselectRow: indexes[i]];
994
995         PL_LOCK;
996         playlist_item_t *p_item = [o_item pointerValue];
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     }
1043     else
1044     /*If no item is selected, sort the whole playlist*/
1045     {
1046         p_item = [self currentPlaylistRoot];
1047     }
1048
1049     PL_LOCK;
1050     if( p_item->i_children > -1 ) // the item is a node
1051     {
1052         playlist_RecursiveNodeSort( p_playlist, p_item, i_mode, ORDER_NORMAL );
1053     }
1054     else
1055     {
1056         playlist_RecursiveNodeSort( p_playlist,
1057                 p_item->p_parent, i_mode, ORDER_NORMAL );
1058     }
1059     PL_UNLOCK;
1060     [self playlistUpdated];
1061 }
1062
1063 - (input_item_t *)createItem:(NSDictionary *)o_one_item
1064 {
1065     intf_thread_t * p_intf = VLCIntf;
1066     playlist_t * p_playlist = pl_Get( p_intf );
1067
1068     input_item_t *p_input;
1069     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
1070     NSString *o_uri, *o_name, *o_path;
1071     NSURL * o_nsurl;
1072     NSArray *o_options;
1073     NSURL *o_true_file;
1074
1075     /* Get the item */
1076     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
1077     o_nsurl = [NSURL URLWithString: o_uri];
1078     o_path = [o_nsurl path];
1079     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
1080     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
1081
1082     if( [[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
1083         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
1084                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL] )
1085     {
1086
1087         id o_vlc_open = [[VLCMain sharedInstance] open];
1088
1089         char *diskType = [o_vlc_open getVolumeTypeFromMountPath: o_path];
1090         msg_Dbg( p_intf, "detected optical media of type '%s' in the file input", diskType );
1091
1092         if (diskType == kVLCMediaDVD)
1093         {
1094             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1095         }
1096         else if (diskType == kVLCMediaVideoTSFolder)
1097         {
1098             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
1099         }
1100         else if (diskType == kVLCMediaAudioCD)
1101         {
1102             o_uri = [NSString stringWithFormat: @"cdda://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1103         }
1104         else if (diskType == kVLCMediaVCD)
1105         {
1106             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1107         }
1108         else if (diskType == kVLCMediaSVCD)
1109         {
1110             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1111         }
1112         else if (diskType == kVLCMediaBD || diskType == kVLCMediaBDMVFolder)
1113         {
1114             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
1115         }
1116         else
1117         {
1118             msg_Warn( VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String] );
1119         }
1120
1121         p_input = input_item_New( [o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String] );
1122     }
1123     else
1124         p_input = input_item_New( [o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL );
1125
1126     if( !p_input )
1127         return NULL;
1128
1129     if( o_options )
1130     {
1131         NSUInteger count = [o_options count];
1132         for( NSUInteger i = 0; i < count; i++ )
1133         {
1134             input_item_AddOption( p_input, [[o_options objectAtIndex:i] UTF8String],
1135                                   VLC_INPUT_OPTION_TRUSTED );
1136         }
1137     }
1138
1139     /* Recent documents menu */
1140     if( o_nsurl != nil && (BOOL)config_GetInt( p_playlist, "macosx-recentitems" ) == YES )
1141     {
1142         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
1143     }
1144     return p_input;
1145 }
1146
1147 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
1148 {
1149     playlist_t * p_playlist = pl_Get( VLCIntf );
1150     NSUInteger count = [o_array count];
1151     BOOL b_usingPlaylist;
1152     if ([self currentPlaylistRoot] == p_playlist->p_ml_category)
1153         b_usingPlaylist = NO;
1154     else
1155         b_usingPlaylist = YES;
1156
1157     PL_LOCK;
1158     for( NSUInteger i_item = 0; i_item < count; i_item++ )
1159     {
1160         input_item_t *p_input;
1161         NSDictionary *o_one_item;
1162
1163         /* Get the item */
1164         o_one_item = [o_array objectAtIndex: i_item];
1165         p_input = [self createItem: o_one_item];
1166         if( !p_input )
1167         {
1168             continue;
1169         }
1170
1171         /* Add the item */
1172         /* FIXME: playlist_AddInput() can fail */
1173
1174         playlist_AddInput( p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist,
1175          pl_Locked );
1176
1177         if( i_item == 0 && !b_enqueue )
1178         {
1179             playlist_item_t *p_item = playlist_ItemGetByInput( p_playlist, p_input );
1180             playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item );
1181         }
1182
1183         vlc_gc_decref( p_input );
1184     }
1185     PL_UNLOCK;
1186     [self playlistUpdated];
1187 }
1188
1189 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
1190 {
1191     playlist_t * p_playlist = pl_Get( VLCIntf );
1192     NSUInteger count = [o_array count];
1193
1194     for( NSUInteger i_item = 0; i_item < count; i_item++ )
1195     {
1196         input_item_t *p_input;
1197         NSDictionary *o_one_item;
1198
1199         /* Get the item */
1200         o_one_item = [o_array objectAtIndex: i_item];
1201         p_input = [self createItem: o_one_item];
1202
1203         if( !p_input ) continue;
1204
1205         /* Add the item */
1206         PL_LOCK;
1207         playlist_NodeAddInput( p_playlist, p_input, p_node,
1208                                       PLAYLIST_INSERT,
1209                                       i_position == -1 ?
1210                                       PLAYLIST_END : i_position + i_item,
1211                                       pl_Locked );
1212
1213
1214         if( i_item == 0 && !b_enqueue )
1215         {
1216             playlist_item_t *p_item;
1217             p_item = playlist_ItemGetByInput( p_playlist, p_input );
1218             playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item );
1219         }
1220         PL_UNLOCK;
1221         vlc_gc_decref( p_input );
1222     }
1223     [self playlistUpdated];
1224 }
1225
1226 - (NSMutableArray *)subSearchItem:(playlist_item_t *)p_item
1227 {
1228     playlist_t *p_playlist = pl_Get( VLCIntf );
1229     playlist_item_t *p_selected_item;
1230     int i_selected_row;
1231
1232     i_selected_row = [o_outline_view selectedRow];
1233     if (i_selected_row < 0)
1234         i_selected_row = 0;
1235
1236     p_selected_item = (playlist_item_t *)[[o_outline_view itemAtRow: i_selected_row] pointerValue];
1237
1238     for( NSUInteger i_current = 0; i_current < p_item->i_children ; i_current++ )
1239     {
1240         char *psz_temp;
1241         NSString *o_current_name, *o_current_author;
1242
1243         PL_LOCK;
1244         o_current_name = [NSString stringWithUTF8String:
1245             p_item->pp_children[i_current]->p_input->psz_name];
1246         psz_temp = input_item_GetInfo( p_item->p_input ,
1247                    _("Meta-information"),_("Artist") );
1248         o_current_author = [NSString stringWithUTF8String: psz_temp];
1249         free( psz_temp);
1250         PL_UNLOCK;
1251
1252         if( p_selected_item == p_item->pp_children[i_current] &&
1253                     b_selected_item_met == NO )
1254         {
1255             b_selected_item_met = YES;
1256         }
1257         else if( p_selected_item == p_item->pp_children[i_current] &&
1258                     b_selected_item_met == YES )
1259         {
1260             return NULL;
1261         }
1262         else if( b_selected_item_met == YES &&
1263                     ( [o_current_name rangeOfString:[o_search_field
1264                         stringValue] options:NSCaseInsensitiveSearch].length ||
1265                       [o_current_author rangeOfString:[o_search_field
1266                         stringValue] options:NSCaseInsensitiveSearch].length ) )
1267         {
1268             /*Adds the parent items in the result array as well, so that we can
1269             expand the tree*/
1270             return [NSMutableArray arrayWithObject: [NSValue
1271                             valueWithPointer: p_item->pp_children[i_current]]];
1272         }
1273         if( p_item->pp_children[i_current]->i_children > 0 )
1274         {
1275             id o_result = [self subSearchItem:
1276                                             p_item->pp_children[i_current]];
1277             if( o_result != NULL )
1278             {
1279                 [o_result insertObject: [NSValue valueWithPointer:
1280                                 p_item->pp_children[i_current]] atIndex:0];
1281                 return o_result;
1282             }
1283         }
1284     }
1285     return NULL;
1286 }
1287
1288 - (IBAction)searchItem:(id)sender
1289 {
1290     playlist_t * p_playlist = pl_Get( VLCIntf );
1291     id o_result;
1292
1293     int i_row = -1;
1294
1295     b_selected_item_met = NO;
1296
1297         /*First, only search after the selected item:*
1298          *(b_selected_item_met = NO)                 */
1299     o_result = [self subSearchItem:[self currentPlaylistRoot]];
1300     if( o_result == NULL )
1301     {
1302         /* If the first search failed, search again from the beginning */
1303         o_result = [self subSearchItem:[self currentPlaylistRoot]];
1304     }
1305     if( o_result != NULL )
1306     {
1307         int i_start;
1308         if( [[o_result objectAtIndex: 0] pointerValue] == p_playlist->p_local_category )
1309             i_start = 1;
1310         else
1311             i_start = 0;
1312         NSUInteger count = [o_result count];
1313
1314         for( NSUInteger i = i_start ; i < count - 1 ; i++ )
1315         {
1316             [o_outline_view expandItem: [o_outline_dict objectForKey:
1317                         [NSString stringWithFormat: @"%p",
1318                         [[o_result objectAtIndex: i] pointerValue]]]];
1319         }
1320         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1321                         [NSString stringWithFormat: @"%p",
1322                         [[o_result objectAtIndex: count - 1 ]
1323                         pointerValue]]]];
1324     }
1325     if( i_row > -1 )
1326     {
1327         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1328         [o_outline_view scrollRowToVisible: i_row];
1329     }
1330 }
1331
1332 - (IBAction)recursiveExpandNode:(id)sender
1333 {
1334     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1335     NSUInteger count = [selectedRows count];
1336     NSUInteger indexes[count];
1337     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1338
1339     id o_item;
1340     playlist_item_t *p_item;
1341     for (NSUInteger i = 0; i < count; i++) {
1342         o_item = [o_outline_view itemAtRow: indexes[i]];
1343         p_item = (playlist_item_t *)[o_item pointerValue];
1344
1345         if( ![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item] )
1346             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1347
1348         /* We need to collapse the node first, since OSX refuses to recursively
1349          expand an already expanded node, even if children nodes are collapsed. */
1350         [o_outline_view collapseItem: o_item collapseChildren: YES];
1351         [o_outline_view expandItem: o_item expandChildren: YES];
1352
1353         selectedRows = [o_outline_view selectedRowIndexes];
1354         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1355     }
1356 }
1357
1358 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1359 {
1360     NSPoint pt;
1361     bool b_rows;
1362     bool b_item_sel;
1363
1364     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1365     int row = [o_outline_view rowAtPoint:pt];
1366     if( row != -1 && [o_outline_view selectedRow] == -1)
1367         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1368
1369     b_item_sel = ( row != -1 && [o_outline_view selectedRow] != -1 );
1370     b_rows = [o_outline_view numberOfRows] != 0;
1371
1372     [o_mi_play setEnabled: b_item_sel];
1373     [o_mi_delete setEnabled: b_item_sel];
1374     [o_mi_selectall setEnabled: b_rows];
1375     [o_mi_info setEnabled: b_item_sel];
1376     [o_mi_preparse setEnabled: b_item_sel];
1377     [o_mi_recursive_expand setEnabled: b_item_sel];
1378     [o_mi_sort_name setEnabled: b_item_sel];
1379     [o_mi_sort_author setEnabled: b_item_sel];
1380
1381     return( o_ctx_menu );
1382 }
1383
1384 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1385 {
1386     int i_mode, i_type = 0;
1387     intf_thread_t *p_intf = VLCIntf;
1388     NSString * o_identifier = [o_tc identifier];
1389
1390     playlist_t *p_playlist = pl_Get( p_intf );
1391
1392     if( [o_identifier isEqualToString:TRACKNUM_COLUMN] )
1393         i_mode = SORT_TRACK_NUMBER;
1394     else if( [o_identifier isEqualToString:TITLE_COLUMN] )
1395         i_mode = SORT_TITLE;
1396     else if( [o_identifier isEqualToString:ARTIST_COLUMN] )
1397         i_mode = SORT_ARTIST;
1398     else if( [o_identifier isEqualToString:GENRE_COLUMN] )
1399         i_mode = SORT_GENRE;
1400     else if( [o_identifier isEqualToString:DURATION_COLUMN] )
1401         i_mode = SORT_DURATION;
1402     else if( [o_identifier isEqualToString:ALBUM_COLUMN] )
1403         i_mode = SORT_ALBUM;
1404     else if( [o_identifier isEqualToString:DESCRIPTION_COLUMN] )
1405         i_mode = SORT_DESCRIPTION;
1406     else if( [o_identifier isEqualToString:URI_COLUMN] )
1407         i_mode = SORT_URI;
1408     else
1409         return;
1410
1411     if( o_tc_sortColumn == o_tc )
1412         b_isSortDescending = !b_isSortDescending;
1413     else
1414         b_isSortDescending = false;
1415
1416     if( b_isSortDescending )
1417         i_type = ORDER_REVERSE;
1418     else
1419         i_type = ORDER_NORMAL;
1420
1421     PL_LOCK;
1422     playlist_RecursiveNodeSort( p_playlist, [self currentPlaylistRoot], i_mode, i_type );
1423     PL_UNLOCK;
1424
1425     [self playlistUpdated];
1426
1427     o_tc_sortColumn = o_tc;
1428     [o_outline_view setHighlightedTableColumn:o_tc];
1429
1430     if( b_isSortDescending )
1431         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1432     else
1433         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1434 }
1435
1436
1437 - (void)outlineView:(NSOutlineView *)outlineView
1438                                 willDisplayCell:(id)cell
1439                                 forTableColumn:(NSTableColumn *)tableColumn
1440                                 item:(id)item
1441 {
1442     /* this method can be called when VLC is already dead, hence the extra checks */
1443     intf_thread_t * p_intf = VLCIntf;
1444     if (!p_intf)
1445         return;
1446     playlist_t *p_playlist = pl_Get( p_intf );
1447     if (!p_playlist)
1448         return;
1449
1450     id o_playing_item;
1451
1452     PL_LOCK;
1453     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem( p_playlist )]];
1454     PL_UNLOCK;
1455
1456     if( [self isItem: [o_playing_item pointerValue] inNode:
1457                         [item pointerValue] checkItemExistence: YES]
1458                         || [o_playing_item isEqual: item] )
1459     {
1460         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1461     }
1462     else
1463     {
1464         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1465     }
1466 }
1467
1468 - (id)playingItem
1469 {
1470     playlist_t *p_playlist = pl_Get( VLCIntf );
1471
1472     id o_playing_item;
1473
1474     PL_LOCK;
1475     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem( p_playlist )]];
1476     PL_UNLOCK;
1477
1478     return o_playing_item;
1479 }
1480
1481 - (NSArray *)draggedItems
1482 {
1483     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1484 }
1485
1486 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state
1487 {
1488     NSTableColumn * o_work_tc;
1489
1490     if (i_state == NSOnState)
1491     {
1492         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1493         [o_work_tc setEditable: NO];
1494         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1495
1496         /* we cannot use a makro here, because gettext isn't clever enough for that */
1497         if ([o_column isEqualToString: TRACKNUM_COLUMN])
1498         {
1499             [o_work_tc setMaxWidth: 20.];
1500             [[o_work_tc headerCell] setStringValue: @"#"];
1501         }
1502         else if ([o_column isEqualToString: TITLE_COLUMN])
1503         {
1504             [[o_work_tc headerCell] setStringValue: _NS("Name")];
1505             [o_work_tc setResizingMask: NSTableColumnAutoresizingMask | NSTableColumnUserResizingMask];
1506         }
1507         else if ([o_column isEqualToString: ARTIST_COLUMN])
1508             [[o_work_tc headerCell] setStringValue: _NS("Author")];
1509         else if ([o_column isEqualToString: DURATION_COLUMN])
1510             [[o_work_tc headerCell] setStringValue: _NS("Duration")];
1511         else if ([o_column isEqualToString: GENRE_COLUMN])
1512             [[o_work_tc headerCell] setStringValue: _NS("Genre")];
1513         else if ([o_column isEqualToString: ALBUM_COLUMN])
1514             [[o_work_tc headerCell] setStringValue: _NS("Album")];
1515         else if ([o_column isEqualToString: DESCRIPTION_COLUMN])
1516             [[o_work_tc headerCell] setStringValue: _NS("Description")];
1517         else if ([o_column isEqualToString: DATE_COLUMN])
1518             [[o_work_tc headerCell] setStringValue: _NS("Date")];
1519         else if ([o_column isEqualToString: LANGUAGE_COLUMN])
1520             [[o_work_tc headerCell] setStringValue: _NS("Language")];
1521         else if ([o_column isEqualToString: URI_COLUMN])
1522             [[o_work_tc headerCell] setStringValue: _NS("URI")];
1523
1524         [o_outline_view addTableColumn: o_work_tc];
1525         [o_work_tc release];
1526         [o_outline_view reloadData];
1527         [o_outline_view setNeedsDisplay: YES];
1528     }
1529     else
1530         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1531 }
1532
1533 - (void)saveTableColumns
1534 {
1535     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1536     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1537     NSUInteger count = [o_columns count];
1538     NSTableColumn * o_currentColumn;
1539     for (NSUInteger i = 0; i < count; i++)
1540     {
1541         o_currentColumn = [o_columns objectAtIndex: i];
1542         [o_arrayToSave addObject: [NSArray arrayWithObjects: [o_currentColumn identifier], [NSNumber numberWithFloat: [o_currentColumn width]], nil]];
1543     }
1544     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1545     [[NSUserDefaults standardUserDefaults] synchronize];
1546     [o_columns release];
1547     [o_arrayToSave release];
1548 }
1549
1550 @end
1551
1552 @implementation VLCPlaylist (NSOutlineViewDataSource)
1553
1554 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1555 {
1556     id o_value = [super outlineView: outlineView child: index ofItem: item];
1557
1558     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1559     return o_value;
1560 }
1561
1562 /* Required for drag & drop and reordering */
1563 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1564 {
1565     playlist_t *p_playlist = pl_Get( VLCIntf );
1566
1567     /* First remove the items that were moved during the last drag & drop
1568        operation */
1569     [o_items_array removeAllObjects];
1570     [o_nodes_array removeAllObjects];
1571
1572     NSUInteger itemCount = [items count];
1573
1574     for( NSUInteger i = 0 ; i < itemCount ; i++ )
1575     {
1576         id o_item = [items objectAtIndex: i];
1577
1578         /* Fill the items and nodes to move in 2 different arrays */
1579         if( ((playlist_item_t *)[o_item pointerValue])->i_children > 0 )
1580             [o_nodes_array addObject: o_item];
1581         else
1582             [o_items_array addObject: o_item];
1583     }
1584
1585     /* Now we need to check if there are selected items that are in already
1586        selected nodes. In that case, we only want to move the nodes */
1587     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1588     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1589
1590     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1591        a Drop operation coming from the playlist. */
1592
1593     [pboard declareTypes: [NSArray arrayWithObjects:
1594         @"VLCPlaylistItemPboardType", nil] owner: self];
1595     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1596
1597     return YES;
1598 }
1599
1600 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1601 {
1602     playlist_t *p_playlist = pl_Get( VLCIntf );
1603     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1604
1605     if( !p_playlist ) return NSDragOperationNone;
1606
1607     /* Dropping ON items is not allowed if item is not a node */
1608     if( item )
1609     {
1610         if( index == NSOutlineViewDropOnItemIndex &&
1611                 ((playlist_item_t *)[item pointerValue])->i_children == -1 )
1612         {
1613             return NSDragOperationNone;
1614         }
1615     }
1616
1617     /* We refuse to drop an item in anything else than a child of the General
1618        Node. We still accept items that would be root nodes of the outlineview
1619        however, to allow drop in an empty playlist. */
1620     if( !( ([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO] ||
1621         ( var_CreateGetBool( p_playlist, "media-library" ) && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO] ) ) || item == nil ) )
1622     {
1623         return NSDragOperationNone;
1624     }
1625
1626     /* Drop from the Playlist */
1627     if( [[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"] )
1628     {
1629         NSUInteger count = [o_nodes_array count];
1630         for( NSUInteger i = 0 ; i < count ; i++ )
1631         {
1632             /* We refuse to Drop in a child of an item we are moving */
1633             if( [self isItem: [item pointerValue] inNode:
1634                     [[o_nodes_array objectAtIndex: i] pointerValue]
1635                     checkItemExistence: NO] )
1636             {
1637                 return NSDragOperationNone;
1638             }
1639         }
1640         return NSDragOperationMove;
1641     }
1642
1643     /* Drop from the Finder */
1644     else if( [[o_pasteboard types] containsObject: NSFilenamesPboardType] )
1645     {
1646         return NSDragOperationGeneric;
1647     }
1648     return NSDragOperationNone;
1649 }
1650
1651 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1652 {
1653     playlist_t * p_playlist =  pl_Get( VLCIntf );
1654     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1655
1656     /* Drag & Drop inside the playlist */
1657     if( [[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"] )
1658     {
1659         int i_row, i_removed_from_node = 0;
1660         playlist_item_t *p_new_parent, *p_item = NULL;
1661         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1662         /* If the item is to be dropped as root item of the outline, make it a
1663            child of the respective general node, if is either the pl or the ml
1664            Else, choose the proposed parent as parent. */
1665         if( item == nil )
1666         {
1667             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1668                 p_new_parent = [self currentPlaylistRoot];
1669             else
1670                 return NO;
1671         }
1672         else
1673             p_new_parent = [item pointerValue];
1674
1675         /* Make sure the proposed parent is a node.
1676            (This should never be true) */
1677         if( p_new_parent->i_children < 0 )
1678         {
1679             return NO;
1680         }
1681
1682         NSUInteger count = [o_all_items count];
1683         for( NSUInteger i = 0; i < count; i++ )
1684         {
1685             playlist_item_t *p_old_parent = NULL;
1686             int i_old_index = 0;
1687
1688             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1689             p_old_parent = p_item->p_parent;
1690             if( !p_old_parent )
1691             continue;
1692             /* We may need the old index later */
1693             if( p_new_parent == p_old_parent )
1694             {
1695                 for( NSInteger j = 0; j < p_old_parent->i_children; j++ )
1696                 {
1697                     if( p_old_parent->pp_children[j] == p_item )
1698                     {
1699                         i_old_index = j;
1700                         break;
1701                     }
1702                 }
1703             }
1704
1705             PL_LOCK;
1706             // Actually detach the item from the old position
1707             if( playlist_NodeRemoveItem( p_playlist, p_item, p_old_parent ) ==
1708                 VLC_SUCCESS )
1709             {
1710                 int i_new_index;
1711                 /* Calculate the new index */
1712                 if( index == -1 )
1713                 i_new_index = -1;
1714                 /* If we move the item in the same node, we need to take into
1715                    account that one item will be deleted */
1716                 else
1717                 {
1718                     if ((p_new_parent == p_old_parent && i_old_index < index + (int)i) )
1719                     {
1720                         i_removed_from_node++;
1721                     }
1722                     i_new_index = index + i - i_removed_from_node;
1723                 }
1724                 // Reattach the item to the new position
1725                 playlist_NodeInsert( p_playlist, p_item, p_new_parent, i_new_index );
1726             }
1727             PL_UNLOCK;
1728         }
1729         [self playlistUpdated];
1730         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex: 0] pointerValue]]]];
1731
1732         if( i_row == -1 )
1733         {
1734             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1735         }
1736
1737         [o_outline_view deselectAll: self];
1738         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1739         [o_outline_view scrollRowToVisible: i_row];
1740
1741         return YES;
1742     }
1743
1744     else if( [[o_pasteboard types] containsObject: NSFilenamesPboardType] )
1745     {
1746         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1747             return NO;
1748
1749         playlist_item_t *p_node = [item pointerValue];
1750
1751         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1752                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1753         NSUInteger count = [o_values count];
1754         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1755         input_thread_t * p_input = pl_CurrentInput( VLCIntf );
1756         BOOL b_returned = NO;
1757
1758         if (count == 1 && p_input)
1759         {
1760             b_returned = input_AddSubtitle( p_input, make_URI([[o_values objectAtIndex:0] UTF8String], NULL), true );
1761             vlc_object_release( p_input );
1762             if(!b_returned)
1763                 return YES;
1764         }
1765         else if( p_input )
1766             vlc_object_release( p_input );
1767
1768         for( NSUInteger i = 0; i < count; i++)
1769         {
1770             NSDictionary *o_dic;
1771             char *psz_uri = make_URI([[o_values objectAtIndex:i] UTF8String], NULL);
1772             if( !psz_uri )
1773                 continue;
1774
1775             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1776
1777             free( psz_uri );
1778
1779             [o_array addObject: o_dic];
1780         }
1781
1782         if ( item == nil )
1783         {
1784             [self appendArray:o_array atPos:index enqueue: YES];
1785         }
1786         else
1787         {
1788             assert( p_node->i_children != -1 );
1789             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1790         }
1791         return YES;
1792     }
1793     return NO;
1794 }
1795
1796 @end