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