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