]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: fix l10n issues
[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_interface.h>
54
55 #include <vlc_url.h>
56
57 /*****************************************************************************
58  * VLCPlaylistView implementation
59  *****************************************************************************/
60 @implementation VLCPlaylistView
61
62 - (NSMenu *)menuForEvent:(NSEvent *)o_event
63 {
64     return([(VLCPlaylist *)[self delegate] menuForEvent: o_event]);
65 }
66
67 - (void)keyDown:(NSEvent *)o_event
68 {
69     unichar key = 0;
70
71     if ([[o_event characters] length])
72         key = [[o_event characters] characterAtIndex: 0];
73
74     switch(key) {
75         case NSDeleteCharacter:
76         case NSDeleteFunctionKey:
77         case NSDeleteCharFunctionKey:
78         case NSBackspaceCharacter:
79             [(VLCPlaylist *)[self delegate] deleteItem:self];
80             break;
81
82         case NSEnterCharacter:
83         case NSCarriageReturnCharacter:
84             [(VLCPlaylist *)[[VLCMain sharedInstance] playlist] playItem:nil];
85             break;
86
87         default:
88             [super keyDown: o_event];
89             break;
90     }
91 }
92
93 - (BOOL)validateMenuItem:(NSMenuItem *)item
94 {
95     if (([self numberOfSelectedRows] >= 1 && [item action] == @selector(delete:)) || [item action] == @selector(selectAll:))
96         return YES;
97
98     return NO;
99 }
100
101 - (BOOL) acceptsFirstResponder
102 {
103     return YES;
104 }
105
106 - (BOOL) becomeFirstResponder
107 {
108     [self setNeedsDisplay:YES];
109     return YES;
110 }
111
112 - (BOOL) resignFirstResponder
113 {
114     [self setNeedsDisplay:YES];
115     return YES;
116 }
117
118 - (IBAction)delete:(id)sender
119 {
120     [[[VLCMain sharedInstance] playlist] deleteItem: sender];
121 }
122
123 @end
124
125 /*****************************************************************************
126  * VLCPlaylistCommon implementation
127  *
128  * This class the superclass of the VLCPlaylist and VLCPlaylistWizard.
129  * It contains the common methods and elements of these 2 entities.
130  *****************************************************************************/
131 @implementation VLCPlaylistCommon
132
133 - (id)init
134 {
135     playlist_t * p_playlist = pl_Get(VLCIntf);
136     p_current_root_item = p_playlist->p_local_category;
137
138     self = [super init];
139     if (self != nil)
140         o_outline_dict = [[NSMutableDictionary alloc] init];
141
142     return self;
143 }
144
145 - (void)awakeFromNib
146 {
147     playlist_t * p_playlist = pl_Get(VLCIntf);
148     [o_outline_view setTarget: self];
149     [o_outline_view setDelegate: self];
150     [o_outline_view setDataSource: self];
151     [o_outline_view setAllowsEmptySelection: NO];
152     [o_outline_view expandItem: [o_outline_view itemAtRow:0]];
153
154     [o_outline_view_other setTarget: self];
155     [o_outline_view_other setDelegate: self];
156     [o_outline_view_other setDataSource: self];
157     [o_outline_view_other setAllowsEmptySelection: NO];
158
159     [[o_tc_name_other headerCell] setStringValue:_NS("Name")];
160     [[o_tc_author_other headerCell] setStringValue:_NS("Author")];
161     [[o_tc_duration_other headerCell] setStringValue:_NS("Duration")];
162 }
163
164 - (void)setPlaylistRoot: (playlist_item_t *)root_item
165 {
166     p_current_root_item = root_item;
167     [o_outline_view reloadData];
168     [o_outline_view_other reloadData];
169 }
170
171 - (playlist_item_t *)currentPlaylistRoot
172 {
173     return p_current_root_item;
174 }
175
176 - (NSOutlineView *)outlineView
177 {
178     return o_outline_view;
179 }
180
181 - (playlist_item_t *)selectedPlaylistItem
182 {
183     return [[o_outline_view itemAtRow: [o_outline_view selectedRow]]
184                                                                 pointerValue];
185 }
186
187 @end
188
189 @implementation VLCPlaylistCommon (NSOutlineViewDataSource)
190 /* return the number of children for Obj-C pointer item */ /* DONE */
191 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
192 {
193     int i_return = 0;
194     playlist_item_t *p_item = NULL;
195     playlist_t * p_playlist = pl_Get(VLCIntf);
196     //assert(outlineView == o_outline_view);
197
198     PL_LOCK;
199     if (!item)
200         p_item = p_current_root_item;
201     else
202         p_item = (playlist_item_t *)[item pointerValue];
203
204     if (p_item)
205         i_return = p_item->i_children;
206     PL_UNLOCK;
207
208     return i_return > 0 ? i_return : 0;
209 }
210
211 /* return the child at index for the Obj-C pointer item */ /* DONE */
212 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
213 {
214     playlist_item_t *p_return = NULL, *p_item = NULL;
215     NSValue *o_value;
216     playlist_t * p_playlist = pl_Get(VLCIntf);
217
218     PL_LOCK;
219     if (item == nil)
220         p_item = p_current_root_item; /* root object */
221     else
222         p_item = (playlist_item_t *)[item pointerValue];
223
224     if (p_item && index < p_item->i_children && index >= 0)
225         p_return = p_item->pp_children[index];
226     PL_UNLOCK;
227
228     o_value = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_return]];
229
230     if (o_value == nil) {
231         /* FIXME: Why is there a warning if that happens all the time and seems
232          * to be normal? Add an assert and fix it.
233          * msg_Warn(VLCIntf, "playlist item misses pointer value, adding one"); */
234         o_value = [[NSValue valueWithPointer: p_return] retain];
235     }
236     return o_value;
237 }
238
239 /* is the item expandable */
240 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
241 {
242     int i_return = 0;
243     playlist_t *p_playlist = pl_Get(VLCIntf);
244
245     PL_LOCK;
246     if (item == nil) {
247         /* root object */
248         if (p_current_root_item) {
249             i_return = p_current_root_item->i_children;
250         }
251     } else {
252         playlist_item_t *p_item = (playlist_item_t *)[item pointerValue];
253         if (p_item)
254             i_return = p_item->i_children;
255     }
256     PL_UNLOCK;
257
258     return (i_return >= 0);
259 }
260
261 /* retrieve the string values for the cells */
262 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)o_tc byItem:(id)item
263 {
264     id o_value = nil;
265     char * psz_value;
266     playlist_item_t *p_item;
267
268     /* For error handling */
269     static BOOL attempted_reload = NO;
270
271     if (item == nil || ![item isKindOfClass: [NSValue class]]) {
272         /* Attempt to fix the error by asking for a data redisplay
273          * This might cause infinite loop, so add a small check */
274         if (!attempted_reload) {
275             attempted_reload = YES;
276             [outlineView reloadData];
277         }
278         return @"error" ;
279     }
280
281     p_item = (playlist_item_t *)[item pointerValue];
282     if (!p_item || !p_item->p_input) {
283         /* Attempt to fix the error by asking for a data redisplay
284          * This might cause infinite loop, so add a small check */
285         if (!attempted_reload) {
286             attempted_reload = YES;
287             [outlineView reloadData];
288         }
289         return @"error";
290     }
291
292     attempted_reload = NO;
293     NSString * o_identifier = [o_tc identifier];
294
295     if ([o_identifier isEqualToString:TRACKNUM_COLUMN]) {
296         psz_value = input_item_GetTrackNumber(p_item->p_input);
297         if (psz_value) {
298             o_value = [NSString stringWithUTF8String:psz_value];
299             free(psz_value);
300         }
301     } else if ([o_identifier isEqualToString:TITLE_COLUMN]) {
302         /* sanity check to prevent the NSString class from crashing */
303         char *psz_title =  input_item_GetTitleFbName(p_item->p_input);
304         if (psz_title) {
305             o_value = [NSString stringWithUTF8String:psz_title];
306             free(psz_title);
307         }
308     } else if ([o_identifier isEqualToString:ARTIST_COLUMN]) {
309         psz_value = input_item_GetArtist(p_item->p_input);
310         if (psz_value) {
311             o_value = [NSString stringWithUTF8String:psz_value];
312             free(psz_value);
313         }
314     } else if ([o_identifier isEqualToString:@"duration"]) {
315         char psz_duration[MSTRTIME_MAX_SIZE];
316         mtime_t dur = input_item_GetDuration(p_item->p_input);
317         if (dur != -1) {
318             secstotimestr(psz_duration, dur/1000000);
319             o_value = [NSString stringWithUTF8String:psz_duration];
320         }
321         else
322             o_value = @"--:--";
323     } else if ([o_identifier isEqualToString:GENRE_COLUMN]) {
324         psz_value = input_item_GetGenre(p_item->p_input);
325         if (psz_value) {
326             o_value = [NSString stringWithUTF8String:psz_value];
327             free(psz_value);
328         }
329     } else if ([o_identifier isEqualToString:ALBUM_COLUMN]) {
330         psz_value = input_item_GetAlbum(p_item->p_input);
331         if (psz_value) {
332             o_value = [NSString stringWithUTF8String:psz_value];
333             free(psz_value);
334         }
335     } else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN]) {
336         psz_value = input_item_GetDescription(p_item->p_input);
337         if (psz_value) {
338             o_value = [NSString stringWithUTF8String:psz_value];
339             free(psz_value);
340         }
341     } else if ([o_identifier isEqualToString:DATE_COLUMN]) {
342         psz_value = input_item_GetDate(p_item->p_input);
343         if (psz_value) {
344             o_value = [NSString stringWithUTF8String:psz_value];
345             free(psz_value);
346         }
347     } else if ([o_identifier isEqualToString:LANGUAGE_COLUMN]) {
348         psz_value = input_item_GetLanguage(p_item->p_input);
349         if (psz_value) {
350             o_value = [NSString stringWithUTF8String:psz_value];
351             free(psz_value);
352         }
353     }
354     else if ([o_identifier isEqualToString:URI_COLUMN]) {
355         psz_value = decode_URI(input_item_GetURI(p_item->p_input));
356         if (psz_value) {
357             o_value = [NSString stringWithUTF8String:psz_value];
358             free(psz_value);
359         }
360     }
361     else if ([o_identifier isEqualToString:@"status"]) {
362         if (input_item_HasErrorWhenReading(p_item->p_input)) {
363             o_value = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kAlertCautionIcon)];
364             [o_value setSize: NSMakeSize(16,16)];
365         }
366     }
367
368     return o_value;
369 }
370
371 @end
372
373 /*****************************************************************************
374  * VLCPlaylistWizard implementation
375  *****************************************************************************/
376 @implementation VLCPlaylistWizard
377
378 - (IBAction)reloadOutlineView
379 {
380     /* Only reload the outlineview if the wizard window is open since this can
381        be quite long on big playlists */
382     if ([[o_outline_view window] isVisible])
383         [o_outline_view reloadData];
384 }
385
386 @end
387
388 /*****************************************************************************
389  * An extension to NSOutlineView's interface to fix compilation warnings
390  * and let us access these 2 functions properly.
391  * This uses a private API, but works fine on all current OSX releases.
392  * Radar ID 11739459 request a public API for this. However, it is probably
393  * easier and faster to recreate similar looking bitmaps ourselves.
394  *****************************************************************************/
395
396 @interface NSOutlineView (UndocumentedSortImages)
397 + (NSImage *)_defaultTableHeaderSortImage;
398 + (NSImage *)_defaultTableHeaderReverseSortImage;
399 @end
400
401
402 /*****************************************************************************
403  * VLCPlaylist implementation
404  *****************************************************************************/
405 @interface VLCPlaylist (Internal)
406 - (void)saveTableColumns;
407 @end
408
409 @implementation VLCPlaylist
410
411 + (void)initialize{
412     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
413     NSMutableArray * o_columnArray = [[NSMutableArray alloc] init];
414     [o_columnArray addObject: [NSArray arrayWithObjects:TITLE_COLUMN, [NSNumber numberWithFloat:190.], nil]];
415     [o_columnArray addObject: [NSArray arrayWithObjects:ARTIST_COLUMN, [NSNumber numberWithFloat:95.], nil]];
416     [o_columnArray addObject: [NSArray arrayWithObjects:DURATION_COLUMN, [NSNumber numberWithFloat:95.], nil]];
417     NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:[NSArray arrayWithArray:o_columnArray] forKey: @"PlaylistColumnSelection"];
418
419     [defaults registerDefaults:appDefaults];
420     [o_columnArray release];
421 }
422
423 - (id)init
424 {
425     self = [super init];
426     if (self != nil) {
427         o_nodes_array = [[NSMutableArray alloc] init];
428         o_items_array = [[NSMutableArray alloc] init];
429     }
430     return self;
431 }
432
433 - (void)dealloc
434 {
435     [o_nodes_array release];
436     [o_items_array release];
437     [super dealloc];
438 }
439
440 - (void)awakeFromNib
441 {
442     playlist_t * p_playlist = pl_Get(VLCIntf);
443
444     [super awakeFromNib];
445     [self initStrings];
446
447     [o_outline_view setDoubleAction: @selector(playItem:)];
448     [o_outline_view_other setDoubleAction: @selector(playItem:)];
449
450     [o_outline_view registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
451     [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
452
453     [o_outline_view_other registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"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:p_item->pp_children[i_current]->p_input->psz_name];
1116         psz_temp = input_item_GetInfo(p_item->p_input, _("Meta-information"),_("Artist"));
1117         o_current_author = [NSString stringWithUTF8String:psz_temp];
1118         free(psz_temp);
1119         PL_UNLOCK;
1120
1121         if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == NO)
1122             b_selected_item_met = YES;
1123         else if (p_selected_item == p_item->pp_children[i_current] && b_selected_item_met == YES)
1124             return NULL;
1125         else if (b_selected_item_met == YES &&
1126                     ([o_current_name rangeOfString:[o_search_field
1127                         stringValue] options:NSCaseInsensitiveSearch].length ||
1128                       [o_current_author rangeOfString:[o_search_field
1129                         stringValue] options:NSCaseInsensitiveSearch].length))
1130             /*Adds the parent items in the result array as well, so that we can
1131             expand the tree*/
1132             return [NSMutableArray arrayWithObject: [NSValue valueWithPointer: p_item->pp_children[i_current]]];
1133
1134         if (p_item->pp_children[i_current]->i_children > 0) {
1135             id o_result = [self subSearchItem:
1136                                             p_item->pp_children[i_current]];
1137             if (o_result != NULL) {
1138                 [o_result insertObject: [NSValue valueWithPointer:
1139                                 p_item->pp_children[i_current]] atIndex:0];
1140                 return o_result;
1141             }
1142         }
1143     }
1144     return NULL;
1145 }
1146
1147 - (IBAction)searchItem:(id)sender
1148 {
1149     playlist_t * p_playlist = pl_Get(VLCIntf);
1150     id o_result;
1151
1152     int i_row = -1;
1153
1154     b_selected_item_met = NO;
1155
1156     /* First, only search after the selected item:
1157      * (b_selected_item_met = NO) */
1158     o_result = [self subSearchItem:[self currentPlaylistRoot]];
1159     if (o_result == NULL)
1160         /* If the first search failed, search again from the beginning */
1161         o_result = [self subSearchItem:[self currentPlaylistRoot]];
1162
1163     if (o_result != NULL) {
1164         int i_start;
1165         if ([[o_result objectAtIndex:0] pointerValue] == p_playlist->p_local_category)
1166             i_start = 1;
1167         else
1168             i_start = 0;
1169         NSUInteger count = [o_result count];
1170
1171         for (NSUInteger i = i_start ; i < count - 1 ; i++) {
1172             [o_outline_view expandItem: [o_outline_dict objectForKey:
1173                         [NSString stringWithFormat: @"%p",
1174                         [[o_result objectAtIndex:i] pointerValue]]]];
1175         }
1176         i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
1177                         [NSString stringWithFormat: @"%p",
1178                         [[o_result objectAtIndex:count - 1 ]
1179                         pointerValue]]]];
1180     }
1181     if (i_row > -1) {
1182         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1183         [o_outline_view scrollRowToVisible: i_row];
1184     }
1185 }
1186
1187 - (IBAction)recursiveExpandNode:(id)sender
1188 {
1189     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
1190     NSUInteger count = [selectedRows count];
1191     NSUInteger indexes[count];
1192     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1193
1194     id o_item;
1195     playlist_item_t *p_item;
1196     for (NSUInteger i = 0; i < count; i++) {
1197         o_item = [o_outline_view itemAtRow: indexes[i]];
1198         p_item = (playlist_item_t *)[o_item pointerValue];
1199
1200         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
1201             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
1202
1203         /* We need to collapse the node first, since OSX refuses to recursively
1204          expand an already expanded node, even if children nodes are collapsed. */
1205         [o_outline_view collapseItem: o_item collapseChildren: YES];
1206         [o_outline_view expandItem: o_item expandChildren: YES];
1207
1208         selectedRows = [o_outline_view selectedRowIndexes];
1209         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
1210     }
1211 }
1212
1213 - (NSMenu *)menuForEvent:(NSEvent *)o_event
1214 {
1215     NSPoint pt;
1216     bool b_rows;
1217     bool b_item_sel;
1218
1219     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
1220     int row = [o_outline_view rowAtPoint:pt];
1221     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
1222         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
1223
1224     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
1225     b_rows = [o_outline_view numberOfRows] != 0;
1226
1227     [o_mi_play setEnabled: b_item_sel];
1228     [o_mi_delete setEnabled: b_item_sel];
1229     [o_mi_selectall setEnabled: b_rows];
1230     [o_mi_info setEnabled: b_item_sel];
1231     [o_mi_preparse setEnabled: b_item_sel];
1232     [o_mi_recursive_expand setEnabled: b_item_sel];
1233     [o_mi_sort_name setEnabled: b_item_sel];
1234     [o_mi_sort_author setEnabled: b_item_sel];
1235
1236     return(o_ctx_menu);
1237 }
1238
1239 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
1240 {
1241     int i_mode, i_type = 0;
1242     intf_thread_t *p_intf = VLCIntf;
1243     NSString * o_identifier = [o_tc identifier];
1244
1245     playlist_t *p_playlist = pl_Get(p_intf);
1246
1247     if ([o_identifier isEqualToString:TRACKNUM_COLUMN])
1248         i_mode = SORT_TRACK_NUMBER;
1249     else if ([o_identifier isEqualToString:TITLE_COLUMN])
1250         i_mode = SORT_TITLE;
1251     else if ([o_identifier isEqualToString:ARTIST_COLUMN])
1252         i_mode = SORT_ARTIST;
1253     else if ([o_identifier isEqualToString:GENRE_COLUMN])
1254         i_mode = SORT_GENRE;
1255     else if ([o_identifier isEqualToString:DURATION_COLUMN])
1256         i_mode = SORT_DURATION;
1257     else if ([o_identifier isEqualToString:ALBUM_COLUMN])
1258         i_mode = SORT_ALBUM;
1259     else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN])
1260         i_mode = SORT_DESCRIPTION;
1261     else if ([o_identifier isEqualToString:URI_COLUMN])
1262         i_mode = SORT_URI;
1263     else
1264         return;
1265
1266     if (o_tc_sortColumn == o_tc)
1267         b_isSortDescending = !b_isSortDescending;
1268     else
1269         b_isSortDescending = false;
1270
1271     if (b_isSortDescending)
1272         i_type = ORDER_REVERSE;
1273     else
1274         i_type = ORDER_NORMAL;
1275
1276     PL_LOCK;
1277     playlist_RecursiveNodeSort(p_playlist, [self currentPlaylistRoot], i_mode, i_type);
1278     PL_UNLOCK;
1279
1280     [self playlistUpdated];
1281
1282     o_tc_sortColumn = o_tc;
1283     [o_outline_view setHighlightedTableColumn:o_tc];
1284
1285     if (b_isSortDescending)
1286         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
1287     else
1288         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
1289 }
1290
1291
1292 - (void)outlineView:(NSOutlineView *)outlineView
1293                                 willDisplayCell:(id)cell
1294                                 forTableColumn:(NSTableColumn *)tableColumn
1295                                 item:(id)item
1296 {
1297     /* this method can be called when VLC is already dead, hence the extra checks */
1298     intf_thread_t * p_intf = VLCIntf;
1299     if (!p_intf)
1300         return;
1301     playlist_t *p_playlist = pl_Get(p_intf);
1302     if (!p_playlist)
1303         return;
1304
1305     id o_playing_item;
1306
1307     PL_LOCK;
1308     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1309     PL_UNLOCK;
1310
1311     if ([self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
1312                         || [o_playing_item isEqual: item])
1313         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toHaveTrait:NSBoldFontMask]];
1314     else
1315         [cell setFont: [[NSFontManager sharedFontManager] convertFont:[cell font] toNotHaveTrait:NSBoldFontMask]];
1316 }
1317
1318 - (id)playingItem
1319 {
1320     playlist_t *p_playlist = pl_Get(VLCIntf);
1321
1322     id o_playing_item;
1323
1324     PL_LOCK;
1325     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1326     PL_UNLOCK;
1327
1328     return o_playing_item;
1329 }
1330
1331 - (NSArray *)draggedItems
1332 {
1333     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1334 }
1335
1336 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1337 {
1338     NSTableColumn * o_work_tc;
1339
1340     if (i_state == NSOnState) {
1341         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1342         [o_work_tc setEditable: NO];
1343         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1344
1345         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1346
1347         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1348             [o_work_tc setWidth: 20.];
1349             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1350             [[o_work_tc headerCell] setStringValue: @"#"];
1351         }
1352
1353         [o_outline_view addTableColumn: o_work_tc];
1354         [o_work_tc release];
1355         [o_outline_view reloadData];
1356         [o_outline_view setNeedsDisplay: YES];
1357     }
1358     else
1359         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1360
1361     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_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 arrayWithObject:@"VLCPlaylistItemPboardType"] owner: self];
1423     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1424
1425     return YES;
1426 }
1427
1428 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1429 {
1430     playlist_t *p_playlist = pl_Get(VLCIntf);
1431     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1432
1433     if (!p_playlist) return NSDragOperationNone;
1434
1435     /* Dropping ON items is not allowed if item is not a node */
1436     if (item) {
1437         if (index == NSOutlineViewDropOnItemIndex &&
1438                 ((playlist_item_t *)[item pointerValue])->i_children == -1) {
1439             return NSDragOperationNone;
1440         }
1441     }
1442
1443     /* We refuse to drop an item in anything else than a child of the General
1444        Node. We still accept items that would be root nodes of the outlineview
1445        however, to allow drop in an empty playlist. */
1446     if (!(([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO locked: NO] ||
1447             (var_CreateGetBool(p_playlist, "media-library") && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO locked: NO])) || item == nil)) {
1448         return NSDragOperationNone;
1449     }
1450
1451     /* Drop from the Playlist */
1452     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1453         NSUInteger count = [o_nodes_array count];
1454         for (NSUInteger i = 0 ; i < count ; i++) {
1455             /* We refuse to Drop in a child of an item we are moving */
1456             if ([self isItem: [item pointerValue] inNode: [[o_nodes_array objectAtIndex:i] pointerValue] checkItemExistence: NO locked:NO]) {
1457                 return NSDragOperationNone;
1458             }
1459         }
1460         return NSDragOperationMove;
1461     }
1462     /* Drop from the Finder */
1463     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1464         return NSDragOperationGeneric;
1465     }
1466     return NSDragOperationNone;
1467 }
1468
1469 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1470 {
1471     playlist_t * p_playlist =  pl_Get(VLCIntf);
1472     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1473
1474     /* Drag & Drop inside the playlist */
1475     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1476         if (index == -1) // this is no valid target, sanitize to top of table
1477             index = 0;
1478
1479         int i_row = 0;
1480         playlist_item_t *p_new_parent, *p_item = NULL;
1481         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1482         /* If the item is to be dropped as root item of the outline, make it a
1483            child of the respective general node, if is either the pl or the ml
1484            Else, choose the proposed parent as parent. */
1485         if (item == nil) {
1486             if ([self currentPlaylistRoot] == p_playlist->p_local_category || [self currentPlaylistRoot] == p_playlist->p_ml_category)
1487                 p_new_parent = [self currentPlaylistRoot];
1488             else
1489                 return NO;
1490         }
1491         else
1492             p_new_parent = [item pointerValue];
1493
1494         /* Make sure the proposed parent is a node.
1495            (This should never be true) */
1496         if (p_new_parent->i_children < 0)
1497             return NO;
1498
1499         NSUInteger count = [o_all_items count];
1500         if (count == 0)
1501             return NO;
1502
1503         playlist_item_t **pp_items = (playlist_item_t **)calloc(count, sizeof(playlist_item_t*));
1504         if (!pp_items)
1505             return NO;
1506
1507         PL_LOCK;
1508         NSUInteger j = 0;
1509         for (NSUInteger i = 0; i < count; i++) {
1510             p_item = [[o_all_items objectAtIndex:i] pointerValue];
1511             if (p_item)
1512                 pp_items[j++] = p_item;
1513         }
1514
1515         if (j == 0 || playlist_TreeMoveMany(p_playlist, j, pp_items, p_new_parent, index) != VLC_SUCCESS) {
1516             PL_UNLOCK;
1517             free(pp_items);
1518             return NO;
1519         }
1520
1521         PL_UNLOCK;
1522         free(pp_items);
1523
1524         [self playlistUpdated];
1525         i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", [[o_all_items objectAtIndex:0] pointerValue]]]];
1526
1527         if (i_row == -1)
1528             i_row = [o_outline_view rowForItem:[o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_new_parent]]];
1529
1530         [o_outline_view deselectAll: self];
1531         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1532         [o_outline_view scrollRowToVisible: i_row];
1533
1534         return YES;
1535     }
1536
1537     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1538         if ([self currentPlaylistRoot] != p_playlist->p_local_category && [self currentPlaylistRoot] != p_playlist->p_ml_category)
1539             return NO;
1540
1541         playlist_item_t *p_node = [item pointerValue];
1542
1543         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1544                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1545         NSUInteger count = [o_values count];
1546         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1547         input_thread_t * p_input = pl_CurrentInput(VLCIntf);
1548         BOOL b_returned = NO;
1549
1550         if (count == 1 && p_input) {
1551             b_returned = input_AddSubtitle(p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true);
1552             vlc_object_release(p_input);
1553             if (!b_returned)
1554                 return YES;
1555         }
1556         else if (p_input)
1557             vlc_object_release(p_input);
1558
1559         for (NSUInteger i = 0; i < count; i++) {
1560             NSDictionary *o_dic;
1561             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1562             if (!psz_uri)
1563                 continue;
1564
1565             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1566
1567             free(psz_uri);
1568
1569             [o_array addObject: o_dic];
1570         }
1571
1572         if (item == nil)
1573             [self appendArray:o_array atPos:index enqueue: YES];
1574         else {
1575             assert(p_node->i_children != -1);
1576             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1577         }
1578         return YES;
1579     }
1580     return NO;
1581 }
1582
1583 @end