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