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