]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: playlist cosmetics
[vlc] / modules / gui / macosx / playlist.m
1 /*****************************************************************************
2  * playlist.m: MacOS X interface module
3  *****************************************************************************
4 * Copyright (C) 2002-2013 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 #include <vlc_url.h>
55
56 /*****************************************************************************
57  * VLCPlaylistView implementation
58  *****************************************************************************/
59 @implementation VLCPlaylistView
60
61 - (NSMenu *)menuForEvent:(NSEvent *)o_event
62 {
63     return([(VLCPlaylist *)[self delegate] menuForEvent: o_event]);
64 }
65
66 - (void)keyDown:(NSEvent *)o_event
67 {
68     unichar key = 0;
69
70     if ([[o_event characters] length])
71         key = [[o_event characters] characterAtIndex: 0];
72
73     switch(key) {
74         case NSDeleteCharacter:
75         case NSDeleteFunctionKey:
76         case NSDeleteCharFunctionKey:
77         case NSBackspaceCharacter:
78             [(VLCPlaylist *)[self delegate] deleteItem:self];
79             break;
80
81         case NSEnterCharacter:
82         case NSCarriageReturnCharacter:
83             [(VLCPlaylist *)[[VLCMain sharedInstance] playlist] playItem:nil];
84             break;
85
86         default:
87             [super keyDown: o_event];
88             break;
89     }
90 }
91
92 - (BOOL)validateMenuItem:(NSMenuItem *)item
93 {
94     if (([self numberOfSelectedRows] >= 1 && [item action] == @selector(delete:)) || [item action] == @selector(selectAll:))
95         return YES;
96
97     return NO;
98 }
99
100 - (BOOL)acceptsFirstResponder
101 {
102     return YES;
103 }
104
105 - (BOOL)becomeFirstResponder
106 {
107     [self setNeedsDisplay:YES];
108     return YES;
109 }
110
111 - (BOOL)resignFirstResponder
112 {
113     [self setNeedsDisplay:YES];
114     return YES;
115 }
116
117 - (IBAction)delete:(id)sender
118 {
119     [[[VLCMain sharedInstance] playlist] deleteItem: sender];
120 }
121
122 @end
123
124 /*****************************************************************************
125  * VLCPlaylistCommon implementation
126  *
127  * This class the superclass of the VLCPlaylist and VLCPlaylistWizard.
128  * It contains the common methods and elements of these 2 entities.
129  *****************************************************************************/
130 @implementation VLCPlaylistCommon
131
132 - (id)init
133 {
134     playlist_t * p_playlist = pl_Get(VLCIntf);
135     p_current_root_item = p_playlist->p_local_category;
136
137     self = [super init];
138     if (self != nil)
139         o_outline_dict = [[NSMutableDictionary alloc] init];
140
141     return self;
142 }
143
144 - (void)awakeFromNib
145 {
146     playlist_t * p_playlist = pl_Get(VLCIntf);
147     [o_outline_view setTarget: self];
148     [o_outline_view setDelegate: self];
149     [o_outline_view setDataSource: self];
150     [o_outline_view setAllowsEmptySelection: NO];
151     [o_outline_view expandItem: [o_outline_view itemAtRow:0]];
152
153     [o_outline_view_other setTarget: self];
154     [o_outline_view_other setDelegate: self];
155     [o_outline_view_other setDataSource: self];
156     [o_outline_view_other setAllowsEmptySelection: NO];
157
158     [[o_tc_name_other headerCell] setStringValue:_NS("Name")];
159     [[o_tc_author_other headerCell] setStringValue:_NS("Author")];
160     [[o_tc_duration_other headerCell] setStringValue:_NS("Duration")];
161 }
162
163 - (void)setPlaylistRoot: (playlist_item_t *)root_item
164 {
165     p_current_root_item = root_item;
166     [o_outline_view reloadData];
167     [o_outline_view_other reloadData];
168 }
169
170 - (playlist_item_t *)currentPlaylistRoot
171 {
172     return p_current_root_item;
173 }
174
175 - (NSOutlineView *)outlineView
176 {
177     return o_outline_view;
178 }
179
180 - (playlist_item_t *)selectedPlaylistItem
181 {
182     return [[o_outline_view itemAtRow: [o_outline_view selectedRow]]
183                                                                 pointerValue];
184 }
185
186 @end
187
188 @implementation VLCPlaylistCommon (NSOutlineViewDataSource)
189 /* return the number of children for Obj-C pointer item */ /* DONE */
190 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
191 {
192     int i_return = 0;
193     playlist_item_t *p_item = NULL;
194     playlist_t * p_playlist = pl_Get(VLCIntf);
195     //assert(outlineView == o_outline_view);
196
197     PL_LOCK;
198     if (!item)
199         p_item = p_current_root_item;
200     else
201         p_item = (playlist_item_t *)[item pointerValue];
202
203     if (p_item)
204         i_return = p_item->i_children;
205     PL_UNLOCK;
206
207     return i_return > 0 ? i_return : 0;
208 }
209
210 /* return the child at index for the Obj-C pointer item */ /* DONE */
211 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
212 {
213     playlist_item_t *p_return = NULL, *p_item = NULL;
214     NSValue *o_value;
215     playlist_t * p_playlist = pl_Get(VLCIntf);
216
217     PL_LOCK;
218     if (item == nil)
219         p_item = p_current_root_item; /* root object */
220     else
221         p_item = (playlist_item_t *)[item pointerValue];
222
223     if (p_item && index < p_item->i_children && index >= 0)
224         p_return = p_item->pp_children[index];
225     PL_UNLOCK;
226
227     o_value = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_return]];
228
229     if (o_value == nil) {
230         /* FIXME: Why is there a warning if that happens all the time and seems
231          * to be normal? Add an assert and fix it.
232          * msg_Warn(VLCIntf, "playlist item misses pointer value, adding one"); */
233         o_value = [[NSValue valueWithPointer: p_return] retain];
234     }
235     return o_value;
236 }
237
238 /* is the item expandable */
239 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
240 {
241     int i_return = 0;
242     playlist_t *p_playlist = pl_Get(VLCIntf);
243
244     PL_LOCK;
245     if (item == nil) {
246         /* root object */
247         if (p_current_root_item) {
248             i_return = p_current_root_item->i_children;
249         }
250     } else {
251         playlist_item_t *p_item = (playlist_item_t *)[item pointerValue];
252         if (p_item)
253             i_return = p_item->i_children;
254     }
255     PL_UNLOCK;
256
257     return (i_return >= 0);
258 }
259
260 /* retrieve the string values for the cells */
261 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)o_tc byItem:(id)item
262 {
263     id o_value = nil;
264     char * psz_value;
265     playlist_item_t *p_item;
266
267     /* For error handling */
268     static BOOL attempted_reload = NO;
269
270     if (item == nil || ![item isKindOfClass: [NSValue class]]) {
271         /* Attempt to fix the error by asking for a data redisplay
272          * This might cause infinite loop, so add a small check */
273         if (!attempted_reload) {
274             attempted_reload = YES;
275             [outlineView reloadData];
276         }
277         return @"error" ;
278     }
279
280     p_item = (playlist_item_t *)[item pointerValue];
281     if (!p_item || !p_item->p_input) {
282         /* Attempt to fix the error by asking for a data redisplay
283          * This might cause infinite loop, so add a small check */
284         if (!attempted_reload) {
285             attempted_reload = YES;
286             [outlineView reloadData];
287         }
288         return @"error";
289     }
290
291     attempted_reload = NO;
292     NSString * o_identifier = [o_tc identifier];
293
294     if ([o_identifier isEqualToString:TRACKNUM_COLUMN]) {
295         psz_value = input_item_GetTrackNumber(p_item->p_input);
296         if (psz_value) {
297             o_value = [NSString stringWithUTF8String:psz_value];
298             free(psz_value);
299         }
300     } else if ([o_identifier isEqualToString:TITLE_COLUMN]) {
301         /* sanity check to prevent the NSString class from crashing */
302         char *psz_title =  input_item_GetTitleFbName(p_item->p_input);
303         if (psz_title) {
304             o_value = [NSString stringWithUTF8String:psz_title];
305             free(psz_title);
306         }
307     } else if ([o_identifier isEqualToString:ARTIST_COLUMN]) {
308         psz_value = input_item_GetArtist(p_item->p_input);
309         if (psz_value) {
310             o_value = [NSString stringWithUTF8String:psz_value];
311             free(psz_value);
312         }
313     } else if ([o_identifier isEqualToString:@"duration"]) {
314         char psz_duration[MSTRTIME_MAX_SIZE];
315         mtime_t dur = input_item_GetDuration(p_item->p_input);
316         if (dur != -1) {
317             secstotimestr(psz_duration, dur/1000000);
318             o_value = [NSString stringWithUTF8String:psz_duration];
319         }
320         else
321             o_value = @"--:--";
322     } else if ([o_identifier isEqualToString:GENRE_COLUMN]) {
323         psz_value = input_item_GetGenre(p_item->p_input);
324         if (psz_value) {
325             o_value = [NSString stringWithUTF8String:psz_value];
326             free(psz_value);
327         }
328     } else if ([o_identifier isEqualToString:ALBUM_COLUMN]) {
329         psz_value = input_item_GetAlbum(p_item->p_input);
330         if (psz_value) {
331             o_value = [NSString stringWithUTF8String:psz_value];
332             free(psz_value);
333         }
334     } else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN]) {
335         psz_value = input_item_GetDescription(p_item->p_input);
336         if (psz_value) {
337             o_value = [NSString stringWithUTF8String:psz_value];
338             free(psz_value);
339         }
340     } else if ([o_identifier isEqualToString:DATE_COLUMN]) {
341         psz_value = input_item_GetDate(p_item->p_input);
342         if (psz_value) {
343             o_value = [NSString stringWithUTF8String:psz_value];
344             free(psz_value);
345         }
346     } else if ([o_identifier isEqualToString:LANGUAGE_COLUMN]) {
347         psz_value = input_item_GetLanguage(p_item->p_input);
348         if (psz_value) {
349             o_value = [NSString stringWithUTF8String:psz_value];
350             free(psz_value);
351         }
352     }
353     else if ([o_identifier isEqualToString:URI_COLUMN]) {
354         psz_value = decode_URI(input_item_GetURI(p_item->p_input));
355         if (psz_value) {
356             o_value = [NSString stringWithUTF8String:psz_value];
357             free(psz_value);
358         }
359     }
360     else if ([o_identifier isEqualToString:@"status"]) {
361         if (input_item_HasErrorWhenReading(p_item->p_input)) {
362             o_value = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kAlertCautionIcon)];
363             [o_value setSize: NSMakeSize(16,16)];
364         }
365     }
366
367     return o_value;
368 }
369
370 @end
371
372 /*****************************************************************************
373  * VLCPlaylistWizard implementation
374  *****************************************************************************/
375 @implementation VLCPlaylistWizard
376
377 - (IBAction)reloadOutlineView
378 {
379     /* Only reload the outlineview if the wizard window is open since this can
380        be quite long on big playlists */
381     if ([[o_outline_view window] isVisible])
382         [o_outline_view reloadData];
383 }
384
385 @end
386
387 /*****************************************************************************
388  * An extension to NSOutlineView's interface to fix compilation warnings
389  * and let us access these 2 functions properly.
390  * This uses a private API, but works fine on all current OSX releases.
391  * Radar ID 11739459 request a public API for this. However, it is probably
392  * easier and faster to recreate similar looking bitmaps ourselves.
393  *****************************************************************************/
394
395 @interface NSOutlineView (UndocumentedSortImages)
396 + (NSImage *)_defaultTableHeaderSortImage;
397 + (NSImage *)_defaultTableHeaderReverseSortImage;
398 @end
399
400
401 /*****************************************************************************
402  * VLCPlaylist implementation
403  *****************************************************************************/
404 @interface VLCPlaylist (Internal)
405 - (void)saveTableColumns;
406 @end
407
408 @implementation VLCPlaylist
409
410 + (void)initialize{
411     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
412     NSMutableArray * o_columnArray = [[NSMutableArray alloc] init];
413     [o_columnArray addObject: [NSArray arrayWithObjects:TITLE_COLUMN, [NSNumber numberWithFloat:190.], nil]];
414     [o_columnArray addObject: [NSArray arrayWithObjects:ARTIST_COLUMN, [NSNumber numberWithFloat:95.], nil]];
415     [o_columnArray addObject: [NSArray arrayWithObjects:DURATION_COLUMN, [NSNumber numberWithFloat:95.], nil]];
416     NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:[NSArray arrayWithArray:o_columnArray] forKey: @"PlaylistColumnSelection"];
417
418     [defaults registerDefaults:appDefaults];
419     [o_columnArray release];
420 }
421
422 - (id)init
423 {
424     self = [super init];
425     if (self != nil) {
426         o_nodes_array = [[NSMutableArray alloc] init];
427         o_items_array = [[NSMutableArray alloc] init];
428     }
429     return self;
430 }
431
432 - (void)dealloc
433 {
434     [o_nodes_array release];
435     [o_items_array release];
436     [super dealloc];
437 }
438
439 - (void)awakeFromNib
440 {
441     playlist_t * p_playlist = pl_Get(VLCIntf);
442
443     [super awakeFromNib];
444     [self initStrings];
445
446     [o_outline_view setDoubleAction: @selector(playItem:)];
447     [o_outline_view_other setDoubleAction: @selector(playItem:)];
448
449     [o_outline_view registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
450     [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
451
452     [o_outline_view_other registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
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         PL_LOCK;
883         playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
884         PL_UNLOCK;
885         [self playlistUpdated];
886         return;
887     }
888     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
889     for (int i = 0; i < i_count; i++) {
890         id o_item = [o_outline_view itemAtRow: indexes[i]];
891         [o_outline_view deselectRow: indexes[i]];
892
893         PL_LOCK;
894         playlist_item_t *p_item = [o_item pointerValue];
895         if (!p_item || !p_item->p_input) {
896             PL_UNLOCK;
897             continue;
898         }
899
900         if (p_item->i_children != -1) {
901         //is a node and not an item
902             if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
903                 [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
904                         checkItemExistence: NO locked:YES] == YES)
905                 // if current item is in selected node and is playing then stop playlist
906                 playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
907
908                 playlist_NodeDelete(p_playlist, p_item, true, false);
909         } else
910             playlist_DeleteFromInput(p_playlist, p_item->p_input, pl_Locked);
911
912         PL_UNLOCK;
913         [o_outline_dict removeObjectForKey:[NSString stringWithFormat:@"%p", [o_item pointerValue]]];
914         [o_item release];
915     }
916
917     [self playlistUpdated];
918 }
919
920 - (IBAction)sortNodeByName:(id)sender
921 {
922     [self sortNode: SORT_TITLE];
923 }
924
925 - (IBAction)sortNodeByAuthor:(id)sender
926 {
927     [self sortNode: SORT_ARTIST];
928 }
929
930 - (void)sortNode:(int)i_mode
931 {
932     playlist_t * p_playlist = pl_Get(VLCIntf);
933     playlist_item_t * p_item;
934
935     if ([o_outline_view selectedRow] > -1) {
936         p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
937         if (!p_item)
938             return;
939     } else
940         p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
941
942     PL_LOCK;
943     if (p_item->i_children > -1) // the item is a node
944         playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
945     else
946         playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
947
948     PL_UNLOCK;
949     [self playlistUpdated];
950 }
951
952 - (input_item_t *)createItem:(NSDictionary *)o_one_item
953 {
954     intf_thread_t * p_intf = VLCIntf;
955     playlist_t * p_playlist = pl_Get(p_intf);
956
957     input_item_t *p_input;
958     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
959     NSString *o_uri, *o_name, *o_path;
960     NSURL * o_nsurl;
961     NSArray *o_options;
962     NSURL *o_true_file;
963
964     /* Get the item */
965     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
966     o_nsurl = [NSURL URLWithString: o_uri];
967     o_path = [o_nsurl path];
968     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
969     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
970
971     if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
972         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
973                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
974         id o_vlc_open = [[VLCMain sharedInstance] open];
975
976         NSString *diskType = [o_vlc_open getVolumeTypeFromMountPath: o_path];
977         msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
978
979         if ([diskType isEqualToString: kVLCMediaDVD])
980             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
981         else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
982             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
983         else if ([diskType isEqualToString: kVLCMediaAudioCD])
984             o_uri = [NSString stringWithFormat: @"cdda://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
985         else if ([diskType isEqualToString: kVLCMediaVCD])
986             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
987         else if ([diskType isEqualToString: kVLCMediaSVCD])
988             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
989         else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
990             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
991         else
992             msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
993
994         p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
995     }
996     else
997         p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
998
999     if (!p_input)
1000         return NULL;
1001
1002     if (o_options) {
1003         NSUInteger count = [o_options count];
1004         for (NSUInteger i = 0; i < count; i++)
1005             input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
1006     }
1007
1008     /* Recent documents menu */
1009     if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
1010         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
1011
1012     return p_input;
1013 }
1014
1015 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
1016 {
1017     playlist_t * p_playlist = pl_Get(VLCIntf);
1018     NSUInteger count = [o_array count];
1019     BOOL b_usingPlaylist;
1020     if ([self currentPlaylistRoot] == p_playlist->p_ml_category)
1021         b_usingPlaylist = NO;
1022     else
1023         b_usingPlaylist = YES;
1024
1025     PL_LOCK;
1026     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1027         input_item_t *p_input;
1028         NSDictionary *o_one_item;
1029
1030         /* Get the item */
1031         o_one_item = [o_array objectAtIndex:i_item];
1032         p_input = [self createItem: o_one_item];
1033         if (!p_input)
1034             continue;
1035
1036         /* Add the item */
1037         int returnValue = playlist_AddInput(p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist, pl_Locked);
1038         if (returnValue != VLC_SUCCESS) {
1039             vlc_gc_decref(p_input);
1040             continue;
1041         }
1042
1043         if (i_item == 0 && !b_enqueue) {
1044             playlist_item_t *p_item = playlist_ItemGetByInput(p_playlist, p_input);
1045             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item);
1046         }
1047
1048         vlc_gc_decref(p_input);
1049     }
1050     PL_UNLOCK;
1051     [self playlistUpdated];
1052 }
1053
1054 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
1055 {
1056     playlist_t * p_playlist = pl_Get(VLCIntf);
1057     NSUInteger count = [o_array count];
1058
1059     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1060         input_item_t *p_input;
1061         NSDictionary *o_one_item;
1062
1063         /* Get the item */
1064         o_one_item = [o_array objectAtIndex:i_item];
1065         p_input = [self createItem: o_one_item];
1066
1067         if (!p_input)
1068             continue;
1069
1070         /* Add the item */
1071         PL_LOCK;
1072         playlist_NodeAddInput(p_playlist, p_input, p_node,
1073                                       PLAYLIST_INSERT,
1074                                       i_position == -1 ?
1075                                       PLAYLIST_END : i_position + i_item,
1076                                       pl_Locked);
1077
1078
1079         if (i_item == 0 && !b_enqueue) {
1080             playlist_item_t *p_item;
1081             p_item = playlist_ItemGetByInput(p_playlist, p_input);
1082             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
1083         }
1084         PL_UNLOCK;
1085         vlc_gc_decref(p_input);
1086     }
1087     [self playlistUpdated];
1088 }
1089
1090 - (NSMutableArray *)subSearchItem:(playlist_item_t *)p_item
1091 {
1092     playlist_t *p_playlist = pl_Get(VLCIntf);
1093     playlist_item_t *p_selected_item;
1094     int i_selected_row;
1095
1096     i_selected_row = [o_outline_view selectedRow];
1097     if (i_selected_row < 0)
1098         i_selected_row = 0;
1099
1100     p_selected_item = (playlist_item_t *)[[o_outline_view itemAtRow: i_selected_row] pointerValue];
1101
1102     for (NSUInteger i_current = 0; i_current < p_item->i_children ; i_current++) {
1103         char *psz_temp;
1104         NSString *o_current_name, *o_current_author;
1105
1106         PL_LOCK;
1107         o_current_name = [NSString stringWithUTF8String:p_item->pp_children[i_current]->p_input->psz_name];
1108         psz_temp = input_item_GetInfo(p_item->p_input, _("Meta-information"),_("Artist"));
1109         o_current_author = [NSString stringWithUTF8String:psz_temp];
1110         free(psz_temp);
1111         PL_UNLOCK;
1112
1113         if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == NO)
1114             b_selected_item_met = YES;
1115         else if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == YES)
1116             return NULL;
1117         else if (b_selected_item_met == YES &&
1118                     ([o_current_name rangeOfString:[o_search_field
1119                         stringValue] options:NSCaseInsensitiveSearch].length ||
1120                       [o_current_author rangeOfString:[o_search_field
1121                         stringValue] options:NSCaseInsensitiveSearch].length))
1122             /*Adds the parent items in the result array as well, so that we can
1123             expand the tree*/
1124             return [NSMutableArray arrayWithObject: [NSValue valueWithPointer: p_item->pp_children[i_current]]];
1125
1126         if (p_item->pp_children[i_current]->i_children > 0) {
1127             id o_result = [self subSearchItem:
1128                                             p_item->pp_children[i_current]];
1129             if (o_result != NULL) {
1130                 [o_result insertObject: [NSValue valueWithPointer:
1131                                 p_item->pp_children[i_current]] atIndex:0];
1132                 return o_result;
1133             }
1134         }
1135     }
1136     return NULL;
1137 }
1138
1139 - (IBAction)searchItem:(id)sender
1140 {
1141     playlist_t * p_playlist = pl_Get(VLCIntf);
1142     id o_result;
1143
1144     int i_row = -1;
1145
1146     b_selected_item_met = NO;
1147
1148     /* First, only search after the selected item:
1149      * (b_selected_item_met = NO) */
1150     o_result = [self subSearchItem:[self currentPlaylistRoot]];
1151     if (o_result == NULL)
1152         /* If the first search failed, search again from the beginning */
1153         o_result = [self subSearchItem:[self currentPlaylistRoot]];
1154
1155     if (o_result != NULL) {
1156         int i_start;
1157         if ([[o_result objectAtIndex:0] pointerValue] == p_playlist->p_local_category)
1158             i_start = 1;
1159         else
1160             i_start = 0;
1161         NSUInteger count = [o_result count];
1162
1163         for (NSUInteger i = i_start ; i < count - 1 ; i++) {
1164             [o_outline_view expandItem: [o_outline_dict objectForKey:
1165                         [NSString stringWithFormat: @"%p",
1166                         [[o_result objectAtIndex:i] pointerValue]]]];
1167         }
1168         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1169                         [NSString stringWithFormat: @"%p",
1170                         [[o_result objectAtIndex:count - 1 ]
1171                         pointerValue]]]];
1172     }
1173     if (i_row > -1) {
1174         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1175         [o_outline_view scrollRowToVisible: i_row];
1176     }
1177 }
1178
1179 - (IBAction)recursiveExpandNode:(id)sender
1180 {
1181     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1182     NSUInteger count = [selectedRows count];
1183     NSUInteger indexes[count];
1184     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1185
1186     id o_item;
1187     playlist_item_t *p_item;
1188     for (NSUInteger i = 0; i < count; i++) {
1189         o_item = [o_outline_view itemAtRow: indexes[i]];
1190         p_item = (playlist_item_t *)[o_item pointerValue];
1191
1192         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
1193             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1194
1195         /* We need to collapse the node first, since OSX refuses to recursively
1196          expand an already expanded node, even if children nodes are collapsed. */
1197         [o_outline_view collapseItem: o_item collapseChildren: YES];
1198         [o_outline_view expandItem: o_item expandChildren: YES];
1199
1200         selectedRows = [o_outline_view selectedRowIndexes];
1201         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1202     }
1203 }
1204
1205 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1206 {
1207     NSPoint pt;
1208     bool b_rows;
1209     bool b_item_sel;
1210
1211     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1212     int row = [o_outline_view rowAtPoint:pt];
1213     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
1214         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1215
1216     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
1217     b_rows = [o_outline_view numberOfRows] != 0;
1218
1219     [o_mi_play setEnabled: b_item_sel];
1220     [o_mi_delete setEnabled: b_item_sel];
1221     [o_mi_selectall setEnabled: b_rows];
1222     [o_mi_info setEnabled: b_item_sel];
1223     [o_mi_preparse setEnabled: b_item_sel];
1224     [o_mi_recursive_expand setEnabled: b_item_sel];
1225     [o_mi_sort_name setEnabled: b_item_sel];
1226     [o_mi_sort_author setEnabled: b_item_sel];
1227
1228     return(o_ctx_menu);
1229 }
1230
1231 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1232 {
1233     int i_mode, i_type = 0;
1234     intf_thread_t *p_intf = VLCIntf;
1235     NSString * o_identifier = [o_tc identifier];
1236
1237     playlist_t *p_playlist = pl_Get(p_intf);
1238
1239     if ([o_identifier isEqualToString:TRACKNUM_COLUMN])
1240         i_mode = SORT_TRACK_NUMBER;
1241     else if ([o_identifier isEqualToString:TITLE_COLUMN])
1242         i_mode = SORT_TITLE;
1243     else if ([o_identifier isEqualToString:ARTIST_COLUMN])
1244         i_mode = SORT_ARTIST;
1245     else if ([o_identifier isEqualToString:GENRE_COLUMN])
1246         i_mode = SORT_GENRE;
1247     else if ([o_identifier isEqualToString:DURATION_COLUMN])
1248         i_mode = SORT_DURATION;
1249     else if ([o_identifier isEqualToString:ALBUM_COLUMN])
1250         i_mode = SORT_ALBUM;
1251     else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN])
1252         i_mode = SORT_DESCRIPTION;
1253     else if ([o_identifier isEqualToString:URI_COLUMN])
1254         i_mode = SORT_URI;
1255     else
1256         return;
1257
1258     if (o_tc_sortColumn == o_tc)
1259         b_isSortDescending = !b_isSortDescending;
1260     else
1261         b_isSortDescending = false;
1262
1263     if (b_isSortDescending)
1264         i_type = ORDER_REVERSE;
1265     else
1266         i_type = ORDER_NORMAL;
1267
1268     PL_LOCK;
1269     playlist_RecursiveNodeSort(p_playlist, [self currentPlaylistRoot], i_mode, i_type);
1270     PL_UNLOCK;
1271
1272     [self playlistUpdated];
1273
1274     o_tc_sortColumn = o_tc;
1275     [o_outline_view setHighlightedTableColumn:o_tc];
1276
1277     if (b_isSortDescending)
1278         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1279     else
1280         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1281 }
1282
1283
1284 - (void)outlineView:(NSOutlineView *)outlineView
1285                                 willDisplayCell:(id)cell
1286                                 forTableColumn:(NSTableColumn *)tableColumn
1287                                 item:(id)item
1288 {
1289     /* this method can be called when VLC is already dead, hence the extra checks */
1290     intf_thread_t * p_intf = VLCIntf;
1291     if (!p_intf)
1292         return;
1293     playlist_t *p_playlist = pl_Get(p_intf);
1294     if (!p_playlist)
1295         return;
1296
1297     id o_playing_item;
1298
1299     PL_LOCK;
1300     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1301     PL_UNLOCK;
1302
1303     if ([self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
1304                         || [o_playing_item isEqual: item])
1305         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1306     else
1307         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1308 }
1309
1310 - (id)playingItem
1311 {
1312     playlist_t *p_playlist = pl_Get(VLCIntf);
1313
1314     id o_playing_item;
1315
1316     PL_LOCK;
1317     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1318     PL_UNLOCK;
1319
1320     return o_playing_item;
1321 }
1322
1323 - (NSArray *)draggedItems
1324 {
1325     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1326 }
1327
1328 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1329 {
1330     NSTableColumn * o_work_tc;
1331
1332     if (i_state == NSOnState) {
1333         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1334         [o_work_tc setEditable: NO];
1335         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1336
1337         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1338
1339         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1340             [o_work_tc setWidth: 20.];
1341             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1342             [[o_work_tc headerCell] setStringValue: @"#"];
1343         }
1344
1345         [o_outline_view addTableColumn: o_work_tc];
1346         [o_work_tc release];
1347         [o_outline_view reloadData];
1348         [o_outline_view setNeedsDisplay: YES];
1349     }
1350     else
1351         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1352
1353     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
1354 }
1355
1356 - (void)saveTableColumns
1357 {
1358     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1359     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1360     NSUInteger count = [o_columns count];
1361     NSTableColumn * o_currentColumn;
1362     for (NSUInteger i = 0; i < count; i++) {
1363         o_currentColumn = [o_columns objectAtIndex:i];
1364         [o_arrayToSave addObject:[NSArray arrayWithObjects:[o_currentColumn identifier], [NSNumber numberWithFloat:[o_currentColumn width]], nil]];
1365     }
1366     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1367     [[NSUserDefaults standardUserDefaults] synchronize];
1368     [o_columns release];
1369     [o_arrayToSave release];
1370 }
1371
1372 @end
1373
1374 @implementation VLCPlaylist (NSOutlineViewDataSource)
1375
1376 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1377 {
1378     id o_value = [super outlineView: outlineView child: index ofItem: item];
1379
1380     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1381     return o_value;
1382 }
1383
1384 /* Required for drag & drop and reordering */
1385 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1386 {
1387     playlist_t *p_playlist = pl_Get(VLCIntf);
1388
1389     /* First remove the items that were moved during the last drag & drop
1390        operation */
1391     [o_items_array removeAllObjects];
1392     [o_nodes_array removeAllObjects];
1393
1394     NSUInteger itemCount = [items count];
1395
1396     for (NSUInteger i = 0 ; i < itemCount ; i++) {
1397         id o_item = [items objectAtIndex:i];
1398
1399         /* Fill the items and nodes to move in 2 different arrays */
1400         if (((playlist_item_t *)[o_item pointerValue])->i_children > 0)
1401             [o_nodes_array addObject: o_item];
1402         else
1403             [o_items_array addObject: o_item];
1404     }
1405
1406     /* Now we need to check if there are selected items that are in already
1407        selected nodes. In that case, we only want to move the nodes */
1408     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1409     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1410
1411     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1412        a Drop operation coming from the playlist. */
1413
1414     [pboard declareTypes: [NSArray arrayWithObject:@"VLCPlaylistItemPboardType"] owner: self];
1415     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1416
1417     return YES;
1418 }
1419
1420 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1421 {
1422     playlist_t *p_playlist = pl_Get(VLCIntf);
1423     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1424
1425     if (!p_playlist) return NSDragOperationNone;
1426
1427     /* Dropping ON items is not allowed if item is not a node */
1428     if (item) {
1429         if (index == NSOutlineViewDropOnItemIndex &&
1430                 ((playlist_item_t *)[item pointerValue])->i_children == -1) {
1431             return NSDragOperationNone;
1432         }
1433     }
1434
1435     /* We refuse to drop an item in anything else than a child of the General
1436        Node. We still accept items that would be root nodes of the outlineview
1437        however, to allow drop in an empty playlist. */
1438     if (!(([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO locked: NO] ||
1439             (var_CreateGetBool(p_playlist, "media-library") && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO locked: NO])) || item == nil)) {
1440         return NSDragOperationNone;
1441     }
1442
1443     /* Drop from the Playlist */
1444     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1445         NSUInteger count = [o_nodes_array count];
1446         for (NSUInteger i = 0 ; i < count ; i++) {
1447             /* We refuse to Drop in a child of an item we are moving */
1448             if ([self isItem: [item pointerValue] inNode: [[o_nodes_array objectAtIndex:i] pointerValue] checkItemExistence: NO locked:NO]) {
1449                 return NSDragOperationNone;
1450             }
1451         }
1452         return NSDragOperationMove;
1453     }
1454     /* Drop from the Finder */
1455     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1456         return NSDragOperationGeneric;
1457     }
1458     return NSDragOperationNone;
1459 }
1460
1461 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1462 {
1463     playlist_t * p_playlist =  pl_Get(VLCIntf);
1464     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1465
1466     /* Drag & Drop inside the playlist */
1467     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1468         if (index == -1) // this is no valid target, sanitize to top of table
1469             index = 0;
1470
1471         int i_row = 0;
1472         playlist_item_t *p_new_parent, *p_item = NULL;
1473         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1474         /* If the item is to be dropped as root item of the outline, make it a
1475            child of the respective general node, if is either the pl or the ml
1476            Else, choose the proposed parent as parent. */
1477         if (item == nil) {
1478             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1479                 p_new_parent = [self currentPlaylistRoot];
1480             else
1481                 return NO;
1482         }
1483         else
1484             p_new_parent = [item pointerValue];
1485
1486         /* Make sure the proposed parent is a node.
1487            (This should never be true) */
1488         if (p_new_parent->i_children < 0)
1489             return NO;
1490
1491         NSUInteger count = [o_all_items count];
1492         if (count == 0)
1493             return NO;
1494
1495         playlist_item_t **pp_items = (playlist_item_t **)calloc(count, sizeof(playlist_item_t*));
1496         if (!pp_items)
1497             return NO;
1498
1499         PL_LOCK;
1500         NSUInteger j = 0;
1501         for (NSUInteger i = 0; i < count; i++) {
1502             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1503             if (p_item)
1504                 pp_items[j++] = p_item;
1505         }
1506
1507         if (j == 0 || playlist_TreeMoveMany(p_playlist, j, pp_items, p_new_parent, index) != VLC_SUCCESS) {
1508             PL_UNLOCK;
1509             free(pp_items);
1510             return NO;
1511         }
1512
1513         PL_UNLOCK;
1514         free(pp_items);
1515
1516         [self playlistUpdated];
1517         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex:0] pointerValue]]]];
1518
1519         if (i_row == -1)
1520             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1521
1522         [o_outline_view deselectAll: self];
1523         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1524         [o_outline_view scrollRowToVisible: i_row];
1525
1526         return YES;
1527     }
1528
1529     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1530         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1531             return NO;
1532
1533         playlist_item_t *p_node = [item pointerValue];
1534
1535         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1536                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1537         NSUInteger count = [o_values count];
1538         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1539         input_thread_t * p_input = pl_CurrentInput(VLCIntf);
1540         BOOL b_returned = NO;
1541
1542         if (count == 1 && p_input) {
1543             b_returned = input_AddSubtitle(p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true);
1544             vlc_object_release(p_input);
1545             if (!b_returned)
1546                 return YES;
1547         }
1548         else if (p_input)
1549             vlc_object_release(p_input);
1550
1551         for (NSUInteger i = 0; i < count; i++) {
1552             NSDictionary *o_dic;
1553             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1554             if (!psz_uri)
1555                 continue;
1556
1557             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1558
1559             free(psz_uri);
1560
1561             [o_array addObject: o_dic];
1562         }
1563
1564         if (item == nil)
1565             [self appendArray:o_array atPos:index enqueue: YES];
1566         else {
1567             assert(p_node->i_children != -1);
1568             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1569         }
1570         return YES;
1571     }
1572     return NO;
1573 }
1574
1575 @end