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