]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: use modern ObjC syntax for C-String to NSString conversations
[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 objectAtIndex:i] objectAtIndex: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 objectAtIndex:i] objectAtIndex: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] objectAtIndex: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 objectAtIndex: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 objectAtIndex:i] pointerValue]
675                     inNode: [[o_nodes objectAtIndex: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 objectAtIndex: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 objectAtIndex: 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 objectAtIndex: 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 objectAtIndex: 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 objectAtIndex: i] pointerValue]]]];
1174         }
1175         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1176                         [NSString stringWithFormat: @"%p",
1177                         [[o_result objectAtIndex: count - 1 ]
1178                         pointerValue]]]];
1179     }
1180     if (i_row > -1) {
1181         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1182         [o_outline_view scrollRowToVisible: i_row];
1183     }
1184 }
1185
1186 - (IBAction)recursiveExpandNode:(id)sender
1187 {
1188     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1189     NSUInteger count = [selectedRows count];
1190     NSUInteger indexes[count];
1191     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1192
1193     id o_item;
1194     playlist_item_t *p_item;
1195     for (NSUInteger i = 0; i < count; i++) {
1196         o_item = [o_outline_view itemAtRow: indexes[i]];
1197         p_item = (playlist_item_t *)[o_item pointerValue];
1198
1199         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
1200             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1201
1202         /* We need to collapse the node first, since OSX refuses to recursively
1203          expand an already expanded node, even if children nodes are collapsed. */
1204         [o_outline_view collapseItem: o_item collapseChildren: YES];
1205         [o_outline_view expandItem: o_item expandChildren: YES];
1206
1207         selectedRows = [o_outline_view selectedRowIndexes];
1208         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1209     }
1210 }
1211
1212 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1213 {
1214     NSPoint pt;
1215     bool b_rows;
1216     bool b_item_sel;
1217
1218     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1219     int row = [o_outline_view rowAtPoint:pt];
1220     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
1221         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1222
1223     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
1224     b_rows = [o_outline_view numberOfRows] != 0;
1225
1226     [o_mi_play setEnabled: b_item_sel];
1227     [o_mi_delete setEnabled: b_item_sel];
1228     [o_mi_selectall setEnabled: b_rows];
1229     [o_mi_info setEnabled: b_item_sel];
1230     [o_mi_preparse setEnabled: b_item_sel];
1231     [o_mi_recursive_expand setEnabled: b_item_sel];
1232     [o_mi_sort_name setEnabled: b_item_sel];
1233     [o_mi_sort_author setEnabled: b_item_sel];
1234
1235     return(o_ctx_menu);
1236 }
1237
1238 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1239 {
1240     int i_mode, i_type = 0;
1241     intf_thread_t *p_intf = VLCIntf;
1242     NSString * o_identifier = [o_tc identifier];
1243
1244     playlist_t *p_playlist = pl_Get(p_intf);
1245
1246     if ([o_identifier isEqualToString:TRACKNUM_COLUMN])
1247         i_mode = SORT_TRACK_NUMBER;
1248     else if ([o_identifier isEqualToString:TITLE_COLUMN])
1249         i_mode = SORT_TITLE;
1250     else if ([o_identifier isEqualToString:ARTIST_COLUMN])
1251         i_mode = SORT_ARTIST;
1252     else if ([o_identifier isEqualToString:GENRE_COLUMN])
1253         i_mode = SORT_GENRE;
1254     else if ([o_identifier isEqualToString:DURATION_COLUMN])
1255         i_mode = SORT_DURATION;
1256     else if ([o_identifier isEqualToString:ALBUM_COLUMN])
1257         i_mode = SORT_ALBUM;
1258     else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN])
1259         i_mode = SORT_DESCRIPTION;
1260     else if ([o_identifier isEqualToString:URI_COLUMN])
1261         i_mode = SORT_URI;
1262     else
1263         return;
1264
1265     if (o_tc_sortColumn == o_tc)
1266         b_isSortDescending = !b_isSortDescending;
1267     else
1268         b_isSortDescending = false;
1269
1270     if (b_isSortDescending)
1271         i_type = ORDER_REVERSE;
1272     else
1273         i_type = ORDER_NORMAL;
1274
1275     PL_LOCK;
1276     playlist_RecursiveNodeSort(p_playlist, [self currentPlaylistRoot], i_mode, i_type);
1277     PL_UNLOCK;
1278
1279     [self playlistUpdated];
1280
1281     o_tc_sortColumn = o_tc;
1282     [o_outline_view setHighlightedTableColumn:o_tc];
1283
1284     if (b_isSortDescending)
1285         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1286     else
1287         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1288 }
1289
1290
1291 - (void)outlineView:(NSOutlineView *)outlineView
1292                                 willDisplayCell:(id)cell
1293                                 forTableColumn:(NSTableColumn *)tableColumn
1294                                 item:(id)item
1295 {
1296     /* this method can be called when VLC is already dead, hence the extra checks */
1297     intf_thread_t * p_intf = VLCIntf;
1298     if (!p_intf)
1299         return;
1300     playlist_t *p_playlist = pl_Get(p_intf);
1301     if (!p_playlist)
1302         return;
1303
1304     id o_playing_item;
1305
1306     PL_LOCK;
1307     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1308     PL_UNLOCK;
1309
1310     if ([self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
1311                         || [o_playing_item isEqual: item])
1312         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1313     else
1314         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1315 }
1316
1317 - (id)playingItem
1318 {
1319     playlist_t *p_playlist = pl_Get(VLCIntf);
1320
1321     id o_playing_item;
1322
1323     PL_LOCK;
1324     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1325     PL_UNLOCK;
1326
1327     return o_playing_item;
1328 }
1329
1330 - (NSArray *)draggedItems
1331 {
1332     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1333 }
1334
1335 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1336 {
1337     NSTableColumn * o_work_tc;
1338
1339     if (i_state == NSOnState) {
1340         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1341         [o_work_tc setEditable: NO];
1342         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1343
1344         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1345
1346         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1347             [o_work_tc setWidth: 20.];
1348             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1349             [[o_work_tc headerCell] setStringValue: @"#"];
1350         }
1351
1352         [o_outline_view addTableColumn: o_work_tc];
1353         [o_work_tc release];
1354         [o_outline_view reloadData];
1355         [o_outline_view setNeedsDisplay: YES];
1356     }
1357     else
1358         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1359
1360     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
1361 }
1362
1363 - (void)saveTableColumns
1364 {
1365     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1366     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1367     NSUInteger count = [o_columns count];
1368     NSTableColumn * o_currentColumn;
1369     for (NSUInteger i = 0; i < count; i++) {
1370         o_currentColumn = [o_columns objectAtIndex: i];
1371         [o_arrayToSave addObject: @[[o_currentColumn identifier], @([o_currentColumn width])]];
1372     }
1373     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1374     [[NSUserDefaults standardUserDefaults] synchronize];
1375     [o_columns release];
1376     [o_arrayToSave release];
1377 }
1378
1379 @end
1380
1381 @implementation VLCPlaylist (NSOutlineViewDataSource)
1382
1383 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1384 {
1385     id o_value = [super outlineView: outlineView child: index ofItem: item];
1386
1387     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1388     return o_value;
1389 }
1390
1391 /* Required for drag & drop and reordering */
1392 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1393 {
1394     playlist_t *p_playlist = pl_Get(VLCIntf);
1395
1396     /* First remove the items that were moved during the last drag & drop
1397        operation */
1398     [o_items_array removeAllObjects];
1399     [o_nodes_array removeAllObjects];
1400
1401     NSUInteger itemCount = [items count];
1402
1403     for (NSUInteger i = 0 ; i < itemCount ; i++) {
1404         id o_item = [items objectAtIndex: i];
1405
1406         /* Fill the items and nodes to move in 2 different arrays */
1407         if (((playlist_item_t *)[o_item pointerValue])->i_children > 0)
1408             [o_nodes_array addObject: o_item];
1409         else
1410             [o_items_array addObject: o_item];
1411     }
1412
1413     /* Now we need to check if there are selected items that are in already
1414        selected nodes. In that case, we only want to move the nodes */
1415     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1416     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1417
1418     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1419        a Drop operation coming from the playlist. */
1420
1421     [pboard declareTypes: @[@"VLCPlaylistItemPboardType"] owner: self];
1422     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1423
1424     return YES;
1425 }
1426
1427 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1428 {
1429     playlist_t *p_playlist = pl_Get(VLCIntf);
1430     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1431
1432     if (!p_playlist) return NSDragOperationNone;
1433
1434     /* Dropping ON items is not allowed if item is not a node */
1435     if (item) {
1436         if (index == NSOutlineViewDropOnItemIndex &&
1437                 ((playlist_item_t *)[item pointerValue])->i_children == -1) {
1438             return NSDragOperationNone;
1439         }
1440     }
1441
1442     /* We refuse to drop an item in anything else than a child of the General
1443        Node. We still accept items that would be root nodes of the outlineview
1444        however, to allow drop in an empty playlist. */
1445     if (!(([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO locked: NO] ||
1446             (var_CreateGetBool(p_playlist, "media-library") && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO locked: NO])) || item == nil)) {
1447         return NSDragOperationNone;
1448     }
1449
1450     /* Drop from the Playlist */
1451     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1452         NSUInteger count = [o_nodes_array count];
1453         for (NSUInteger i = 0 ; i < count ; i++) {
1454             /* We refuse to Drop in a child of an item we are moving */
1455             if ([self isItem: [item pointerValue] inNode: [[o_nodes_array objectAtIndex: i] pointerValue] checkItemExistence: NO locked:NO]) {
1456                 return NSDragOperationNone;
1457             }
1458         }
1459         return NSDragOperationMove;
1460     }
1461     /* Drop from the Finder */
1462     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1463         return NSDragOperationGeneric;
1464     }
1465     return NSDragOperationNone;
1466 }
1467
1468 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1469 {
1470     playlist_t * p_playlist =  pl_Get(VLCIntf);
1471     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1472
1473     /* Drag & Drop inside the playlist */
1474     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1475         if (index == -1) // this is no valid target, sanitize to top of table
1476             index = 0;
1477
1478         int i_row = 0;
1479         playlist_item_t *p_new_parent, *p_item = NULL;
1480         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1481         /* If the item is to be dropped as root item of the outline, make it a
1482            child of the respective general node, if is either the pl or the ml
1483            Else, choose the proposed parent as parent. */
1484         if (item == nil) {
1485             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1486                 p_new_parent = [self currentPlaylistRoot];
1487             else
1488                 return NO;
1489         }
1490         else
1491             p_new_parent = [item pointerValue];
1492
1493         /* Make sure the proposed parent is a node.
1494            (This should never be true) */
1495         if (p_new_parent->i_children < 0)
1496             return NO;
1497
1498         NSUInteger count = [o_all_items count];
1499         if (count == 0)
1500             return NO;
1501
1502         playlist_item_t **pp_items = (playlist_item_t **)calloc(count, sizeof(playlist_item_t*));
1503         if (!pp_items)
1504             return NO;
1505
1506         PL_LOCK;
1507         NSUInteger j = 0;
1508         for (NSUInteger i = 0; i < count; i++) {
1509             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1510             if (p_item)
1511                 pp_items[j++] = p_item;
1512         }
1513
1514         if (j == 0 || playlist_TreeMoveMany(p_playlist, j, pp_items, p_new_parent, index) != VLC_SUCCESS) {
1515             PL_UNLOCK;
1516             free(pp_items);
1517             return NO;
1518         }
1519
1520         PL_UNLOCK;
1521         free(pp_items);
1522
1523         [self playlistUpdated];
1524         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex: 0] pointerValue]]]];
1525
1526         if (i_row == -1)
1527             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1528
1529         [o_outline_view deselectAll: self];
1530         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1531         [o_outline_view scrollRowToVisible: i_row];
1532
1533         return YES;
1534     }
1535
1536     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1537         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1538             return NO;
1539
1540         playlist_item_t *p_node = [item pointerValue];
1541
1542         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1543                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1544         NSUInteger count = [o_values count];
1545         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1546         input_thread_t * p_input = pl_CurrentInput(VLCIntf);
1547         BOOL b_returned = NO;
1548
1549         if (count == 1 && p_input) {
1550             b_returned = input_AddSubtitle(p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true);
1551             vlc_object_release(p_input);
1552             if (!b_returned)
1553                 return YES;
1554         }
1555         else if (p_input)
1556             vlc_object_release(p_input);
1557
1558         for (NSUInteger i = 0; i < count; i++) {
1559             NSDictionary *o_dic;
1560             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1561             if (!psz_uri)
1562                 continue;
1563
1564             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1565
1566             free(psz_uri);
1567
1568             [o_array addObject: o_dic];
1569         }
1570
1571         if (item == nil)
1572             [self appendArray:o_array atPos:index enqueue: YES];
1573         else {
1574             assert(p_node->i_children != -1);
1575             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1576         }
1577         return YES;
1578     }
1579     return NO;
1580 }
1581
1582 @end