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