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