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