]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: fixed Reveal-in-Finder for eastern european umlauts and probably other charac...
[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 @implementation VLCPlaylist
407
408 + (void)initialize{
409     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
410     NSMutableArray * o_columnArray = [[NSMutableArray alloc] init];
411     [o_columnArray addObject: [NSArray arrayWithObjects: TITLE_COLUMN, [NSNumber numberWithFloat: 190.], nil]];
412     [o_columnArray addObject: [NSArray arrayWithObjects: ARTIST_COLUMN, [NSNumber numberWithFloat: 95.], nil]];
413     [o_columnArray addObject: [NSArray arrayWithObjects: DURATION_COLUMN, [NSNumber numberWithFloat: 95.], nil]];
414     NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:[NSArray arrayWithArray:o_columnArray] forKey:@"PlaylistColumnSelection"];
415
416     [defaults registerDefaults:appDefaults];
417     [o_columnArray release];
418 }
419
420 - (id)init
421 {
422     self = [super init];
423     if (self != nil) {
424         o_nodes_array = [[NSMutableArray alloc] init];
425         o_items_array = [[NSMutableArray alloc] init];
426     }
427     return self;
428 }
429
430 - (void)dealloc
431 {
432     [o_nodes_array release];
433     [o_items_array release];
434     [super dealloc];
435 }
436
437 - (void)awakeFromNib
438 {
439     playlist_t * p_playlist = pl_Get(VLCIntf);
440
441     [super awakeFromNib];
442
443     [o_outline_view setDoubleAction: @selector(playItem:)];
444     [o_outline_view_other setDoubleAction: @selector(playItem:)];
445
446     [o_outline_view registerForDraggedTypes:
447         [NSArray arrayWithObjects: NSFilenamesPboardType,
448         @"VLCPlaylistItemPboardType", nil]];
449     [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
450
451     [o_outline_view_other registerForDraggedTypes:
452      [NSArray arrayWithObjects: NSFilenamesPboardType,
453       @"VLCPlaylistItemPboardType", nil]];
454     [o_outline_view_other setIntercellSpacing: NSMakeSize (0.0, 1.0)];
455
456     /* This uses a private API, but works fine on all current OSX releases.
457      * Radar ID 11739459 request a public API for this. However, it is probably
458      * easier and faster to recreate similar looking bitmaps ourselves. */
459     o_ascendingSortingImage = [[NSOutlineView class] _defaultTableHeaderSortImage];
460     o_descendingSortingImage = [[NSOutlineView class] _defaultTableHeaderReverseSortImage];
461
462     o_tc_sortColumn = nil;
463
464     NSArray * o_columnArray = [[NSUserDefaults standardUserDefaults] arrayForKey:@"PlaylistColumnSelection"];
465     NSUInteger count = [o_columnArray count];
466
467     id o_menu = [[VLCMain sharedInstance] mainMenu];
468     NSString * o_column;
469
470     NSMenu *o_context_menu = [o_menu setupPlaylistTableColumnsMenu];
471     [o_playlist_header setMenu: o_context_menu];
472
473     for (NSUInteger i = 0; i < count; i++) {
474         o_column = [[o_columnArray objectAtIndex:i] objectAtIndex:0];
475         if ([o_column isEqualToString:@"status"])
476             continue;
477
478         [o_menu setPlaylistColumnTableState: NSOnState forColumn: o_column];
479         [[o_outline_view tableColumnWithIdentifier: o_column] setWidth: [[[o_columnArray objectAtIndex:i] objectAtIndex:1] floatValue]];
480     }
481
482     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];
483 }
484
485 - (void)applicationWillTerminate:(NSNotification *)notification
486 {
487     /* 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 */
488     [self saveTableColumns];
489 }
490
491 - (void)searchfieldChanged:(NSNotification *)o_notification
492 {
493     [o_search_field setStringValue:[[o_notification object] stringValue]];
494 }
495
496 - (void)initStrings
497 {
498     [o_mi_save_playlist setTitle: _NS("Save Playlist...")];
499     [o_mi_play setTitle: _NS("Play")];
500     [o_mi_delete setTitle: _NS("Delete")];
501     [o_mi_recursive_expand setTitle: _NS("Expand Node")];
502     [o_mi_selectall setTitle: _NS("Select All")];
503     [o_mi_info setTitle: _NS("Media Information...")];
504     [o_mi_dl_cover_art setTitle: _NS("Download Cover Art")];
505     [o_mi_preparse setTitle: _NS("Fetch Meta Data")];
506     [o_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
507     [o_mm_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
508     [[o_mm_mi_revealInFinder menu] setAutoenablesItems: NO];
509     [o_mi_sort_name setTitle: _NS("Sort Node by Name")];
510     [o_mi_sort_author setTitle: _NS("Sort Node by Author")];
511
512     [o_search_field setToolTip: _NS("Search in Playlist")];
513     [o_search_field_other setToolTip: _NS("Search in Playlist")];
514
515     [o_save_accessory_text setStringValue: _NS("File Format:")];
516     [[o_save_accessory_popup itemAtIndex:0] setTitle: _NS("Extended M3U")];
517     [[o_save_accessory_popup itemAtIndex:1] setTitle: _NS("XML Shareable Playlist Format (XSPF)")];
518     [[o_save_accessory_popup itemAtIndex:2] setTitle: _NS("HTML Playlist")];
519 }
520
521 - (void)playlistUpdated
522 {
523     /* Clear indications of any existing column sorting */
524     NSUInteger count = [[o_outline_view tableColumns] count];
525     for (NSUInteger i = 0 ; i < count ; i++)
526         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
527
528     [o_outline_view setHighlightedTableColumn:nil];
529     o_tc_sortColumn = nil;
530     // TODO Find a way to keep the dict size to a minimum
531     //[o_outline_dict removeAllObjects];
532     [o_outline_view reloadData];
533     [[[[VLCMain sharedInstance] wizard] playlistWizard] reloadOutlineView];
534     [[[[VLCMain sharedInstance] bookmarks] dataTable] reloadData];
535
536     [self outlineViewSelectionDidChange: nil];
537     [[VLCMain sharedInstance] updateMainWindow];
538 }
539
540 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
541 {
542     // FIXME: unsafe
543     playlist_item_t * p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
544
545     if (p_item) {
546         /* update the state of our Reveal-in-Finder menu items */
547         NSMutableString *o_mrl;
548         char *psz_uri = input_item_GetURI(p_item->p_input);
549
550         [o_mi_revealInFinder setEnabled: NO];
551         [o_mm_mi_revealInFinder setEnabled: NO];
552         if (psz_uri) {
553             o_mrl = [NSMutableString stringWithUTF8String: psz_uri];
554
555             /* perform some checks whether it is a file and if it is local at all... */
556             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
557             if (prefix_range.location != NSNotFound)
558                 [o_mrl deleteCharactersInRange: prefix_range];
559
560             if ([o_mrl characterAtIndex:0] == '/') {
561                 [o_mi_revealInFinder setEnabled: YES];
562                 [o_mm_mi_revealInFinder setEnabled: YES];
563             }
564             free(psz_uri);
565         }
566
567         /* update our info-panel to reflect the new item */
568         [[[VLCMain sharedInstance] info] updatePanelWithItem:p_item->p_input];
569     }
570 }
571
572 - (BOOL)isSelectionEmpty
573 {
574     return [o_outline_view selectedRow] == -1;
575 }
576
577 - (void)updateRowSelection
578 {
579     // FIXME: unsafe
580     playlist_t *p_playlist = pl_Get(VLCIntf);
581     playlist_item_t *p_item, *p_temp_item;
582     NSMutableArray *o_array = [NSMutableArray array];
583
584     PL_LOCK;
585     p_item = playlist_CurrentPlayingItem(p_playlist);
586     if (p_item == NULL) {
587         PL_UNLOCK;
588         return;
589     }
590
591     p_temp_item = p_item;
592     while(p_temp_item->p_parent) {
593         [o_array insertObject: [NSValue valueWithPointer: p_temp_item] atIndex: 0];
594         p_temp_item = p_temp_item->p_parent;
595     }
596     PL_UNLOCK;
597
598     NSUInteger count = [o_array count];
599     for (NSUInteger j = 0; j < count - 1; j++) {
600         id o_item;
601         if ((o_item = [o_outline_dict objectForKey:
602                             [NSString stringWithFormat: @"%p",
603                             [[o_array objectAtIndex:j] pointerValue]]]) != nil) {
604             [o_outline_view expandItem: o_item];
605         }
606     }
607
608     id o_item = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_item]];
609     NSInteger i_index = [o_outline_view rowForItem:o_item];
610     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_index] byExtendingSelection:NO];
611     [o_outline_view setNeedsDisplay:YES];
612 }
613
614 /* Check if p_item is a child of p_node recursively. We need to check the item
615    existence first since OSX sometimes tries to redraw items that have been
616    deleted. We don't do it when not required since this verification takes
617    quite a long time on big playlists (yes, pretty hacky). */
618
619 - (BOOL)isItem: (playlist_item_t *)p_item inNode: (playlist_item_t *)p_node checkItemExistence:(BOOL)b_check locked:(BOOL)b_locked
620 {
621     playlist_t * p_playlist = pl_Get(VLCIntf);
622     playlist_item_t *p_temp_item = p_item;
623
624     if (!p_node)
625         return NO;
626
627     if (p_node == p_item)
628         return YES;
629
630     if (p_node->i_children < 1)
631         return NO;
632
633     if (p_temp_item) {
634         int i;
635         if (!b_locked) PL_LOCK;
636
637         if (b_check) {
638         /* Since outlineView: willDisplayCell:... may call this function with
639            p_items that don't exist anymore, first check if the item is still
640            in the playlist. Any cleaner solution welcomed. */
641             for (i = 0; i < p_playlist->all_items.i_size; i++) {
642                 if (ARRAY_VAL(p_playlist->all_items, i) == p_item)
643                     break;
644                 else if (i == p_playlist->all_items.i_size - 1)
645                 {
646                     if (!b_locked) PL_UNLOCK;
647                     return NO;
648                 }
649             }
650         }
651
652         while(p_temp_item) {
653             p_temp_item = p_temp_item->p_parent;
654             if (p_temp_item == p_node) {
655                 if (!b_locked) PL_UNLOCK;
656                 return YES;
657             }
658         }
659         if (!b_locked) PL_UNLOCK;
660     }
661     return NO;
662 }
663
664 /* This method is useful for instance to remove the selected children of an
665    already selected node */
666 - (void)removeItemsFrom:(id)o_items ifChildrenOf:(id)o_nodes
667 {
668     NSUInteger itemCount = [o_items count];
669     NSUInteger nodeCount = [o_nodes count];
670     for (NSUInteger i = 0 ; i < itemCount ; i++) {
671         for (NSUInteger j = 0 ; j < nodeCount ; j++) {
672             if (o_items == o_nodes) {
673                 if (j == i) continue;
674             }
675             if ([self isItem: [[o_items objectAtIndex:i] pointerValue]
676                     inNode: [[o_nodes objectAtIndex:j] pointerValue]
677                     checkItemExistence: NO locked:NO]) {
678                 [o_items removeObjectAtIndex:i];
679                 /* We need to execute the next iteration with the same index
680                    since the current item has been deleted */
681                 i--;
682                 break;
683             }
684         }
685     }
686 }
687
688 - (IBAction)savePlaylist:(id)sender
689 {
690     playlist_t * p_playlist = pl_Get(VLCIntf);
691
692     NSSavePanel *o_save_panel = [NSSavePanel savePanel];
693     NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
694
695     [o_save_panel setTitle: _NS("Save Playlist")];
696     [o_save_panel setPrompt: _NS("Save")];
697     [o_save_panel setAccessoryView: o_save_accessory_view];
698     [o_save_panel setNameFieldStringValue: o_name];
699
700     if ([o_save_panel runModal] == NSFileHandlingPanelOKButton) {
701         NSString *o_filename = [[o_save_panel URL] path];
702
703         if ([o_save_accessory_popup indexOfSelectedItem] == 0) {
704             NSString * o_real_filename;
705             NSRange range;
706             range.location = [o_filename length] - [@".m3u" length];
707             range.length = [@".m3u" length];
708
709             if ([o_filename compare:@".m3u" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
710                 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
711             else
712                 o_real_filename = o_filename;
713
714             playlist_Export(p_playlist,
715                 [o_real_filename fileSystemRepresentation],
716                 p_playlist->p_local_category, "export-m3u");
717         } else if ([o_save_accessory_popup indexOfSelectedItem] == 1) {
718             NSString * o_real_filename;
719             NSRange range;
720             range.location = [o_filename length] - [@".xspf" length];
721             range.length = [@".xspf" length];
722
723             if ([o_filename compare:@".xspf" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
724                 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
725             else
726                 o_real_filename = o_filename;
727
728             playlist_Export(p_playlist,
729                 [o_real_filename fileSystemRepresentation],
730                 p_playlist->p_local_category, "export-xspf");
731         } else {
732             NSString * o_real_filename;
733             NSRange range;
734             range.location = [o_filename length] - [@".html" length];
735             range.length = [@".html" length];
736
737             if ([o_filename compare:@".html" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
738                 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
739             else
740                 o_real_filename = o_filename;
741
742             playlist_Export(p_playlist,
743                 [o_real_filename fileSystemRepresentation],
744                 p_playlist->p_local_category, "export-html");
745         }
746     }
747 }
748
749 /* When called retrieves the selected outlineview row and plays that node or item */
750 - (IBAction)playItem:(id)sender
751 {
752     intf_thread_t * p_intf = VLCIntf;
753     playlist_t * p_playlist = pl_Get(p_intf);
754
755     playlist_item_t *p_item;
756     playlist_item_t *p_node = NULL;
757
758     // ignore clicks on column header when handling double action
759     if (sender != nil && [o_outline_view clickedRow] == -1 && sender != o_mi_play)
760         return;
761
762     p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
763
764     PL_LOCK;
765     if (p_item) {
766         if (p_item->i_children == -1) {
767             p_node = p_item->p_parent;
768         } else {
769             p_node = p_item;
770             if (p_node->i_children > 0 && p_node->pp_children[0]->i_children == -1)
771                 p_item = p_node->pp_children[0];
772             else
773                 p_item = NULL;
774         }
775         playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
776     }
777     PL_UNLOCK;
778 }
779
780 - (IBAction)revealItemInFinder:(id)sender
781 {
782     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
783     NSUInteger count = [selectedRows count];
784     NSUInteger indexes[count];
785     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
786
787     NSMutableString * o_mrl;
788     playlist_item_t *p_item;
789     for (NSUInteger i = 0; i < count; i++) {
790         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
791
792         if (! p_item || !p_item->p_input)
793             continue;
794
795         char * psz_url = decode_URI(input_item_GetURI(p_item->p_input));
796         o_mrl = [[NSMutableString alloc] initWithString: [NSString stringWithUTF8String: psz_url ? psz_url : ""]];
797         if (psz_url != NULL)
798             free( psz_url );
799
800         /* perform some checks whether it is a file and if it is local at all... */
801         if ([o_mrl length] > 0) {
802             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
803             if (prefix_range.location != NSNotFound)
804                 [o_mrl deleteCharactersInRange: prefix_range];
805
806             if ([o_mrl characterAtIndex:0] == '/')
807                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
808         }
809
810         [o_mrl release];
811     }
812 }
813
814 /* When called retrieves the selected outlineview row and plays that node or item */
815 - (IBAction)preparseItem:(id)sender
816 {
817     int i_count;
818     NSIndexSet *o_selected_indexes;
819     intf_thread_t * p_intf = VLCIntf;
820     playlist_t * p_playlist = pl_Get(p_intf);
821     playlist_item_t *p_item = NULL;
822
823     o_selected_indexes = [o_outline_view selectedRowIndexes];
824     i_count = [o_selected_indexes count];
825
826     NSUInteger indexes[i_count];
827     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
828     for (int i = 0; i < i_count; i++) {
829         p_item = [[o_outline_view itemAtRow:indexes[i]] pointerValue];
830         [o_outline_view deselectRow: indexes[i]];
831
832         if (p_item) {
833             if (p_item->i_children == -1)
834                 playlist_PreparseEnqueue(p_playlist, p_item->p_input);
835             else
836                 msg_Dbg(p_intf, "preparsing nodes not implemented");
837         }
838     }
839     [self playlistUpdated];
840 }
841
842 - (IBAction)downloadCoverArt:(id)sender
843 {
844     int i_count;
845     NSIndexSet *o_selected_indexes;
846     intf_thread_t * p_intf = VLCIntf;
847     playlist_t * p_playlist = pl_Get(p_intf);
848     playlist_item_t *p_item = NULL;
849
850     o_selected_indexes = [o_outline_view selectedRowIndexes];
851     i_count = [o_selected_indexes count];
852
853     NSUInteger indexes[i_count];
854     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
855     for (int i = 0; i < i_count; i++) {
856         p_item = [[o_outline_view itemAtRow: indexes[i]] pointerValue];
857
858         if (p_item && p_item->i_children == -1)
859             playlist_AskForArtEnqueue(p_playlist, p_item->p_input);
860     }
861     [self playlistUpdated];
862 }
863
864 - (IBAction)selectAll:(id)sender
865 {
866     [o_outline_view selectAll: nil];
867 }
868
869 - (IBAction)deleteItem:(id)sender
870 {
871     int i_count;
872     NSIndexSet *o_selected_indexes;
873     playlist_t * p_playlist;
874     intf_thread_t * p_intf = VLCIntf;
875
876     o_selected_indexes = [o_outline_view selectedRowIndexes];
877     i_count = [o_selected_indexes count];
878
879     p_playlist = pl_Get(p_intf);
880
881     NSUInteger indexes[i_count];
882     if (i_count == [o_outline_view numberOfRows]) {
883 #ifndef NDEBUG
884         msg_Dbg(p_intf, "user selected entire list, deleting current playlist root instead of individual items");
885 #endif
886         PL_LOCK;
887         playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
888         PL_UNLOCK;
889         [self playlistUpdated];
890         return;
891     }
892     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
893     for (int i = 0; i < i_count; i++) {
894         id o_item = [o_outline_view itemAtRow: indexes[i]];
895         [o_outline_view deselectRow: indexes[i]];
896
897         PL_LOCK;
898         playlist_item_t *p_item = [o_item pointerValue];
899         if (!p_item || !p_item->p_input) {
900             PL_UNLOCK;
901             continue;
902         }
903 #ifndef NDEBUG
904         msg_Dbg(p_intf, "deleting item %i (of %i) with id \"%i\", pointerValue \"%p\" and %i children", i+1, i_count,
905                 p_item->p_input->i_id, [o_item pointerValue], p_item->i_children +1);
906 #endif
907
908         if (p_item->i_children != -1) {
909         //is a node and not an item
910             if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
911                 [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
912                         checkItemExistence: NO locked:YES] == YES)
913                 // if current item is in selected node and is playing then stop playlist
914                 playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
915
916                 playlist_NodeDelete(p_playlist, p_item, true, false);
917         } else
918             playlist_DeleteFromInput(p_playlist, p_item->p_input, pl_Locked);
919
920         PL_UNLOCK;
921         [o_outline_dict removeObjectForKey:[NSString stringWithFormat:@"%p", [o_item pointerValue]]];
922         [o_item release];
923     }
924
925     [self playlistUpdated];
926 }
927
928 - (IBAction)sortNodeByName:(id)sender
929 {
930     [self sortNode: SORT_TITLE];
931 }
932
933 - (IBAction)sortNodeByAuthor:(id)sender
934 {
935     [self sortNode: SORT_ARTIST];
936 }
937
938 - (void)sortNode:(int)i_mode
939 {
940     playlist_t * p_playlist = pl_Get(VLCIntf);
941     playlist_item_t * p_item;
942
943     if ([o_outline_view selectedRow] > -1) {
944         p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
945         if (!p_item)
946             return;
947     } else
948         p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
949
950     PL_LOCK;
951     if (p_item->i_children > -1) // the item is a node
952         playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
953     else
954         playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
955
956     PL_UNLOCK;
957     [self playlistUpdated];
958 }
959
960 - (input_item_t *)createItem:(NSDictionary *)o_one_item
961 {
962     intf_thread_t * p_intf = VLCIntf;
963     playlist_t * p_playlist = pl_Get(p_intf);
964
965     input_item_t *p_input;
966     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
967     NSString *o_uri, *o_name, *o_path;
968     NSURL * o_nsurl;
969     NSArray *o_options;
970     NSURL *o_true_file;
971
972     /* Get the item */
973     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
974     o_nsurl = [NSURL URLWithString: o_uri];
975     o_path = [o_nsurl path];
976     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
977     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
978
979     if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
980         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
981                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
982         id o_vlc_open = [[VLCMain sharedInstance] open];
983
984         NSString *diskType = [o_vlc_open getVolumeTypeFromMountPath: o_path];
985         msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
986
987         if ([diskType isEqualToString: kVLCMediaDVD])
988             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
989         else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
990             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
991         else if ([diskType isEqualToString: kVLCMediaAudioCD])
992             o_uri = [NSString stringWithFormat: @"cdda://%@", [o_vlc_open getBSDNodeFromMountPath: o_path]];
993         else if ([diskType isEqualToString: kVLCMediaVCD])
994             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
995         else if ([diskType isEqualToString: kVLCMediaSVCD])
996             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [o_vlc_open getBSDNodeFromMountPath: o_path]];
997         else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
998             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
999         else
1000             msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
1001
1002         p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
1003     }
1004     else
1005         p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
1006
1007     if (!p_input)
1008         return NULL;
1009
1010     if (o_options) {
1011         NSUInteger count = [o_options count];
1012         for (NSUInteger i = 0; i < count; i++)
1013             input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
1014     }
1015
1016     /* Recent documents menu */
1017     if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
1018         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
1019
1020     return p_input;
1021 }
1022
1023 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
1024 {
1025     playlist_t * p_playlist = pl_Get(VLCIntf);
1026     NSUInteger count = [o_array count];
1027     BOOL b_usingPlaylist;
1028     if ([self currentPlaylistRoot] == p_playlist->p_ml_category)
1029         b_usingPlaylist = NO;
1030     else
1031         b_usingPlaylist = YES;
1032
1033     PL_LOCK;
1034     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1035         input_item_t *p_input;
1036         NSDictionary *o_one_item;
1037
1038         /* Get the item */
1039         o_one_item = [o_array objectAtIndex: i_item];
1040         p_input = [self createItem: o_one_item];
1041         if (!p_input)
1042             continue;
1043
1044         /* Add the item */
1045         int returnValue = playlist_AddInput(p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist, pl_Locked);
1046         if (returnValue != VLC_SUCCESS) {
1047             vlc_gc_decref(p_input);
1048             continue;
1049         }
1050
1051         if (i_item == 0 && !b_enqueue) {
1052             playlist_item_t *p_item = playlist_ItemGetByInput(p_playlist, p_input);
1053             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item);
1054         }
1055
1056         vlc_gc_decref(p_input);
1057     }
1058     PL_UNLOCK;
1059     [self playlistUpdated];
1060 }
1061
1062 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
1063 {
1064     playlist_t * p_playlist = pl_Get(VLCIntf);
1065     NSUInteger count = [o_array count];
1066
1067     for (NSUInteger i_item = 0; i_item < count; i_item++) {
1068         input_item_t *p_input;
1069         NSDictionary *o_one_item;
1070
1071         /* Get the item */
1072         o_one_item = [o_array objectAtIndex: i_item];
1073         p_input = [self createItem: o_one_item];
1074
1075         if (!p_input)
1076             continue;
1077
1078         /* Add the item */
1079         PL_LOCK;
1080         playlist_NodeAddInput(p_playlist, p_input, p_node,
1081                                       PLAYLIST_INSERT,
1082                                       i_position == -1 ?
1083                                       PLAYLIST_END : i_position + i_item,
1084                                       pl_Locked);
1085
1086
1087         if (i_item == 0 && !b_enqueue) {
1088             playlist_item_t *p_item;
1089             p_item = playlist_ItemGetByInput(p_playlist, p_input);
1090             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
1091         }
1092         PL_UNLOCK;
1093         vlc_gc_decref(p_input);
1094     }
1095     [self playlistUpdated];
1096 }
1097
1098 - (NSMutableArray *)subSearchItem:(playlist_item_t *)p_item
1099 {
1100     playlist_t *p_playlist = pl_Get(VLCIntf);
1101     playlist_item_t *p_selected_item;
1102     int i_selected_row;
1103
1104     i_selected_row = [o_outline_view selectedRow];
1105     if (i_selected_row < 0)
1106         i_selected_row = 0;
1107
1108     p_selected_item = (playlist_item_t *)[[o_outline_view itemAtRow: i_selected_row] pointerValue];
1109
1110     for (NSUInteger i_current = 0; i_current < p_item->i_children ; i_current++) {
1111         char *psz_temp;
1112         NSString *o_current_name, *o_current_author;
1113
1114         PL_LOCK;
1115         o_current_name = [NSString stringWithUTF8String:
1116             p_item->pp_children[i_current]->p_input->psz_name];
1117         psz_temp = input_item_GetInfo(p_item->p_input ,
1118                    _("Meta-information"),_("Artist"));
1119         o_current_author = [NSString stringWithUTF8String: psz_temp];
1120         free(psz_temp);
1121         PL_UNLOCK;
1122
1123         if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == NO)
1124             b_selected_item_met = YES;
1125         else if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == YES)
1126             return NULL;
1127         else if (b_selected_item_met == YES &&
1128                     ([o_current_name rangeOfString:[o_search_field
1129                         stringValue] options:NSCaseInsensitiveSearch].length ||
1130                       [o_current_author rangeOfString:[o_search_field
1131                         stringValue] options:NSCaseInsensitiveSearch].length))
1132             /*Adds the parent items in the result array as well, so that we can
1133             expand the tree*/
1134             return [NSMutableArray arrayWithObject: [NSValue valueWithPointer: p_item->pp_children[i_current]]];
1135
1136         if (p_item->pp_children[i_current]->i_children > 0) {
1137             id o_result = [self subSearchItem:
1138                                             p_item->pp_children[i_current]];
1139             if (o_result != NULL) {
1140                 [o_result insertObject: [NSValue valueWithPointer:
1141                                 p_item->pp_children[i_current]] atIndex:0];
1142                 return o_result;
1143             }
1144         }
1145     }
1146     return NULL;
1147 }
1148
1149 - (IBAction)searchItem:(id)sender
1150 {
1151     playlist_t * p_playlist = pl_Get(VLCIntf);
1152     id o_result;
1153
1154     int i_row = -1;
1155
1156     b_selected_item_met = NO;
1157
1158     /* First, only search after the selected item:
1159      * (b_selected_item_met = NO) */
1160     o_result = [self subSearchItem:[self currentPlaylistRoot]];
1161     if (o_result == NULL)
1162         /* If the first search failed, search again from the beginning */
1163         o_result = [self subSearchItem:[self currentPlaylistRoot]];
1164
1165     if (o_result != NULL) {
1166         int i_start;
1167         if ([[o_result objectAtIndex: 0] pointerValue] == p_playlist->p_local_category)
1168             i_start = 1;
1169         else
1170             i_start = 0;
1171         NSUInteger count = [o_result count];
1172
1173         for (NSUInteger i = i_start ; i < count - 1 ; i++) {
1174             [o_outline_view expandItem: [o_outline_dict objectForKey:
1175                         [NSString stringWithFormat: @"%p",
1176                         [[o_result objectAtIndex: i] pointerValue]]]];
1177         }
1178         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1179                         [NSString stringWithFormat: @"%p",
1180                         [[o_result objectAtIndex: count - 1 ]
1181                         pointerValue]]]];
1182     }
1183     if (i_row > -1) {
1184         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1185         [o_outline_view scrollRowToVisible: i_row];
1186     }
1187 }
1188
1189 - (IBAction)recursiveExpandNode:(id)sender
1190 {
1191     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1192     NSUInteger count = [selectedRows count];
1193     NSUInteger indexes[count];
1194     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1195
1196     id o_item;
1197     playlist_item_t *p_item;
1198     for (NSUInteger i = 0; i < count; i++) {
1199         o_item = [o_outline_view itemAtRow: indexes[i]];
1200         p_item = (playlist_item_t *)[o_item pointerValue];
1201
1202         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
1203             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1204
1205         /* We need to collapse the node first, since OSX refuses to recursively
1206          expand an already expanded node, even if children nodes are collapsed. */
1207         [o_outline_view collapseItem: o_item collapseChildren: YES];
1208         [o_outline_view expandItem: o_item expandChildren: YES];
1209
1210         selectedRows = [o_outline_view selectedRowIndexes];
1211         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1212     }
1213 }
1214
1215 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1216 {
1217     NSPoint pt;
1218     bool b_rows;
1219     bool b_item_sel;
1220
1221     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1222     int row = [o_outline_view rowAtPoint:pt];
1223     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
1224         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1225
1226     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
1227     b_rows = [o_outline_view numberOfRows] != 0;
1228
1229     [o_mi_play setEnabled: b_item_sel];
1230     [o_mi_delete setEnabled: b_item_sel];
1231     [o_mi_selectall setEnabled: b_rows];
1232     [o_mi_info setEnabled: b_item_sel];
1233     [o_mi_preparse setEnabled: b_item_sel];
1234     [o_mi_recursive_expand setEnabled: b_item_sel];
1235     [o_mi_sort_name setEnabled: b_item_sel];
1236     [o_mi_sort_author setEnabled: b_item_sel];
1237
1238     return(o_ctx_menu);
1239 }
1240
1241 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1242 {
1243     int i_mode, i_type = 0;
1244     intf_thread_t *p_intf = VLCIntf;
1245     NSString * o_identifier = [o_tc identifier];
1246
1247     playlist_t *p_playlist = pl_Get(p_intf);
1248
1249     if ([o_identifier isEqualToString:TRACKNUM_COLUMN])
1250         i_mode = SORT_TRACK_NUMBER;
1251     else if ([o_identifier isEqualToString:TITLE_COLUMN])
1252         i_mode = SORT_TITLE;
1253     else if ([o_identifier isEqualToString:ARTIST_COLUMN])
1254         i_mode = SORT_ARTIST;
1255     else if ([o_identifier isEqualToString:GENRE_COLUMN])
1256         i_mode = SORT_GENRE;
1257     else if ([o_identifier isEqualToString:DURATION_COLUMN])
1258         i_mode = SORT_DURATION;
1259     else if ([o_identifier isEqualToString:ALBUM_COLUMN])
1260         i_mode = SORT_ALBUM;
1261     else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN])
1262         i_mode = SORT_DESCRIPTION;
1263     else if ([o_identifier isEqualToString:URI_COLUMN])
1264         i_mode = SORT_URI;
1265     else
1266         return;
1267
1268     if (o_tc_sortColumn == o_tc)
1269         b_isSortDescending = !b_isSortDescending;
1270     else
1271         b_isSortDescending = false;
1272
1273     if (b_isSortDescending)
1274         i_type = ORDER_REVERSE;
1275     else
1276         i_type = ORDER_NORMAL;
1277
1278     PL_LOCK;
1279     playlist_RecursiveNodeSort(p_playlist, [self currentPlaylistRoot], i_mode, i_type);
1280     PL_UNLOCK;
1281
1282     [self playlistUpdated];
1283
1284     o_tc_sortColumn = o_tc;
1285     [o_outline_view setHighlightedTableColumn:o_tc];
1286
1287     if (b_isSortDescending)
1288         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1289     else
1290         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1291 }
1292
1293
1294 - (void)outlineView:(NSOutlineView *)outlineView
1295                                 willDisplayCell:(id)cell
1296                                 forTableColumn:(NSTableColumn *)tableColumn
1297                                 item:(id)item
1298 {
1299     /* this method can be called when VLC is already dead, hence the extra checks */
1300     intf_thread_t * p_intf = VLCIntf;
1301     if (!p_intf)
1302         return;
1303     playlist_t *p_playlist = pl_Get(p_intf);
1304     if (!p_playlist)
1305         return;
1306
1307     id o_playing_item;
1308
1309     PL_LOCK;
1310     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1311     PL_UNLOCK;
1312
1313     if ([self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
1314                         || [o_playing_item isEqual: item])
1315         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1316     else
1317         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1318 }
1319
1320 - (id)playingItem
1321 {
1322     playlist_t *p_playlist = pl_Get(VLCIntf);
1323
1324     id o_playing_item;
1325
1326     PL_LOCK;
1327     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1328     PL_UNLOCK;
1329
1330     return o_playing_item;
1331 }
1332
1333 - (NSArray *)draggedItems
1334 {
1335     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1336 }
1337
1338 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1339 {
1340     NSTableColumn * o_work_tc;
1341
1342     if (i_state == NSOnState) {
1343         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1344         [o_work_tc setEditable: NO];
1345         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1346
1347         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1348
1349         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1350             [o_work_tc setWidth: 20.];
1351             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1352             [[o_work_tc headerCell] setStringValue: @"#"];
1353         }
1354
1355         [o_outline_view addTableColumn: o_work_tc];
1356         [o_work_tc release];
1357         [o_outline_view reloadData];
1358         [o_outline_view setNeedsDisplay: YES];
1359     }
1360     else
1361         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1362 }
1363
1364 - (void)saveTableColumns
1365 {
1366     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1367     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1368     NSUInteger count = [o_columns count];
1369     NSTableColumn * o_currentColumn;
1370     for (NSUInteger i = 0; i < count; i++) {
1371         o_currentColumn = [o_columns objectAtIndex: i];
1372         [o_arrayToSave addObject: [NSArray arrayWithObjects: [o_currentColumn identifier], [NSNumber numberWithFloat: [o_currentColumn width]], nil]];
1373     }
1374     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1375     [[NSUserDefaults standardUserDefaults] synchronize];
1376     [o_columns release];
1377     [o_arrayToSave release];
1378 }
1379
1380 @end
1381
1382 @implementation VLCPlaylist (NSOutlineViewDataSource)
1383
1384 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1385 {
1386     id o_value = [super outlineView: outlineView child: index ofItem: item];
1387
1388     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1389     return o_value;
1390 }
1391
1392 /* Required for drag & drop and reordering */
1393 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1394 {
1395     playlist_t *p_playlist = pl_Get(VLCIntf);
1396
1397     /* First remove the items that were moved during the last drag & drop
1398        operation */
1399     [o_items_array removeAllObjects];
1400     [o_nodes_array removeAllObjects];
1401
1402     NSUInteger itemCount = [items count];
1403
1404     for (NSUInteger i = 0 ; i < itemCount ; i++) {
1405         id o_item = [items objectAtIndex: i];
1406
1407         /* Fill the items and nodes to move in 2 different arrays */
1408         if (((playlist_item_t *)[o_item pointerValue])->i_children > 0)
1409             [o_nodes_array addObject: o_item];
1410         else
1411             [o_items_array addObject: o_item];
1412     }
1413
1414     /* Now we need to check if there are selected items that are in already
1415        selected nodes. In that case, we only want to move the nodes */
1416     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1417     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1418
1419     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1420        a Drop operation coming from the playlist. */
1421
1422     [pboard declareTypes: [NSArray arrayWithObjects:
1423         @"VLCPlaylistItemPboardType", nil] owner: self];
1424     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1425
1426     return YES;
1427 }
1428
1429 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1430 {
1431     playlist_t *p_playlist = pl_Get(VLCIntf);
1432     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1433
1434     if (!p_playlist) return NSDragOperationNone;
1435
1436     /* Dropping ON items is not allowed if item is not a node */
1437     if (item) {
1438         if (index == NSOutlineViewDropOnItemIndex &&
1439                 ((playlist_item_t *)[item pointerValue])->i_children == -1) {
1440             return NSDragOperationNone;
1441         }
1442     }
1443
1444     /* We refuse to drop an item in anything else than a child of the General
1445        Node. We still accept items that would be root nodes of the outlineview
1446        however, to allow drop in an empty playlist. */
1447     if (!(([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO locked: NO] ||
1448             (var_CreateGetBool(p_playlist, "media-library") && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO locked: NO])) || item == nil)) {
1449         return NSDragOperationNone;
1450     }
1451
1452     /* Drop from the Playlist */
1453     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1454         NSUInteger count = [o_nodes_array count];
1455         for (NSUInteger i = 0 ; i < count ; i++) {
1456             /* We refuse to Drop in a child of an item we are moving */
1457             if ([self isItem: [item pointerValue] inNode: [[o_nodes_array objectAtIndex: i] pointerValue] checkItemExistence: NO locked:NO]) {
1458                 return NSDragOperationNone;
1459             }
1460         }
1461         return NSDragOperationMove;
1462     }
1463     /* Drop from the Finder */
1464     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1465         return NSDragOperationGeneric;
1466     }
1467     return NSDragOperationNone;
1468 }
1469
1470 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1471 {
1472     playlist_t * p_playlist =  pl_Get(VLCIntf);
1473     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1474
1475     /* Drag & Drop inside the playlist */
1476     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1477         int i_row = 0;
1478         playlist_item_t *p_new_parent, *p_item = NULL;
1479         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1480         /* If the item is to be dropped as root item of the outline, make it a
1481            child of the respective general node, if is either the pl or the ml
1482            Else, choose the proposed parent as parent. */
1483         if (item == nil) {
1484             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1485                 p_new_parent = [self currentPlaylistRoot];
1486             else
1487                 return NO;
1488         }
1489         else
1490             p_new_parent = [item pointerValue];
1491
1492         /* Make sure the proposed parent is a node.
1493            (This should never be true) */
1494         if (p_new_parent->i_children < 0)
1495             return NO;
1496
1497         NSUInteger count = [o_all_items count];
1498         if (count == 0)
1499             return NO;
1500
1501         playlist_item_t **pp_items = (playlist_item_t **)calloc(count, sizeof(playlist_item_t*));
1502         if (!pp_items)
1503             return NO;
1504
1505         PL_LOCK;
1506         NSUInteger j = 0;
1507         for (NSUInteger i = 0; i < count; i++) {
1508             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1509             if (p_item)
1510                 pp_items[j++] = p_item;
1511         }
1512
1513         if (j == 0 || playlist_TreeMoveMany(p_playlist, j, pp_items, p_new_parent, index) != VLC_SUCCESS) {
1514             PL_UNLOCK;
1515             free(pp_items);
1516             return NO;
1517         }
1518
1519         PL_UNLOCK;
1520         free(pp_items);
1521
1522         [self playlistUpdated];
1523         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex: 0] pointerValue]]]];
1524
1525         if (i_row == -1)
1526             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1527
1528         [o_outline_view deselectAll: self];
1529         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1530         [o_outline_view scrollRowToVisible: i_row];
1531
1532         return YES;
1533     }
1534
1535     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1536         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1537             return NO;
1538
1539         playlist_item_t *p_node = [item pointerValue];
1540
1541         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1542                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1543         NSUInteger count = [o_values count];
1544         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1545         input_thread_t * p_input = pl_CurrentInput(VLCIntf);
1546         BOOL b_returned = NO;
1547
1548         if (count == 1 && p_input) {
1549             b_returned = input_AddSubtitle(p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true);
1550             vlc_object_release(p_input);
1551             if (!b_returned)
1552                 return YES;
1553         }
1554         else if (p_input)
1555             vlc_object_release(p_input);
1556
1557         for (NSUInteger i = 0; i < count; i++) {
1558             NSDictionary *o_dic;
1559             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1560             if (!psz_uri)
1561                 continue;
1562
1563             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1564
1565             free(psz_uri);
1566
1567             [o_array addObject: o_dic];
1568         }
1569
1570         if (item == nil)
1571             [self appendArray:o_array atPos:index enqueue: YES];
1572         else {
1573             assert(p_node->i_children != -1);
1574             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1575         }
1576         return YES;
1577     }
1578     return NO;
1579 }
1580
1581 @end