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