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