]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: fix utf8 issue
[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
829         /* continue playback where you left off */
830         input_item_t *p_input = p_item->p_input;
831         if (p_input)
832             [self _continuePlaybackWhereYouLeftOff:p_input];
833
834         playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
835     }
836     PL_UNLOCK;
837 }
838
839 - (IBAction)revealItemInFinder:(id)sender
840 {
841     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
842     NSUInteger count = [selectedRows count];
843     NSUInteger indexes[count];
844     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
845
846     NSMutableString * o_mrl;
847     playlist_item_t *p_item;
848     for (NSUInteger i = 0; i < count; i++) {
849         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
850
851         if (! p_item || !p_item->p_input)
852             continue;
853
854         char * psz_url = decode_URI(input_item_GetURI(p_item->p_input));
855         o_mrl = [[NSMutableString alloc] initWithString: [NSString stringWithUTF8String:psz_url ? psz_url : ""]];
856         if (psz_url != NULL)
857             free( psz_url );
858
859         /* perform some checks whether it is a file and if it is local at all... */
860         if ([o_mrl length] > 0) {
861             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
862             if (prefix_range.location != NSNotFound)
863                 [o_mrl deleteCharactersInRange: prefix_range];
864
865             if ([o_mrl characterAtIndex:0] == '/')
866                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
867         }
868
869         [o_mrl release];
870     }
871 }
872
873 /* When called retrieves the selected outlineview row and plays that node or item */
874 - (IBAction)preparseItem:(id)sender
875 {
876     int i_count;
877     NSIndexSet *o_selected_indexes;
878     intf_thread_t * p_intf = VLCIntf;
879     playlist_t * p_playlist = pl_Get(p_intf);
880     playlist_item_t *p_item = NULL;
881
882     o_selected_indexes = [o_outline_view selectedRowIndexes];
883     i_count = [o_selected_indexes count];
884
885     NSUInteger indexes[i_count];
886     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
887     for (int i = 0; i < i_count; i++) {
888         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
889         [o_outline_view deselectRow: indexes[i]];
890
891         if (p_item) {
892             if (p_item->i_children == -1)
893                 libvlc_MetaRequest(p_intf->p_libvlc, p_item->p_input);
894             else
895                 msg_Dbg(p_intf, "preparsing nodes not implemented");
896         }
897     }
898     [self playlistUpdated];
899 }
900
901 - (IBAction)downloadCoverArt:(id)sender
902 {
903     int i_count;
904     NSIndexSet *o_selected_indexes;
905     intf_thread_t * p_intf = VLCIntf;
906     playlist_t * p_playlist = pl_Get(p_intf);
907     playlist_item_t *p_item = NULL;
908
909     o_selected_indexes = [o_outline_view selectedRowIndexes];
910     i_count = [o_selected_indexes count];
911
912     NSUInteger indexes[i_count];
913     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
914     for (int i = 0; i < i_count; i++) {
915         p_item = [[o_outline_view itemAtRow: indexes[i]] pointerValue];
916
917         if (p_item && p_item->i_children == -1)
918             libvlc_ArtRequest(p_intf->p_libvlc, p_item->p_input);
919     }
920     [self playlistUpdated];
921 }
922
923 - (IBAction)selectAll:(id)sender
924 {
925     [o_outline_view selectAll: nil];
926 }
927
928 - (IBAction)showInfoPanel:(id)sender
929 {
930     [[[VLCMain sharedInstance] info] initPanel];
931 }
932
933 - (IBAction)deleteItem:(id)sender
934 {
935     int i_count;
936     NSIndexSet *o_selected_indexes;
937     playlist_t * p_playlist;
938     intf_thread_t * p_intf = VLCIntf;
939
940     o_selected_indexes = [o_outline_view selectedRowIndexes];
941     i_count = [o_selected_indexes count];
942     retainedRowSelection = [o_selected_indexes firstIndex];
943
944     p_playlist = pl_Get(p_intf);
945
946     NSUInteger indexes[i_count];
947     if (i_count == [o_outline_view numberOfRows]) {
948         PL_LOCK;
949         playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
950         PL_UNLOCK;
951         [self playlistUpdated];
952         return;
953     }
954     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
955     for (int i = 0; i < i_count; i++) {
956         id o_item = [o_outline_view itemAtRow: indexes[i]];
957         [o_outline_view deselectRow: indexes[i]];
958
959         PL_LOCK;
960         playlist_item_t *p_item = [o_item pointerValue];
961         if (!p_item || !p_item->p_input) {
962             PL_UNLOCK;
963             continue;
964         }
965
966         if (p_item->i_children != -1) {
967         //is a node and not an item
968             if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
969                 [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
970                         checkItemExistence: NO locked:YES] == YES)
971                 // if current item is in selected node and is playing then stop playlist
972                 playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
973
974                 playlist_NodeDelete(p_playlist, p_item, true, false);
975         } else
976             playlist_DeleteFromInput(p_playlist, p_item->p_input, pl_Locked);
977
978         PL_UNLOCK;
979         [o_outline_dict removeObjectForKey:[NSString stringWithFormat:@"%p", [o_item pointerValue]]];
980         [o_item release];
981     }
982
983     [self playlistUpdated];
984 }
985
986 - (IBAction)sortNodeByName:(id)sender
987 {
988     [self sortNode: SORT_TITLE];
989 }
990
991 - (IBAction)sortNodeByAuthor:(id)sender
992 {
993     [self sortNode: SORT_ARTIST];
994 }
995
996 - (void)sortNode:(int)i_mode
997 {
998     playlist_t * p_playlist = pl_Get(VLCIntf);
999     playlist_item_t * p_item;
1000
1001     if ([o_outline_view selectedRow] > -1) {
1002         p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
1003         if (!p_item)
1004             return;
1005     } else
1006         p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
1007
1008     PL_LOCK;
1009     if (p_item->i_children > -1) // the item is a node
1010         playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
1011     else
1012         playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
1013
1014     PL_UNLOCK;
1015     [self playlistUpdated];
1016 }
1017
1018 - (input_item_t *)createItem:(NSDictionary *)o_one_item
1019 {
1020     intf_thread_t * p_intf = VLCIntf;
1021     playlist_t * p_playlist = pl_Get(p_intf);
1022
1023     input_item_t *p_input;
1024     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
1025     NSString *o_uri, *o_name, *o_path;
1026     NSURL * o_nsurl;
1027     NSArray *o_options;
1028     NSURL *o_true_file;
1029
1030     /* Get the item */
1031     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
1032     o_nsurl = [NSURL URLWithString: o_uri];
1033     o_path = [o_nsurl path];
1034     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
1035     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
1036
1037     if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
1038         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
1039                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
1040         id o_vlc_open = [[VLCMain sharedInstance] open];
1041
1042         NSString *diskType = [o_vlc_open getVolumeTypeFromMountPath: o_path];
1043         msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
1044
1045         if ([diskType isEqualToString: kVLCMediaDVD])
1046             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1047         else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
1048             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
1049         else if ([diskType isEqualToString: kVLCMediaAudioCD])
1050             o_uri = [NSString stringWithFormat: @"cdda://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1051         else if ([diskType isEqualToString: kVLCMediaVCD])
1052             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1053         else if ([diskType isEqualToString: kVLCMediaSVCD])
1054             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
1055         else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
1056             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
1057         else
1058             msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
1059
1060         p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
1061     }
1062     else
1063         p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
1064
1065     if (!p_input)
1066         return NULL;
1067
1068     if (o_options) {
1069         NSUInteger count = [o_options count];
1070         for (NSUInteger i = 0; i < count; i++)
1071             input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
1072     }
1073
1074     [self _continuePlaybackWhereYouLeftOff:p_input];
1075
1076     /* Recent documents menu */
1077     if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
1078         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
1079
1080     return p_input;
1081 }
1082
1083 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
1084 {
1085     playlist_t * p_playlist = pl_Get(VLCIntf);
1086     NSUInteger count = [o_array count];
1087     BOOL b_usingPlaylist;
1088     if ([self currentPlaylistRoot] == p_playlist->p_ml_category)
1089         b_usingPlaylist = NO;
1090     else
1091         b_usingPlaylist = YES;
1092
1093     PL_LOCK;
1094     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1095         input_item_t *p_input;
1096         NSDictionary *o_one_item;
1097
1098         /* Get the item */
1099         o_one_item = [o_array objectAtIndex:i_item];
1100         p_input = [self createItem: o_one_item];
1101         if (!p_input)
1102             continue;
1103
1104         /* Add the item */
1105         int returnValue = playlist_AddInput(p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist, pl_Locked);
1106         if (returnValue != VLC_SUCCESS) {
1107             vlc_gc_decref(p_input);
1108             continue;
1109         }
1110
1111         if (i_item == 0 && !b_enqueue) {
1112             playlist_item_t *p_item = playlist_ItemGetByInput(p_playlist, p_input);
1113             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item);
1114         }
1115
1116         vlc_gc_decref(p_input);
1117     }
1118     PL_UNLOCK;
1119     [self playlistUpdated];
1120 }
1121
1122 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
1123 {
1124     playlist_t * p_playlist = pl_Get(VLCIntf);
1125     NSUInteger count = [o_array count];
1126
1127     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1128         input_item_t *p_input;
1129         NSDictionary *o_one_item;
1130
1131         /* Get the item */
1132         PL_LOCK;
1133         o_one_item = [o_array objectAtIndex:i_item];
1134         p_input = [self createItem: o_one_item];
1135
1136         if (!p_input)
1137             continue;
1138
1139         /* Add the item */
1140         playlist_NodeAddInput(p_playlist, p_input, p_node,
1141                                       PLAYLIST_INSERT,
1142                                       i_position == -1 ?
1143                                       PLAYLIST_END : i_position + i_item,
1144                                       pl_Locked);
1145
1146
1147         if (i_item == 0 && !b_enqueue) {
1148             playlist_item_t *p_item;
1149             p_item = playlist_ItemGetByInput(p_playlist, p_input);
1150             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
1151         }
1152         PL_UNLOCK;
1153         vlc_gc_decref(p_input);
1154     }
1155     [self playlistUpdated];
1156 }
1157
1158 - (NSMutableArray *)subSearchItem:(playlist_item_t *)p_item
1159 {
1160     playlist_t *p_playlist = pl_Get(VLCIntf);
1161     playlist_item_t *p_selected_item;
1162     int i_selected_row;
1163
1164     i_selected_row = [o_outline_view selectedRow];
1165     if (i_selected_row < 0)
1166         i_selected_row = 0;
1167
1168     p_selected_item = (playlist_item_t *)[[o_outline_view itemAtRow: i_selected_row] pointerValue];
1169
1170     for (NSUInteger i_current = 0; i_current < p_item->i_children ; i_current++) {
1171         char *psz_temp;
1172         NSString *o_current_name, *o_current_author;
1173
1174         PL_LOCK;
1175         o_current_name = [NSString stringWithUTF8String:p_item->pp_children[i_current]->p_input->psz_name];
1176         psz_temp = input_item_GetInfo(p_item->p_input, _("Meta-information"),_("Artist"));
1177         o_current_author = [NSString stringWithUTF8String:psz_temp];
1178         free(psz_temp);
1179         PL_UNLOCK;
1180
1181         if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == NO)
1182             b_selected_item_met = YES;
1183         else if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == YES)
1184             return NULL;
1185         else if (b_selected_item_met == YES &&
1186                     ([o_current_name rangeOfString:[o_search_field
1187                         stringValue] options:NSCaseInsensitiveSearch].length ||
1188                       [o_current_author rangeOfString:[o_search_field
1189                         stringValue] options:NSCaseInsensitiveSearch].length))
1190             /*Adds the parent items in the result array as well, so that we can
1191             expand the tree*/
1192             return [NSMutableArray arrayWithObject: [NSValue valueWithPointer: p_item->pp_children[i_current]]];
1193
1194         if (p_item->pp_children[i_current]->i_children > 0) {
1195             id o_result = [self subSearchItem:
1196                                             p_item->pp_children[i_current]];
1197             if (o_result != NULL) {
1198                 [o_result insertObject: [NSValue valueWithPointer:
1199                                 p_item->pp_children[i_current]] atIndex:0];
1200                 return o_result;
1201             }
1202         }
1203     }
1204     return NULL;
1205 }
1206
1207 - (IBAction)searchItem:(id)sender
1208 {
1209     playlist_t * p_playlist = pl_Get(VLCIntf);
1210     id o_result;
1211
1212     int i_row = -1;
1213
1214     b_selected_item_met = NO;
1215
1216     /* First, only search after the selected item:
1217      * (b_selected_item_met = NO) */
1218     o_result = [self subSearchItem:[self currentPlaylistRoot]];
1219     if (o_result == NULL)
1220         /* If the first search failed, search again from the beginning */
1221         o_result = [self subSearchItem:[self currentPlaylistRoot]];
1222
1223     if (o_result != NULL) {
1224         int i_start;
1225         if ([[o_result objectAtIndex:0] pointerValue] == p_playlist->p_local_category)
1226             i_start = 1;
1227         else
1228             i_start = 0;
1229         NSUInteger count = [o_result count];
1230
1231         for (NSUInteger i = i_start ; i < count - 1 ; i++) {
1232             [o_outline_view expandItem: [o_outline_dict objectForKey:
1233                         [NSString stringWithFormat: @"%p",
1234                         [[o_result objectAtIndex:i] pointerValue]]]];
1235         }
1236         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1237                         [NSString stringWithFormat: @"%p",
1238                         [[o_result objectAtIndex:count - 1 ]
1239                         pointerValue]]]];
1240     }
1241     if (i_row > -1) {
1242         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1243         [o_outline_view scrollRowToVisible: i_row];
1244     }
1245 }
1246
1247 - (IBAction)recursiveExpandNode:(id)sender
1248 {
1249     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1250     NSUInteger count = [selectedRows count];
1251     NSUInteger indexes[count];
1252     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1253
1254     id o_item;
1255     playlist_item_t *p_item;
1256     for (NSUInteger i = 0; i < count; i++) {
1257         o_item = [o_outline_view itemAtRow: indexes[i]];
1258         p_item = (playlist_item_t *)[o_item pointerValue];
1259
1260         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
1261             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1262
1263         /* We need to collapse the node first, since OSX refuses to recursively
1264          expand an already expanded node, even if children nodes are collapsed. */
1265         [o_outline_view collapseItem: o_item collapseChildren: YES];
1266         [o_outline_view expandItem: o_item expandChildren: YES];
1267
1268         selectedRows = [o_outline_view selectedRowIndexes];
1269         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1270     }
1271 }
1272
1273 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1274 {
1275     if (!b_playlistmenu_nib_loaded)
1276         b_playlistmenu_nib_loaded = [NSBundle loadNibNamed:@"PlaylistMenu" owner:self];
1277
1278     NSPoint pt;
1279     bool b_rows;
1280     bool b_item_sel;
1281
1282     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1283     int row = [o_outline_view rowAtPoint:pt];
1284     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
1285         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1286
1287     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
1288     b_rows = [o_outline_view numberOfRows] != 0;
1289
1290     [o_mi_play setEnabled: b_item_sel];
1291     [o_mi_delete setEnabled: b_item_sel];
1292     [o_mi_selectall setEnabled: b_rows];
1293     [o_mi_info setEnabled: b_item_sel];
1294     [o_mi_preparse setEnabled: b_item_sel];
1295     [o_mi_recursive_expand setEnabled: b_item_sel];
1296     [o_mi_sort_name setEnabled: b_item_sel];
1297     [o_mi_sort_author setEnabled: b_item_sel];
1298
1299     return o_ctx_menu;
1300 }
1301
1302 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1303 {
1304     int i_mode, i_type = 0;
1305     intf_thread_t *p_intf = VLCIntf;
1306     NSString * o_identifier = [o_tc identifier];
1307
1308     playlist_t *p_playlist = pl_Get(p_intf);
1309
1310     if ([o_identifier isEqualToString:TRACKNUM_COLUMN])
1311         i_mode = SORT_TRACK_NUMBER;
1312     else if ([o_identifier isEqualToString:TITLE_COLUMN])
1313         i_mode = SORT_TITLE;
1314     else if ([o_identifier isEqualToString:ARTIST_COLUMN])
1315         i_mode = SORT_ARTIST;
1316     else if ([o_identifier isEqualToString:GENRE_COLUMN])
1317         i_mode = SORT_GENRE;
1318     else if ([o_identifier isEqualToString:DURATION_COLUMN])
1319         i_mode = SORT_DURATION;
1320     else if ([o_identifier isEqualToString:ALBUM_COLUMN])
1321         i_mode = SORT_ALBUM;
1322     else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN])
1323         i_mode = SORT_DESCRIPTION;
1324     else if ([o_identifier isEqualToString:URI_COLUMN])
1325         i_mode = SORT_URI;
1326     else
1327         return;
1328
1329     if (o_tc_sortColumn == o_tc)
1330         b_isSortDescending = !b_isSortDescending;
1331     else
1332         b_isSortDescending = false;
1333
1334     if (b_isSortDescending)
1335         i_type = ORDER_REVERSE;
1336     else
1337         i_type = ORDER_NORMAL;
1338
1339     PL_LOCK;
1340     playlist_RecursiveNodeSort(p_playlist, [self currentPlaylistRoot], i_mode, i_type);
1341     PL_UNLOCK;
1342
1343     [self playlistUpdated];
1344
1345     o_tc_sortColumn = o_tc;
1346     [o_outline_view setHighlightedTableColumn:o_tc];
1347
1348     if (b_isSortDescending)
1349         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1350     else
1351         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1352 }
1353
1354
1355 - (void)outlineView:(NSOutlineView *)outlineView
1356                                 willDisplayCell:(id)cell
1357                                 forTableColumn:(NSTableColumn *)tableColumn
1358                                 item:(id)item
1359 {
1360     /* this method can be called when VLC is already dead, hence the extra checks */
1361     intf_thread_t * p_intf = VLCIntf;
1362     if (!p_intf)
1363         return;
1364     playlist_t *p_playlist = pl_Get(p_intf);
1365     if (!p_playlist)
1366         return;
1367
1368     id o_playing_item;
1369
1370     PL_LOCK;
1371     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1372     PL_UNLOCK;
1373
1374     if ([self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
1375                         || [o_playing_item isEqual: item])
1376         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1377     else
1378         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1379 }
1380
1381 - (id)playingItem
1382 {
1383     playlist_t *p_playlist = pl_Get(VLCIntf);
1384
1385     id o_playing_item;
1386
1387     PL_LOCK;
1388     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1389     PL_UNLOCK;
1390
1391     return o_playing_item;
1392 }
1393
1394 - (NSArray *)draggedItems
1395 {
1396     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1397 }
1398
1399 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1400 {
1401     NSTableColumn * o_work_tc;
1402
1403     if (i_state == NSOnState) {
1404         NSString *o_title = [o_dict objectForKey:o_column];
1405         if (!o_title)
1406             return;
1407
1408         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1409         [o_work_tc setEditable: NO];
1410         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1411
1412         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1413
1414         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1415             [o_work_tc setWidth: 20.];
1416             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1417             [[o_work_tc headerCell] setStringValue: @"#"];
1418         }
1419
1420         [o_outline_view addTableColumn: o_work_tc];
1421         [o_work_tc release];
1422         [o_outline_view reloadData];
1423         [o_outline_view setNeedsDisplay: YES];
1424     }
1425     else
1426         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1427
1428     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
1429 }
1430
1431 - (void)saveTableColumns
1432 {
1433     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1434     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1435     NSUInteger count = [o_columns count];
1436     NSTableColumn * o_currentColumn;
1437     for (NSUInteger i = 0; i < count; i++) {
1438         o_currentColumn = [o_columns objectAtIndex:i];
1439         [o_arrayToSave addObject:[NSArray arrayWithObjects:[o_currentColumn identifier], [NSNumber numberWithFloat:[o_currentColumn width]], nil]];
1440     }
1441     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1442     [[NSUserDefaults standardUserDefaults] synchronize];
1443     [o_columns release];
1444     [o_arrayToSave release];
1445 }
1446
1447 - (void)_continuePlaybackWhereYouLeftOff:(input_item_t *)p_input
1448 {
1449     NSDictionary *recentlyPlayedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"recentlyPlayedMedia"];
1450     if (recentlyPlayedFiles) {
1451         char *psz_url = decode_URI(input_item_GetURI(p_input));
1452         NSString *url = [NSString stringWithUTF8String:psz_url ? psz_url : ""];
1453         free(psz_url);
1454
1455         NSNumber *lastPosition = [recentlyPlayedFiles objectForKey:url];
1456         if (lastPosition.intValue > 0) {
1457             msg_Dbg(VLCIntf, "last playback position for %s was %i", [url UTF8String], lastPosition.intValue);
1458
1459             int settingValue = config_GetInt(VLCIntf, "macosx-continue-playback");
1460             NSInteger returnValue = 0;
1461
1462             if (settingValue == 0) {
1463                 NSAlert *theAlert = [NSAlert alertWithMessageText:_NS("Continue playback?") defaultButton:_NS("Continue") alternateButton:_NS("Restart playback") otherButton:_NS("Always continue") informativeTextWithFormat:_NS("Playback of \"%@\" will continue at %@"), [NSString stringWithUTF8String:input_item_GetTitleFbName(p_input)], [[VLCStringUtility sharedInstance] stringForTime:lastPosition.intValue]];
1464
1465                 playlist_t *p_playlist = pl_Get(VLCIntf);
1466                 PL_UNLOCK;
1467                 returnValue = [theAlert runModal];
1468                 PL_LOCK;
1469             }
1470
1471             if (returnValue !=0 || settingValue == 1)
1472                 input_item_AddOption(p_input, [[NSString stringWithFormat:@"start-time=%i", lastPosition.intValue] UTF8String], VLC_INPUT_OPTION_TRUSTED);
1473
1474             if (returnValue == -1)
1475                 config_PutInt(VLCIntf, "macosx-continue-playback", 1);
1476         }
1477     }
1478 }
1479
1480 @end
1481
1482 @implementation VLCPlaylist (NSOutlineViewDataSource)
1483
1484 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1485 {
1486     id o_value = [super outlineView: outlineView child: index ofItem: item];
1487
1488     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1489     return o_value;
1490 }
1491
1492 /* Required for drag & drop and reordering */
1493 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1494 {
1495     playlist_t *p_playlist = pl_Get(VLCIntf);
1496
1497     /* First remove the items that were moved during the last drag & drop
1498        operation */
1499     [o_items_array removeAllObjects];
1500     [o_nodes_array removeAllObjects];
1501
1502     NSUInteger itemCount = [items count];
1503
1504     for (NSUInteger i = 0 ; i < itemCount ; i++) {
1505         id o_item = [items objectAtIndex:i];
1506
1507         /* Fill the items and nodes to move in 2 different arrays */
1508         if (((playlist_item_t *)[o_item pointerValue])->i_children > 0)
1509             [o_nodes_array addObject: o_item];
1510         else
1511             [o_items_array addObject: o_item];
1512     }
1513
1514     /* Now we need to check if there are selected items that are in already
1515        selected nodes. In that case, we only want to move the nodes */
1516     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1517     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1518
1519     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1520        a Drop operation coming from the playlist. */
1521
1522     [pboard declareTypes: [NSArray arrayWithObject:@"VLCPlaylistItemPboardType"] owner: self];
1523     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1524
1525     return YES;
1526 }
1527
1528 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1529 {
1530     playlist_t *p_playlist = pl_Get(VLCIntf);
1531     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1532
1533     if (!p_playlist) return NSDragOperationNone;
1534
1535     /* Dropping ON items is not allowed if item is not a node */
1536     if (item) {
1537         if (index == NSOutlineViewDropOnItemIndex &&
1538                 ((playlist_item_t *)[item pointerValue])->i_children == -1) {
1539             return NSDragOperationNone;
1540         }
1541     }
1542
1543     /* We refuse to drop an item in anything else than a child of the General
1544        Node. We still accept items that would be root nodes of the outlineview
1545        however, to allow drop in an empty playlist. */
1546     if (!(([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO locked: NO] ||
1547             (var_CreateGetBool(p_playlist, "media-library") && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO locked: NO])) || item == nil)) {
1548         return NSDragOperationNone;
1549     }
1550
1551     /* Drop from the Playlist */
1552     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1553         NSUInteger count = [o_nodes_array count];
1554         for (NSUInteger i = 0 ; i < count ; i++) {
1555             /* We refuse to Drop in a child of an item we are moving */
1556             if ([self isItem: [item pointerValue] inNode: [[o_nodes_array objectAtIndex:i] pointerValue] checkItemExistence: NO locked:NO]) {
1557                 return NSDragOperationNone;
1558             }
1559         }
1560         return NSDragOperationMove;
1561     }
1562     /* Drop from the Finder */
1563     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1564         return NSDragOperationGeneric;
1565     }
1566     return NSDragOperationNone;
1567 }
1568
1569 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1570 {
1571     playlist_t * p_playlist =  pl_Get(VLCIntf);
1572     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1573
1574     /* Drag & Drop inside the playlist */
1575     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1576         if (index == -1) // this is no valid target, sanitize to top of table
1577             index = 0;
1578
1579         int i_row = 0;
1580         playlist_item_t *p_new_parent, *p_item = NULL;
1581         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1582         /* If the item is to be dropped as root item of the outline, make it a
1583            child of the respective general node, if is either the pl or the ml
1584            Else, choose the proposed parent as parent. */
1585         if (item == nil) {
1586             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1587                 p_new_parent = [self currentPlaylistRoot];
1588             else
1589                 return NO;
1590         }
1591         else
1592             p_new_parent = [item pointerValue];
1593
1594         /* Make sure the proposed parent is a node.
1595            (This should never be true) */
1596         if (p_new_parent->i_children < 0)
1597             return NO;
1598
1599         NSUInteger count = [o_all_items count];
1600         if (count == 0)
1601             return NO;
1602
1603         playlist_item_t **pp_items = (playlist_item_t **)calloc(count, sizeof(playlist_item_t*));
1604         if (!pp_items)
1605             return NO;
1606
1607         PL_LOCK;
1608         NSUInteger j = 0;
1609         for (NSUInteger i = 0; i < count; i++) {
1610             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1611             if (p_item)
1612                 pp_items[j++] = p_item;
1613         }
1614
1615         if (j == 0 || playlist_TreeMoveMany(p_playlist, j, pp_items, p_new_parent, index) != VLC_SUCCESS) {
1616             PL_UNLOCK;
1617             free(pp_items);
1618             return NO;
1619         }
1620
1621         PL_UNLOCK;
1622         free(pp_items);
1623
1624         [self playlistUpdated];
1625         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex:0] pointerValue]]]];
1626
1627         if (i_row == -1)
1628             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1629
1630         [o_outline_view deselectAll: self];
1631         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1632         [o_outline_view scrollRowToVisible: i_row];
1633
1634         return YES;
1635     }
1636
1637     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1638         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1639             return NO;
1640
1641         playlist_item_t *p_node = [item pointerValue];
1642
1643         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1644                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1645         NSUInteger count = [o_values count];
1646         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1647         input_thread_t * p_input = pl_CurrentInput(VLCIntf);
1648
1649         if (count == 1 && p_input) {
1650             int i_result = input_AddSubtitleOSD(p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true, true);
1651             vlc_object_release(p_input);
1652             if (i_result == VLC_SUCCESS)
1653                 return YES;
1654         }
1655         else if (p_input)
1656             vlc_object_release(p_input);
1657
1658         for (NSUInteger i = 0; i < count; i++) {
1659             NSDictionary *o_dic;
1660             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1661             if (!psz_uri)
1662                 continue;
1663
1664             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1665
1666             free(psz_uri);
1667
1668             [o_array addObject: o_dic];
1669         }
1670
1671         if (item == nil)
1672             [self appendArray:o_array atPos:index enqueue: YES];
1673         else {
1674             assert(p_node->i_children != -1);
1675             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1676         }
1677         return YES;
1678     }
1679     return NO;
1680 }
1681
1682 @end