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