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