]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: Move drag and drop support to new PLModel and rewrite
[vlc] / modules / gui / macosx / playlist.m
1 /*****************************************************************************
2  * playlist.m: MacOS X interface module
3  *****************************************************************************
4 * Copyright (C) 2002-2014 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 "CompatibilityFixes.h"
43
44 #import "intf.h"
45 #import "wizard.h"
46 #import "bookmarks.h"
47 #import "playlistinfo.h"
48 #import "playlist.h"
49 #import "controls.h"
50 #import "misc.h"
51 #import "open.h"
52 #import "MainMenu.h"
53 #import "CoreInteraction.h"
54
55 #include <vlc_keys.h>
56 #import <vlc_interface.h>
57 #include <vlc_url.h>
58
59 /*****************************************************************************
60  * VLCPlaylistView implementation
61  *****************************************************************************/
62 @implementation VLCPlaylistView
63
64 - (NSMenu *)menuForEvent:(NSEvent *)o_event
65 {
66     return([(VLCPlaylist *)[self delegate] menuForEvent: o_event]);
67 }
68
69 - (void)keyDown:(NSEvent *)o_event
70 {
71     unichar key = 0;
72
73     if ([[o_event characters] length])
74         key = [[o_event characters] characterAtIndex: 0];
75
76     switch(key) {
77         case NSDeleteCharacter:
78         case NSDeleteFunctionKey:
79         case NSDeleteCharFunctionKey:
80         case NSBackspaceCharacter:
81             [(VLCPlaylist *)[self delegate] deleteItem:self];
82             break;
83
84         case NSEnterCharacter:
85         case NSCarriageReturnCharacter:
86             [(VLCPlaylist *)[[VLCMain sharedInstance] playlist] playItem:nil];
87             break;
88
89         default:
90             [super keyDown: o_event];
91             break;
92     }
93 }
94
95 - (BOOL)validateMenuItem:(NSMenuItem *)item
96 {
97     if (([self numberOfSelectedRows] >= 1 && [item action] == @selector(delete:)) || [item action] == @selector(selectAll:))
98         return YES;
99
100     return NO;
101 }
102
103 - (BOOL)acceptsFirstResponder
104 {
105     return YES;
106 }
107
108 - (BOOL)becomeFirstResponder
109 {
110     [self setNeedsDisplay:YES];
111     return YES;
112 }
113
114 - (BOOL)resignFirstResponder
115 {
116     [self setNeedsDisplay:YES];
117     return YES;
118 }
119
120 - (IBAction)delete:(id)sender
121 {
122     [[[VLCMain sharedInstance] playlist] deleteItem: sender];
123 }
124
125 @end
126
127 /*****************************************************************************
128  * VLCPlaylistWizard implementation
129  *****************************************************************************/
130 @implementation VLCPlaylistWizard
131
132 - (IBAction)reloadOutlineView
133 {
134     /* Only reload the outlineview if the wizard window is open since this can
135        be quite long on big playlists */
136
137
138     // to be removed
139 }
140
141 @end
142
143 /*****************************************************************************
144  * An extension to NSOutlineView's interface to fix compilation warnings
145  * and let us access these 2 functions properly.
146  * This uses a private API, but works fine on all current OSX releases.
147  * Radar ID 11739459 request a public API for this. However, it is probably
148  * easier and faster to recreate similar looking bitmaps ourselves.
149  *****************************************************************************/
150
151 @interface NSOutlineView (UndocumentedSortImages)
152 + (NSImage *)_defaultTableHeaderSortImage;
153 + (NSImage *)_defaultTableHeaderReverseSortImage;
154 @end
155
156
157 /*****************************************************************************
158  * VLCPlaylist implementation
159  *****************************************************************************/
160 @interface VLCPlaylist ()
161 {
162     playlist_item_t * p_current_root_item;
163
164     NSImage *o_descendingSortingImage;
165     NSImage *o_ascendingSortingImage;
166
167     BOOL b_selected_item_met;
168     BOOL b_isSortDescending;
169     id o_tc_sortColumn;
170     NSUInteger retainedRowSelection;
171
172     BOOL b_playlistmenu_nib_loaded;
173     BOOL b_view_setup;
174 }
175
176 - (void)saveTableColumns;
177 @end
178
179 @implementation VLCPlaylist
180
181 + (void)initialize
182 {
183     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
184     NSMutableArray *o_columnArray = [[NSMutableArray alloc] init];
185     [o_columnArray addObject: [NSArray arrayWithObjects:TITLE_COLUMN, [NSNumber numberWithFloat:190.], nil]];
186     [o_columnArray addObject: [NSArray arrayWithObjects:ARTIST_COLUMN, [NSNumber numberWithFloat:95.], nil]];
187     [o_columnArray addObject: [NSArray arrayWithObjects:DURATION_COLUMN, [NSNumber numberWithFloat:95.], nil]];
188
189     NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
190                                  [NSArray arrayWithArray:o_columnArray], @"PlaylistColumnSelection",
191                                  [NSArray array], @"recentlyPlayedMediaList",
192                                  [NSDictionary dictionary], @"recentlyPlayedMedia", nil];
193
194     [defaults registerDefaults:appDefaults];
195     [o_columnArray release];
196 }
197
198 - (playlist_item_t *)currentPlaylistRoot
199 {
200     // TODO remove
201     playlist_t *p_playlist = pl_Get(VLCIntf);
202     return p_playlist->p_playing;
203 }
204
205 - (PLModel *)model
206 {
207     return o_model;
208 }
209
210 - (void)reloadStyles
211 {
212     NSFont *fontToUse;
213     CGFloat rowHeight;
214     if (config_GetInt(VLCIntf, "macosx-large-text")) {
215         fontToUse = [NSFont systemFontOfSize:13.];
216         rowHeight = 21.;
217     } else {
218         fontToUse = [NSFont systemFontOfSize:11.];
219         rowHeight = 16.;
220     }
221
222     NSArray *columns = [o_outline_view tableColumns];
223     NSUInteger count = columns.count;
224     for (NSUInteger x = 0; x < count; x++)
225         [[[columns objectAtIndex:x] dataCell] setFont:fontToUse];
226     [o_outline_view setRowHeight:rowHeight];
227 }
228
229 - (id)init
230 {
231     self = [super init];
232     if (self != nil) {
233
234
235         playlist_t * p_playlist = pl_Get(VLCIntf);
236         p_current_root_item = p_playlist->p_local_category;
237         o_outline_dict = [[NSMutableDictionary alloc] init];
238     }
239     return self;
240 }
241
242 - (void)dealloc
243 {
244     [o_outline_dict release];
245     [super dealloc];
246 }
247
248 - (void)awakeFromNib
249 {
250     if (b_view_setup)
251         return;
252
253     playlist_t * p_playlist = pl_Get(VLCIntf);
254     [o_outline_view setTarget: self];
255     [o_outline_view setDelegate: self];
256     [o_outline_view setAllowsEmptySelection: NO];
257     [o_outline_view expandItem: [o_outline_view itemAtRow:0]];
258
259     [self reloadStyles];
260     [self initStrings];
261
262     o_model = [[PLModel alloc] initWithOutlineView:o_outline_view playlist:p_playlist rootItem:p_current_root_item playlistObject:self];
263     [o_outline_view setDataSource:o_model];
264     [o_outline_view reloadData];
265
266     [o_outline_view setDoubleAction: @selector(playItem:)];
267
268     [o_outline_view registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
269     [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
270
271     /* This uses a private API, but works fine on all current OSX releases.
272      * Radar ID 11739459 request a public API for this. However, it is probably
273      * easier and faster to recreate similar looking bitmaps ourselves. */
274     o_ascendingSortingImage = [[NSOutlineView class] _defaultTableHeaderSortImage];
275     o_descendingSortingImage = [[NSOutlineView class] _defaultTableHeaderReverseSortImage];
276
277     o_tc_sortColumn = nil;
278
279     NSArray * o_columnArray = [[NSUserDefaults standardUserDefaults] arrayForKey:@"PlaylistColumnSelection"];
280     NSUInteger count = [o_columnArray count];
281
282     id o_menu = [[VLCMain sharedInstance] mainMenu];
283     NSString * o_column;
284
285     NSMenu *o_context_menu = [o_menu setupPlaylistTableColumnsMenu];
286     [o_playlist_header setMenu: o_context_menu];
287
288     for (NSUInteger i = 0; i < count; i++) {
289         o_column = [[o_columnArray objectAtIndex:i] objectAtIndex:0];
290         if ([o_column isEqualToString:@"status"])
291             continue;
292
293         if(![o_menu setPlaylistColumnTableState: NSOnState forColumn: o_column])
294             continue;
295
296         [[o_outline_view tableColumnWithIdentifier: o_column] setWidth: [[[o_columnArray objectAtIndex:i] objectAtIndex:1] floatValue]];
297     }
298
299     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];
300
301     b_view_setup = YES;
302 }
303
304 - (void)applicationWillTerminate:(NSNotification *)notification
305 {
306     /* 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 */
307     [self saveTableColumns];
308 }
309
310 - (void)initStrings
311 {
312     [o_mi_play setTitle: _NS("Play")];
313     [o_mi_delete setTitle: _NS("Delete")];
314     [o_mi_recursive_expand setTitle: _NS("Expand Node")];
315     [o_mi_selectall setTitle: _NS("Select All")];
316     [o_mi_info setTitle: _NS("Media Information...")];
317     [o_mi_dl_cover_art setTitle: _NS("Download Cover Art")];
318     [o_mi_preparse setTitle: _NS("Fetch Meta Data")];
319     [o_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
320     [o_mm_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
321     [[o_mm_mi_revealInFinder menu] setAutoenablesItems: NO];
322     [o_mi_sort_name setTitle: _NS("Sort Node by Name")];
323     [o_mi_sort_author setTitle: _NS("Sort Node by Author")];
324
325     [o_search_field setToolTip: _NS("Search in Playlist")];
326 }
327
328 - (void)playlistUpdated
329 {
330     /* Clear indications of any existing column sorting */
331     NSUInteger count = [[o_outline_view tableColumns] count];
332     for (NSUInteger i = 0 ; i < count ; i++)
333         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
334
335     [o_outline_view setHighlightedTableColumn:nil];
336     o_tc_sortColumn = nil;
337     // TODO Find a way to keep the dict size to a minimum
338     //[o_outline_dict removeAllObjects];
339     [o_outline_view reloadData];
340     [[[[VLCMain sharedInstance] wizard] playlistWizard] reloadOutlineView];
341
342     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:retainedRowSelection] byExtendingSelection:NO];
343
344     [self outlineViewSelectionDidChange: nil];
345     [[VLCMain sharedInstance] updateMainWindow];
346 }
347
348 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
349 {
350 //    // FIXME: unsafe
351 //    playlist_item_t * p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
352 //
353 //    if (p_item) {
354 //        /* update the state of our Reveal-in-Finder menu items */
355 //        NSMutableString *o_mrl;
356 //        char *psz_uri = input_item_GetURI(p_item->p_input);
357 //
358 //        [o_mi_revealInFinder setEnabled: NO];
359 //        [o_mm_mi_revealInFinder setEnabled: NO];
360 //        if (psz_uri) {
361 //            o_mrl = [NSMutableString stringWithUTF8String: psz_uri];
362 //
363 //            /* perform some checks whether it is a file and if it is local at all... */
364 //            NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
365 //            if (prefix_range.location != NSNotFound)
366 //                [o_mrl deleteCharactersInRange: prefix_range];
367 //
368 //            if ([o_mrl characterAtIndex:0] == '/') {
369 //                [o_mi_revealInFinder setEnabled: YES];
370 //                [o_mm_mi_revealInFinder setEnabled: YES];
371 //            }
372 //            free(psz_uri);
373 //        }
374 //
375 //        /* update our info-panel to reflect the new item */
376 //        [[[VLCMain sharedInstance] info] updatePanelWithItem:p_item->p_input];
377 //    }
378 }
379
380 - (BOOL)isSelectionEmpty
381 {
382     return [o_outline_view selectedRow] == -1;
383 }
384
385 - (void)updateRowSelection
386 {
387     // FIXME: unsafe
388     playlist_t *p_playlist = pl_Get(VLCIntf);
389     playlist_item_t *p_item, *p_temp_item;
390     NSMutableArray *o_array = [NSMutableArray array];
391
392     PL_LOCK;
393     p_item = playlist_CurrentPlayingItem(p_playlist);
394     if (p_item == NULL) {
395         PL_UNLOCK;
396         return;
397     }
398
399     p_temp_item = p_item;
400     while(p_temp_item->p_parent) {
401         [o_array insertObject: [NSValue valueWithPointer: p_temp_item] atIndex: 0];
402         p_temp_item = p_temp_item->p_parent;
403     }
404     PL_UNLOCK;
405
406     NSUInteger count = [o_array count];
407     for (NSUInteger j = 0; j < count - 1; j++) {
408         id o_item;
409         if ((o_item = [o_outline_dict objectForKey:
410                             [NSString stringWithFormat: @"%p",
411                             [[o_array objectAtIndex:j] pointerValue]]]) != nil) {
412             [o_outline_view expandItem: o_item];
413         }
414     }
415
416     id o_item = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_item]];
417     NSInteger i_index = [o_outline_view rowForItem:o_item];
418     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_index] byExtendingSelection:NO];
419     [o_outline_view setNeedsDisplay:YES];
420 }
421
422 /* Check if p_item is a child of p_node recursively. We need to check the item
423    existence first since OSX sometimes tries to redraw items that have been
424    deleted. We don't do it when not required since this verification takes
425    quite a long time on big playlists (yes, pretty hacky). */
426
427 // todo remove useless parameters
428 - (BOOL)isItem: (PLItem *)p_item inNode: (PLItem *)p_node checkItemExistence:(BOOL)b_check locked:(BOOL)b_locked
429 {
430     PLItem *p_temp_item = p_item;
431
432     if ([p_node plItemId] == [p_item plItemId])
433         return YES;
434
435     while(p_temp_item) {
436         p_temp_item = [p_temp_item parent];
437         if ([p_temp_item plItemId] == [p_node plItemId]) {
438             return YES;
439         }
440     }
441
442     return NO;
443 }
444
445 - (IBAction)savePlaylist:(id)sender
446 {
447     playlist_t * p_playlist = pl_Get(VLCIntf);
448
449     NSSavePanel *o_save_panel = [NSSavePanel savePanel];
450     NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
451
452     [NSBundle loadNibNamed:@"PlaylistAccessoryView" owner:self];
453
454     [o_save_accessory_text setStringValue: _NS("File Format:")];
455     [[o_save_accessory_popup itemAtIndex:0] setTitle: _NS("Extended M3U")];
456     [[o_save_accessory_popup itemAtIndex:1] setTitle: _NS("XML Shareable Playlist Format (XSPF)")];
457     [[o_save_accessory_popup itemAtIndex:2] setTitle: _NS("HTML playlist")];
458
459     [o_save_panel setTitle: _NS("Save Playlist")];
460     [o_save_panel setPrompt: _NS("Save")];
461     [o_save_panel setAccessoryView: o_save_accessory_view];
462     [o_save_panel setNameFieldStringValue: o_name];
463
464     if ([o_save_panel runModal] == NSFileHandlingPanelOKButton) {
465         NSString *o_filename = [[o_save_panel URL] path];
466
467         if ([o_save_accessory_popup indexOfSelectedItem] == 0) {
468             NSString * o_real_filename;
469             NSRange range;
470             range.location = [o_filename length] - [@".m3u" length];
471             range.length = [@".m3u" length];
472
473             if ([o_filename compare:@".m3u" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
474                 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
475             else
476                 o_real_filename = o_filename;
477
478             playlist_Export(p_playlist,
479                 [o_real_filename fileSystemRepresentation],
480                 p_playlist->p_local_category, "export-m3u");
481         } else if ([o_save_accessory_popup indexOfSelectedItem] == 1) {
482             NSString * o_real_filename;
483             NSRange range;
484             range.location = [o_filename length] - [@".xspf" length];
485             range.length = [@".xspf" length];
486
487             if ([o_filename compare:@".xspf" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
488                 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
489             else
490                 o_real_filename = o_filename;
491
492             playlist_Export(p_playlist,
493                 [o_real_filename fileSystemRepresentation],
494                 p_playlist->p_local_category, "export-xspf");
495         } else {
496             NSString * o_real_filename;
497             NSRange range;
498             range.location = [o_filename length] - [@".html" length];
499             range.length = [@".html" length];
500
501             if ([o_filename compare:@".html" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
502                 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
503             else
504                 o_real_filename = o_filename;
505
506             playlist_Export(p_playlist,
507                 [o_real_filename fileSystemRepresentation],
508                 p_playlist->p_local_category, "export-html");
509         }
510     }
511 }
512
513 /* When called retrieves the selected outlineview row and plays that node or item */
514 - (IBAction)playItem:(id)sender
515 {
516     intf_thread_t * p_intf = VLCIntf;
517     playlist_t * p_playlist = pl_Get(p_intf);
518
519     playlist_item_t *p_item;
520     playlist_item_t *p_node = NULL;
521
522     // ignore clicks on column header when handling double action
523     if (sender != nil && [o_outline_view clickedRow] == -1 && sender != o_mi_play)
524         return;
525
526     PL_LOCK;
527     PLItem *o_item = [o_outline_view itemAtRow:[o_outline_view selectedRow]];
528     p_item = playlist_ItemGetById(p_playlist, [o_item plItemId]);
529
530     if (p_item) {
531         if (p_item->i_children == -1) {
532             p_node = p_item->p_parent;
533         } else {
534             p_node = p_item;
535             if (p_node->i_children > 0 && p_node->pp_children[0]->i_children == -1)
536                 p_item = p_node->pp_children[0];
537             else
538                 p_item = NULL;
539         }
540
541         playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
542     }
543     PL_UNLOCK;
544 }
545
546 - (IBAction)revealItemInFinder:(id)sender
547 {
548     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
549     NSUInteger count = [selectedRows count];
550     NSUInteger indexes[count];
551     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
552
553     NSMutableString * o_mrl;
554     for (NSUInteger i = 0; i < count; i++) {
555         PLItem *o_item = [o_outline_view itemAtRow:indexes[i]];
556
557         char * psz_url = decode_URI(input_item_GetURI([o_item input]));
558         o_mrl = [[NSMutableString alloc] initWithString: [NSString stringWithUTF8String:psz_url ? psz_url : ""]];
559         if (psz_url != NULL)
560             free( psz_url );
561
562         /* perform some checks whether it is a file and if it is local at all... */
563         if ([o_mrl length] > 0) {
564             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
565             if (prefix_range.location != NSNotFound)
566                 [o_mrl deleteCharactersInRange: prefix_range];
567
568             if ([o_mrl characterAtIndex:0] == '/')
569                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
570         }
571
572         [o_mrl release];
573     }
574 }
575
576 /* When called retrieves the selected outlineview row and plays that node or item */
577 - (IBAction)preparseItem:(id)sender
578 {
579     int i_count;
580     NSIndexSet *o_selected_indexes;
581     intf_thread_t * p_intf = VLCIntf;
582     playlist_t * p_playlist = pl_Get(p_intf);
583     playlist_item_t *p_item = NULL;
584
585     o_selected_indexes = [o_outline_view selectedRowIndexes];
586     i_count = [o_selected_indexes count];
587
588     NSUInteger indexes[i_count];
589     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
590     for (int i = 0; i < i_count; i++) {
591         PLItem *o_item = [o_outline_view itemAtRow:indexes[i]];
592         [o_outline_view deselectRow: indexes[i]];
593
594         if (![o_item isLeaf]) {
595             msg_Dbg(p_intf, "preparsing nodes not implemented");
596             continue;
597         }
598
599         libvlc_MetaRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
600
601     }
602     [self playlistUpdated];
603 }
604
605 - (IBAction)downloadCoverArt:(id)sender
606 {
607     int i_count;
608     NSIndexSet *o_selected_indexes;
609     intf_thread_t * p_intf = VLCIntf;
610     playlist_t * p_playlist = pl_Get(p_intf);
611     playlist_item_t *p_item = NULL;
612
613     o_selected_indexes = [o_outline_view selectedRowIndexes];
614     i_count = [o_selected_indexes count];
615
616     NSUInteger indexes[i_count];
617     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
618     for (int i = 0; i < i_count; i++) {
619         PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
620
621         if (![o_item isLeaf])
622             continue;
623
624         libvlc_ArtRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
625     }
626     [self playlistUpdated];
627 }
628
629 - (IBAction)selectAll:(id)sender
630 {
631     [o_outline_view selectAll: nil];
632 }
633
634 - (IBAction)showInfoPanel:(id)sender
635 {
636     [[[VLCMain sharedInstance] info] initPanel];
637 }
638
639 - (IBAction)deleteItem:(id)sender
640 {
641     int i_count;
642     NSIndexSet *o_selected_indexes;
643     intf_thread_t * p_intf = VLCIntf;
644     playlist_t * p_playlist = pl_Get(p_intf);
645
646     // check if deletion is allowed
647     if (![[self model] editAllowed])
648         return;
649
650     o_selected_indexes = [o_outline_view selectedRowIndexes];
651     i_count = [o_selected_indexes count];
652     retainedRowSelection = [o_selected_indexes firstIndex];
653     if (retainedRowSelection == NSNotFound)
654         retainedRowSelection = 0;
655
656
657     NSUInteger indexes[i_count];
658 //    if (i_count == [o_outline_view numberOfRows]) {
659 //        PL_LOCK;
660 //        playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
661 //        PL_UNLOCK;
662 //        [self playlistUpdated];
663 //        return;
664 //    }
665     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
666     for (int i = 0; i < i_count; i++) {
667         PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
668         [o_outline_view deselectRow: indexes[i]];
669
670         /// TODO
671 //        if (p_item->i_children != -1) {
672 //        //is a node and not an item
673 //            if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
674 //                [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
675 //                        checkItemExistence: NO locked:YES] == YES)
676 //                // if current item is in selected node and is playing then stop playlist
677 //                playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
678 //
679 //                playlist_NodeDelete(p_playlist, p_item, true, false);
680 //        } else
681
682             playlist_DeleteFromInput(p_playlist, [o_item input], pl_Unlocked);
683 //        [[o_item parent] deleteChild:o_item];
684 //
685 //        [o_outline_view reloadData];
686     }
687
688 //    [self playlistUpdated];
689 }
690
691 - (IBAction)sortNodeByName:(id)sender
692 {
693     [self sortNode: SORT_TITLE];
694 }
695
696 - (IBAction)sortNodeByAuthor:(id)sender
697 {
698     [self sortNode: SORT_ARTIST];
699 }
700
701 - (void)sortNode:(int)i_mode
702 {
703     playlist_t * p_playlist = pl_Get(VLCIntf);
704     playlist_item_t * p_item;
705
706     // TODO why do we need this kind of sort? It looks crap and confusing...
707
708 //    if ([o_outline_view selectedRow] > -1) {
709 //        p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
710 //        if (!p_item)
711 //            return;
712 //    } else
713 //        p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
714 //
715 //    PL_LOCK;
716 //    if (p_item->i_children > -1) // the item is a node
717 //        playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
718 //    else
719 //        playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
720 //
721 //    PL_UNLOCK;
722 //    [self playlistUpdated];
723 }
724
725 - (input_item_t *)createItem:(NSDictionary *)o_one_item
726 {
727     intf_thread_t * p_intf = VLCIntf;
728     playlist_t * p_playlist = pl_Get(p_intf);
729
730     input_item_t *p_input;
731     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
732     NSString *o_uri, *o_name, *o_path;
733     NSURL * o_nsurl;
734     NSArray *o_options;
735     NSURL *o_true_file;
736
737     /* Get the item */
738     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
739     o_nsurl = [NSURL URLWithString: o_uri];
740     o_path = [o_nsurl path];
741     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
742     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
743
744     if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
745         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
746                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
747
748         NSString *diskType = [VLCOpen getVolumeTypeFromMountPath: o_path];
749         msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
750
751         if ([diskType isEqualToString: kVLCMediaDVD])
752             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
753         else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
754             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
755         else if ([diskType isEqualToString: kVLCMediaAudioCD])
756             o_uri = [NSString stringWithFormat: @"cdda://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
757         else if ([diskType isEqualToString: kVLCMediaVCD])
758             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
759         else if ([diskType isEqualToString: kVLCMediaSVCD])
760             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
761         else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
762             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
763         else
764             msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
765
766         p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
767     }
768     else
769         p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
770
771     if (!p_input)
772         return NULL;
773
774     if (o_options) {
775         NSUInteger count = [o_options count];
776         for (NSUInteger i = 0; i < count; i++)
777             input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
778     }
779
780     /* Recent documents menu */
781     if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
782         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
783
784     return p_input;
785 }
786
787 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
788 {
789     playlist_t * p_playlist = pl_Get(VLCIntf);
790     NSUInteger count = [o_array count];
791     BOOL b_usingPlaylist = [[self model] currentRootType] == ROOT_TYPE_PLAYLIST;
792
793     PL_LOCK;
794     for (NSUInteger i_item = 0; i_item < count; i_item++) {
795         input_item_t *p_input;
796         NSDictionary *o_one_item;
797
798         /* Get the item */
799         o_one_item = [o_array objectAtIndex:i_item];
800         p_input = [self createItem: o_one_item];
801         if (!p_input)
802             continue;
803
804         /* Add the item */
805         int returnValue = playlist_AddInput(p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist, pl_Locked);
806         if (returnValue != VLC_SUCCESS) {
807             vlc_gc_decref(p_input);
808             continue;
809         }
810
811         if (i_item == 0 && !b_enqueue) {
812             playlist_item_t *p_item = playlist_ItemGetByInput(p_playlist, p_input);
813             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item);
814         }
815
816         vlc_gc_decref(p_input);
817     }
818     PL_UNLOCK;
819     [self playlistUpdated];
820 }
821
822 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
823 {
824     playlist_t * p_playlist = pl_Get(VLCIntf);
825     NSUInteger count = [o_array count];
826
827     for (NSUInteger i_item = 0; i_item < count; i_item++) {
828         input_item_t *p_input;
829         NSDictionary *o_one_item;
830
831         /* Get the item */
832         PL_LOCK;
833         o_one_item = [o_array objectAtIndex:i_item];
834         p_input = [self createItem: o_one_item];
835
836         if (!p_input)
837             continue;
838
839         /* Add the item */
840         playlist_NodeAddInput(p_playlist, p_input, p_node,
841                                       PLAYLIST_INSERT,
842                                       i_position == -1 ?
843                                       PLAYLIST_END : i_position + i_item,
844                                       pl_Locked);
845
846
847         if (i_item == 0 && !b_enqueue) {
848             playlist_item_t *p_item;
849             p_item = playlist_ItemGetByInput(p_playlist, p_input);
850             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
851         }
852         PL_UNLOCK;
853         vlc_gc_decref(p_input);
854     }
855 //    [self playlistUpdated];
856 }
857
858 - (IBAction)searchItem:(id)sender
859 {
860     [[self model] searchUpdate:[o_search_field stringValue]];
861 }
862
863 - (IBAction)recursiveExpandNode:(id)sender
864 {
865     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
866     NSUInteger count = [selectedRows count];
867     NSUInteger indexes[count];
868     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
869
870     id o_item;
871     playlist_item_t *p_item;
872     for (NSUInteger i = 0; i < count; i++) {
873         o_item = [o_outline_view itemAtRow: indexes[i]];
874         p_item = (playlist_item_t *)[o_item pointerValue];
875
876         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
877             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
878
879         /* We need to collapse the node first, since OSX refuses to recursively
880          expand an already expanded node, even if children nodes are collapsed. */
881         [o_outline_view collapseItem: o_item collapseChildren: YES];
882         [o_outline_view expandItem: o_item expandChildren: YES];
883
884         selectedRows = [o_outline_view selectedRowIndexes];
885         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
886     }
887 }
888
889 - (NSMenu *)menuForEvent:(NSEvent *)o_event
890 {
891     if (!b_playlistmenu_nib_loaded)
892         b_playlistmenu_nib_loaded = [NSBundle loadNibNamed:@"PlaylistMenu" owner:self];
893
894     NSPoint pt;
895     bool b_rows;
896     bool b_item_sel;
897
898     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
899     int row = [o_outline_view rowAtPoint:pt];
900     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
901         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
902
903     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
904     b_rows = [o_outline_view numberOfRows] != 0;
905
906     playlist_t *p_playlist = pl_Get(VLCIntf);
907     bool b_del_allowed = [[self model] editAllowed];
908
909     [o_mi_play setEnabled: b_item_sel];
910     [o_mi_delete setEnabled: b_item_sel && b_del_allowed];
911     [o_mi_selectall setEnabled: b_rows];
912     [o_mi_info setEnabled: b_item_sel];
913     [o_mi_preparse setEnabled: b_item_sel];
914     [o_mi_recursive_expand setEnabled: b_item_sel];
915     [o_mi_sort_name setEnabled: b_item_sel];
916     [o_mi_sort_author setEnabled: b_item_sel];
917
918     return o_ctx_menu;
919 }
920
921 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
922 {
923     int i_mode, i_type = 0;
924     intf_thread_t *p_intf = VLCIntf;
925     NSString * o_identifier = [o_tc identifier];
926
927     playlist_t *p_playlist = pl_Get(p_intf);
928
929     if (o_tc_sortColumn == o_tc)
930         b_isSortDescending = !b_isSortDescending;
931     else
932         b_isSortDescending = false;
933
934     if (b_isSortDescending)
935         i_type = ORDER_REVERSE;
936     else
937         i_type = ORDER_NORMAL;
938
939     [[self model] sortForColumn:o_identifier withMode:i_type];
940
941     // TODO rework, why do we need a full call here?
942 //    [self playlistUpdated];
943
944     /* Clear indications of any existing column sorting */
945     NSUInteger count = [[o_outline_view tableColumns] count];
946     for (NSUInteger i = 0 ; i < count ; i++)
947         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
948
949     [o_outline_view setHighlightedTableColumn:nil];
950     o_tc_sortColumn = nil;
951
952
953     o_tc_sortColumn = o_tc;
954     [o_outline_view setHighlightedTableColumn:o_tc];
955
956     if (b_isSortDescending)
957         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
958     else
959         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
960 }
961
962
963 - (void)outlineView:(NSOutlineView *)outlineView
964     willDisplayCell:(id)cell
965      forTableColumn:(NSTableColumn *)tableColumn
966                item:(id)item
967 {
968     /* this method can be called when VLC is already dead, hence the extra checks */
969     intf_thread_t * p_intf = VLCIntf;
970     if (!p_intf)
971         return;
972     playlist_t *p_playlist = pl_Get(p_intf);
973
974     id o_playing_item;
975
976     PL_LOCK;
977     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
978     PL_UNLOCK;
979
980     NSFont *fontToUse;
981     if (config_GetInt(VLCIntf, "macosx-large-text"))
982         fontToUse = [NSFont systemFontOfSize:13.];
983     else
984         fontToUse = [NSFont systemFontOfSize:11.];
985
986     BOOL b_is_playing = NO;
987     PL_LOCK;
988     playlist_item_t *p_current_item = playlist_CurrentPlayingItem(p_playlist);
989     if (p_current_item) {
990         b_is_playing = p_current_item->i_id == [item plItemId];
991     }
992     PL_UNLOCK;
993
994     /*
995      TODO: repaint all items bold:
996      [self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
997      || [o_playing_item isEqual: item]
998      */
999
1000     if (b_is_playing)
1001         [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toHaveTrait:NSBoldFontMask]];
1002     else
1003         [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toNotHaveTrait:NSBoldFontMask]];
1004 }
1005
1006 - (id)playingItem
1007 {
1008     playlist_t *p_playlist = pl_Get(VLCIntf);
1009
1010     id o_playing_item;
1011
1012     PL_LOCK;
1013     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1014     PL_UNLOCK;
1015
1016     return o_playing_item;
1017 }
1018
1019 // TODO remove method
1020 - (NSArray *)draggedItems
1021 {
1022     return [[self model] draggedItems];
1023 }
1024
1025 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1026 {
1027     NSTableColumn * o_work_tc;
1028
1029     if (i_state == NSOnState) {
1030         NSString *o_title = [o_dict objectForKey:o_column];
1031         if (!o_title)
1032             return;
1033
1034         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1035         [o_work_tc setEditable: NO];
1036         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1037
1038         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1039
1040         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1041             [o_work_tc setWidth: 20.];
1042             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1043             [[o_work_tc headerCell] setStringValue: @"#"];
1044         }
1045
1046         [o_outline_view addTableColumn: o_work_tc];
1047         [o_work_tc release];
1048         [o_outline_view reloadData];
1049         [o_outline_view setNeedsDisplay: YES];
1050     }
1051     else
1052         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1053
1054     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
1055 }
1056
1057 - (void)saveTableColumns
1058 {
1059     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1060     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1061     NSUInteger count = [o_columns count];
1062     NSTableColumn * o_currentColumn;
1063     for (NSUInteger i = 0; i < count; i++) {
1064         o_currentColumn = [o_columns objectAtIndex:i];
1065         [o_arrayToSave addObject:[NSArray arrayWithObjects:[o_currentColumn identifier], [NSNumber numberWithFloat:[o_currentColumn width]], nil]];
1066     }
1067     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1068     [[NSUserDefaults standardUserDefaults] synchronize];
1069     [o_columns release];
1070     [o_arrayToSave release];
1071 }
1072
1073 - (BOOL)isValidResumeItem:(input_item_t *)p_item
1074 {
1075     char *psz_url = input_item_GetURI(p_item);
1076     NSString *o_url_string = toNSStr(psz_url);
1077     free(psz_url);
1078
1079     if ([o_url_string isEqualToString:@""])
1080         return NO;
1081
1082     NSURL *o_url = [NSURL URLWithString:o_url_string];
1083
1084     if (![o_url isFileURL])
1085         return NO;
1086
1087     BOOL isDir = false;
1088     if (![[NSFileManager defaultManager] fileExistsAtPath:[o_url path] isDirectory:&isDir])
1089         return NO;
1090
1091     if (isDir)
1092         return NO;
1093
1094     return YES;
1095 }
1096
1097 - (void)updateAlertWindow:(NSTimer *)timer
1098 {
1099     NSAlert *alert = [timer userInfo];
1100
1101     --currentResumeTimeout;
1102     if (currentResumeTimeout <= 0) {
1103         [[alert window] close];
1104         [NSApp abortModal];
1105     }
1106
1107     NSString *buttonLabel = _NS("Restart playback");
1108     buttonLabel = [buttonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1109
1110     [[[alert buttons] objectAtIndex:2] setTitle:buttonLabel];
1111 }
1112
1113 - (void)continuePlaybackWhereYouLeftOff:(input_thread_t *)p_input_thread
1114 {
1115     NSDictionary *recentlyPlayedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"recentlyPlayedMedia"];
1116     if (!recentlyPlayedFiles)
1117         return;
1118
1119     input_item_t *p_item = input_GetItem(p_input_thread);
1120     if (!p_item)
1121         return;
1122
1123     /* allow the user to over-write the start/stop/run-time */
1124     if (var_GetFloat(p_input_thread, "run-time") > 0 ||
1125         var_GetFloat(p_input_thread, "start-time") > 0 ||
1126         var_GetFloat(p_input_thread, "stop-time") > 0) {
1127         return;
1128     }
1129
1130     /* check for file existance before resuming */
1131     if (![self isValidResumeItem:p_item])
1132         return;
1133
1134     char *psz_url = decode_URI(input_item_GetURI(p_item));
1135     if (!psz_url)
1136         return;
1137     NSString *url = toNSStr(psz_url);
1138     free(psz_url);
1139
1140     NSNumber *lastPosition = [recentlyPlayedFiles objectForKey:url];
1141     if (!lastPosition || lastPosition.intValue <= 0)
1142         return;
1143
1144     int settingValue = config_GetInt(VLCIntf, "macosx-continue-playback");
1145     if (settingValue == 2) // never resume
1146         return;
1147
1148     NSInteger returnValue = NSAlertErrorReturn;
1149     if (settingValue == 0) { // ask
1150
1151         currentResumeTimeout = 6;
1152         NSString *o_restartButtonLabel = _NS("Restart playback");
1153         o_restartButtonLabel = [o_restartButtonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1154         NSAlert *theAlert = [NSAlert alertWithMessageText:_NS("Continue playback?") defaultButton:_NS("Continue") alternateButton:o_restartButtonLabel otherButton:_NS("Always continue") informativeTextWithFormat:_NS("Playback of \"%@\" will continue at %@"), [NSString stringWithUTF8String:input_item_GetTitleFbName(p_item)], [[VLCStringUtility sharedInstance] stringForTime:lastPosition.intValue]];
1155
1156         NSTimer *timer = [NSTimer timerWithTimeInterval:1
1157                                                  target:self
1158                                                selector:@selector(updateAlertWindow:)
1159                                                userInfo:theAlert
1160                                                 repeats:YES];
1161
1162         [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSModalPanelRunLoopMode];
1163
1164         [[VLCCoreInteraction sharedInstance] pause];
1165         returnValue = [theAlert runModal];
1166         [timer invalidate];
1167         [[VLCCoreInteraction sharedInstance] playOrPause];
1168
1169         // restart button was pressed or timeout happened
1170         if (returnValue == NSAlertAlternateReturn ||
1171             returnValue == NSRunAbortedResponse)
1172             return;
1173     }
1174
1175     mtime_t lastPos = (mtime_t)lastPosition.intValue * 1000000;
1176     msg_Dbg(VLCIntf, "continuing playback at %lld", lastPos);
1177     var_SetTime(p_input_thread, "time", lastPos);
1178
1179     if (returnValue == NSAlertOtherReturn)
1180         config_PutInt(VLCIntf, "macosx-continue-playback", 1);
1181 }
1182
1183 - (void)storePlaybackPositionForItem:(input_thread_t *)p_input_thread
1184 {
1185     if (!var_InheritBool(VLCIntf, "macosx-recentitems"))
1186         return;
1187
1188     input_item_t *p_item = input_GetItem(p_input_thread);
1189     if (!p_item)
1190         return;
1191
1192     if (![self isValidResumeItem:p_item])
1193         return;
1194
1195     char *psz_url = decode_URI(input_item_GetURI(p_item));
1196     if (!psz_url)
1197         return;
1198     NSString *url = toNSStr(psz_url);
1199     free(psz_url);
1200
1201     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
1202     NSMutableDictionary *mutDict = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:@"recentlyPlayedMedia"]];
1203
1204     float relativePos = var_GetFloat(p_input_thread, "position");
1205     mtime_t pos = var_GetTime(p_input_thread, "time") / 1000000;
1206     mtime_t dur = input_item_GetDuration(p_item) / 1000000;
1207
1208     NSMutableArray *mediaList = [[defaults objectForKey:@"recentlyPlayedMediaList"] mutableCopy];
1209
1210     if (relativePos > .05 && relativePos < .95 && dur > 180) {
1211         [mutDict setObject:[NSNumber numberWithInt:pos] forKey:url];
1212
1213         [mediaList removeObject:url];
1214         [mediaList addObject:url];
1215         NSUInteger mediaListCount = mediaList.count;
1216         if (mediaListCount > 30) {
1217             for (NSUInteger x = 0; x < mediaListCount - 30; x++) {
1218                 [mutDict removeObjectForKey:[mediaList objectAtIndex:0]];
1219                 [mediaList removeObjectAtIndex:0];
1220             }
1221         }
1222     } else {
1223         [mutDict removeObjectForKey:url];
1224         [mediaList removeObject:url];
1225     }
1226     [defaults setObject:mutDict forKey:@"recentlyPlayedMedia"];
1227     [defaults setObject:mediaList forKey:@"recentlyPlayedMediaList"];
1228     [defaults synchronize];
1229
1230     [mutDict release];
1231     [mediaList release];
1232 }
1233
1234 @end
1235
1236
1237 @implementation VLCPlaylist (NSOutlineViewDataSource)
1238 /* return the number of children for Obj-C pointer item */ /* DONE */
1239 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1240 {
1241     int i_return = 0;
1242     playlist_item_t *p_item = NULL;
1243     playlist_t * p_playlist = pl_Get(VLCIntf);
1244     //assert(outlineView == o_outline_view);
1245
1246     PL_LOCK;
1247     if (!item)
1248         p_item = p_current_root_item;
1249     else
1250         p_item = (playlist_item_t *)[item pointerValue];
1251
1252     if (p_item)
1253         i_return = p_item->i_children;
1254     PL_UNLOCK;
1255
1256     return i_return > 0 ? i_return : 0;
1257 }
1258
1259 /* return the child at index for the Obj-C pointer item */ /* DONE */
1260 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1261 {
1262     playlist_item_t *p_return = NULL, *p_item = NULL;
1263     NSValue *o_value;
1264     playlist_t * p_playlist = pl_Get(VLCIntf);
1265
1266     PL_LOCK;
1267     if (item == nil)
1268         p_item = p_current_root_item; /* root object */
1269     else
1270         p_item = (playlist_item_t *)[item pointerValue];
1271
1272     if (p_item && index < p_item->i_children && index >= 0)
1273         p_return = p_item->pp_children[index];
1274     PL_UNLOCK;
1275
1276     o_value = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_return]];
1277
1278     if (o_value == nil) {
1279         /* FIXME: Why is there a warning if that happens all the time and seems
1280          * to be normal? Add an assert and fix it.
1281          * msg_Warn(VLCIntf, "playlist item misses pointer value, adding one"); */
1282         o_value = [[NSValue valueWithPointer: p_return] retain];
1283     }
1284
1285     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1286
1287     return o_value;
1288 }
1289
1290 /* is the item expandable */
1291 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1292 {
1293     int i_return = 0;
1294     playlist_t *p_playlist = pl_Get(VLCIntf);
1295
1296     PL_LOCK;
1297     if (item == nil) {
1298         /* root object */
1299         if (p_current_root_item) {
1300             i_return = p_current_root_item->i_children;
1301         }
1302     } else {
1303         playlist_item_t *p_item = (playlist_item_t *)[item pointerValue];
1304         if (p_item)
1305             i_return = p_item->i_children;
1306     }
1307     PL_UNLOCK;
1308
1309     return (i_return >= 0);
1310 }
1311
1312 /* retrieve the string values for the cells */
1313 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)o_tc byItem:(id)item
1314 {
1315     id o_value = nil;
1316     char * psz_value;
1317     playlist_item_t *p_item;
1318
1319     /* For error handling */
1320     static BOOL attempted_reload = NO;
1321
1322     if (item == nil || ![item isKindOfClass: [NSValue class]]) {
1323         /* Attempt to fix the error by asking for a data redisplay
1324          * This might cause infinite loop, so add a small check */
1325         if (!attempted_reload) {
1326             attempted_reload = YES;
1327             [outlineView reloadData];
1328         }
1329         return @"error" ;
1330     }
1331
1332     p_item = (playlist_item_t *)[item pointerValue];
1333     if (!p_item || !p_item->p_input) {
1334         /* Attempt to fix the error by asking for a data redisplay
1335          * This might cause infinite loop, so add a small check */
1336         if (!attempted_reload) {
1337             attempted_reload = YES;
1338             [outlineView reloadData];
1339         }
1340         return @"error";
1341     }
1342
1343     attempted_reload = NO;
1344     NSString * o_identifier = [o_tc identifier];
1345
1346     if ([o_identifier isEqualToString:TRACKNUM_COLUMN]) {
1347         psz_value = input_item_GetTrackNumber(p_item->p_input);
1348         if (psz_value) {
1349             o_value = [NSString stringWithUTF8String:psz_value];
1350             free(psz_value);
1351         }
1352     } else if ([o_identifier isEqualToString:TITLE_COLUMN]) {
1353         /* sanity check to prevent the NSString class from crashing */
1354         char *psz_title =  input_item_GetTitleFbName(p_item->p_input);
1355         if (psz_title) {
1356             o_value = [NSString stringWithUTF8String:psz_title];
1357             free(psz_title);
1358         }
1359     } else if ([o_identifier isEqualToString:ARTIST_COLUMN]) {
1360         psz_value = input_item_GetArtist(p_item->p_input);
1361         if (psz_value) {
1362             o_value = [NSString stringWithUTF8String:psz_value];
1363             free(psz_value);
1364         }
1365     } else if ([o_identifier isEqualToString:@"duration"]) {
1366         char psz_duration[MSTRTIME_MAX_SIZE];
1367         mtime_t dur = input_item_GetDuration(p_item->p_input);
1368         if (dur != -1) {
1369             secstotimestr(psz_duration, dur/1000000);
1370             o_value = [NSString stringWithUTF8String:psz_duration];
1371         }
1372         else
1373             o_value = @"--:--";
1374     } else if ([o_identifier isEqualToString:GENRE_COLUMN]) {
1375         psz_value = input_item_GetGenre(p_item->p_input);
1376         if (psz_value) {
1377             o_value = [NSString stringWithUTF8String:psz_value];
1378             free(psz_value);
1379         }
1380     } else if ([o_identifier isEqualToString:ALBUM_COLUMN]) {
1381         psz_value = input_item_GetAlbum(p_item->p_input);
1382         if (psz_value) {
1383             o_value = [NSString stringWithUTF8String:psz_value];
1384             free(psz_value);
1385         }
1386     } else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN]) {
1387         psz_value = input_item_GetDescription(p_item->p_input);
1388         if (psz_value) {
1389             o_value = [NSString stringWithUTF8String:psz_value];
1390             free(psz_value);
1391         }
1392     } else if ([o_identifier isEqualToString:DATE_COLUMN]) {
1393         psz_value = input_item_GetDate(p_item->p_input);
1394         if (psz_value) {
1395             o_value = [NSString stringWithUTF8String:psz_value];
1396             free(psz_value);
1397         }
1398     } else if ([o_identifier isEqualToString:LANGUAGE_COLUMN]) {
1399         psz_value = input_item_GetLanguage(p_item->p_input);
1400         if (psz_value) {
1401             o_value = [NSString stringWithUTF8String:psz_value];
1402             free(psz_value);
1403         }
1404     }
1405     else if ([o_identifier isEqualToString:URI_COLUMN]) {
1406         psz_value = decode_URI(input_item_GetURI(p_item->p_input));
1407         if (psz_value) {
1408             o_value = [NSString stringWithUTF8String:psz_value];
1409             free(psz_value);
1410         }
1411     }
1412     else if ([o_identifier isEqualToString:FILESIZE_COLUMN]) {
1413         psz_value = input_item_GetURI(p_item->p_input);
1414         o_value = @"";
1415         if (psz_value) {
1416             NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:psz_value]];
1417             if ([url isFileURL]) {
1418                 NSFileManager *fileManager = [NSFileManager defaultManager];
1419                 if ([fileManager fileExistsAtPath:[url path]]) {
1420                     NSError *error;
1421                     NSDictionary *attributes = [fileManager attributesOfItemAtPath:[url path] error:&error];
1422                     o_value = [VLCByteCountFormatter stringFromByteCount:[attributes fileSize] countStyle:NSByteCountFormatterCountStyleDecimal];
1423                 }
1424             }
1425             free(psz_value);
1426         }
1427     }
1428     else if ([o_identifier isEqualToString:@"status"]) {
1429         if (input_item_HasErrorWhenReading(p_item->p_input)) {
1430             o_value = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kAlertCautionIcon)];
1431             [o_value setSize: NSMakeSize(16,16)];
1432         }
1433     }
1434
1435     return o_value;
1436 }
1437
1438 @end