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