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