]> git.sesse.net Git - vlc/blob - modules/gui/macosx/playlist.m
macosx: Use core-provided playlist search functionality
[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     NSMutableArray *o_nodes_array;
168     NSMutableArray *o_items_array;
169
170     BOOL b_selected_item_met;
171     BOOL b_isSortDescending;
172     id o_tc_sortColumn;
173     NSUInteger retainedRowSelection;
174
175     BOOL b_playlistmenu_nib_loaded;
176     BOOL b_view_setup;
177 }
178
179 - (void)saveTableColumns;
180 @end
181
182 @implementation VLCPlaylist
183
184 + (void)initialize
185 {
186     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
187     NSMutableArray *o_columnArray = [[NSMutableArray alloc] init];
188     [o_columnArray addObject: [NSArray arrayWithObjects:TITLE_COLUMN, [NSNumber numberWithFloat:190.], nil]];
189     [o_columnArray addObject: [NSArray arrayWithObjects:ARTIST_COLUMN, [NSNumber numberWithFloat:95.], nil]];
190     [o_columnArray addObject: [NSArray arrayWithObjects:DURATION_COLUMN, [NSNumber numberWithFloat:95.], nil]];
191
192     NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
193                                  [NSArray arrayWithArray:o_columnArray], @"PlaylistColumnSelection",
194                                  [NSArray array], @"recentlyPlayedMediaList",
195                                  [NSDictionary dictionary], @"recentlyPlayedMedia", nil];
196
197     [defaults registerDefaults:appDefaults];
198     [o_columnArray release];
199 }
200
201 - (playlist_item_t *)currentPlaylistRoot
202 {
203     // TODO remove
204     playlist_t *p_playlist = pl_Get(VLCIntf);
205     return p_playlist->p_playing;
206 }
207
208 - (PLModel *)model
209 {
210     return o_model;
211 }
212
213 - (void)reloadStyles
214 {
215     NSFont *fontToUse;
216     CGFloat rowHeight;
217     if (config_GetInt(VLCIntf, "macosx-large-text")) {
218         fontToUse = [NSFont systemFontOfSize:13.];
219         rowHeight = 21.;
220     } else {
221         fontToUse = [NSFont systemFontOfSize:11.];
222         rowHeight = 16.;
223     }
224
225     NSArray *columns = [o_outline_view tableColumns];
226     NSUInteger count = columns.count;
227     for (NSUInteger x = 0; x < count; x++)
228         [[[columns objectAtIndex:x] dataCell] setFont:fontToUse];
229     [o_outline_view setRowHeight:rowHeight];
230 }
231
232 - (id)init
233 {
234     self = [super init];
235     if (self != nil) {
236
237
238         playlist_t * p_playlist = pl_Get(VLCIntf);
239         p_current_root_item = p_playlist->p_local_category;
240         o_outline_dict = [[NSMutableDictionary alloc] init];
241
242         o_nodes_array = [[NSMutableArray alloc] init];
243         o_items_array = [[NSMutableArray alloc] init];
244     }
245     return self;
246 }
247
248 - (void)dealloc
249 {
250     [o_outline_dict release];
251     [o_nodes_array release];
252     [o_items_array release];
253     [super dealloc];
254 }
255
256 - (void)awakeFromNib
257 {
258     if (b_view_setup)
259         return;
260
261     playlist_t * p_playlist = pl_Get(VLCIntf);
262     [o_outline_view setTarget: self];
263     [o_outline_view setDelegate: self];
264     [o_outline_view setAllowsEmptySelection: NO];
265     [o_outline_view expandItem: [o_outline_view itemAtRow:0]];
266
267     [self reloadStyles];
268     [self initStrings];
269
270     o_model = [[PLModel alloc] initWithOutlineView:o_outline_view playlist:p_playlist rootItem:p_current_root_item];
271     [o_outline_view setDataSource:o_model];
272     [o_outline_view reloadData];
273
274     [o_outline_view setDoubleAction: @selector(playItem:)];
275
276     [o_outline_view registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
277     [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
278
279     /* This uses a private API, but works fine on all current OSX releases.
280      * Radar ID 11739459 request a public API for this. However, it is probably
281      * easier and faster to recreate similar looking bitmaps ourselves. */
282     o_ascendingSortingImage = [[NSOutlineView class] _defaultTableHeaderSortImage];
283     o_descendingSortingImage = [[NSOutlineView class] _defaultTableHeaderReverseSortImage];
284
285     o_tc_sortColumn = nil;
286
287     NSArray * o_columnArray = [[NSUserDefaults standardUserDefaults] arrayForKey:@"PlaylistColumnSelection"];
288     NSUInteger count = [o_columnArray count];
289
290     id o_menu = [[VLCMain sharedInstance] mainMenu];
291     NSString * o_column;
292
293     NSMenu *o_context_menu = [o_menu setupPlaylistTableColumnsMenu];
294     [o_playlist_header setMenu: o_context_menu];
295
296     for (NSUInteger i = 0; i < count; i++) {
297         o_column = [[o_columnArray objectAtIndex:i] objectAtIndex:0];
298         if ([o_column isEqualToString:@"status"])
299             continue;
300
301         if(![o_menu setPlaylistColumnTableState: NSOnState forColumn: o_column])
302             continue;
303
304         [[o_outline_view tableColumnWithIdentifier: o_column] setWidth: [[[o_columnArray objectAtIndex:i] objectAtIndex:1] floatValue]];
305     }
306
307     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];
308
309     b_view_setup = YES;
310 }
311
312 - (void)applicationWillTerminate:(NSNotification *)notification
313 {
314     /* 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 */
315     [self saveTableColumns];
316 }
317
318 - (void)initStrings
319 {
320     [o_mi_play setTitle: _NS("Play")];
321     [o_mi_delete setTitle: _NS("Delete")];
322     [o_mi_recursive_expand setTitle: _NS("Expand Node")];
323     [o_mi_selectall setTitle: _NS("Select All")];
324     [o_mi_info setTitle: _NS("Media Information...")];
325     [o_mi_dl_cover_art setTitle: _NS("Download Cover Art")];
326     [o_mi_preparse setTitle: _NS("Fetch Meta Data")];
327     [o_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
328     [o_mm_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
329     [[o_mm_mi_revealInFinder menu] setAutoenablesItems: NO];
330     [o_mi_sort_name setTitle: _NS("Sort Node by Name")];
331     [o_mi_sort_author setTitle: _NS("Sort Node by Author")];
332
333     [o_search_field setToolTip: _NS("Search in Playlist")];
334 }
335
336 - (void)playlistUpdated
337 {
338     /* Clear indications of any existing column sorting */
339     NSUInteger count = [[o_outline_view tableColumns] count];
340     for (NSUInteger i = 0 ; i < count ; i++)
341         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
342
343     [o_outline_view setHighlightedTableColumn:nil];
344     o_tc_sortColumn = nil;
345     // TODO Find a way to keep the dict size to a minimum
346     //[o_outline_dict removeAllObjects];
347     [o_outline_view reloadData];
348     [[[[VLCMain sharedInstance] wizard] playlistWizard] reloadOutlineView];
349
350     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:retainedRowSelection] byExtendingSelection:NO];
351
352     [self outlineViewSelectionDidChange: nil];
353     [[VLCMain sharedInstance] updateMainWindow];
354 }
355
356 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
357 {
358 //    // FIXME: unsafe
359 //    playlist_item_t * p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
360 //
361 //    if (p_item) {
362 //        /* update the state of our Reveal-in-Finder menu items */
363 //        NSMutableString *o_mrl;
364 //        char *psz_uri = input_item_GetURI(p_item->p_input);
365 //
366 //        [o_mi_revealInFinder setEnabled: NO];
367 //        [o_mm_mi_revealInFinder setEnabled: NO];
368 //        if (psz_uri) {
369 //            o_mrl = [NSMutableString stringWithUTF8String: psz_uri];
370 //
371 //            /* perform some checks whether it is a file and if it is local at all... */
372 //            NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
373 //            if (prefix_range.location != NSNotFound)
374 //                [o_mrl deleteCharactersInRange: prefix_range];
375 //
376 //            if ([o_mrl characterAtIndex:0] == '/') {
377 //                [o_mi_revealInFinder setEnabled: YES];
378 //                [o_mm_mi_revealInFinder setEnabled: YES];
379 //            }
380 //            free(psz_uri);
381 //        }
382 //
383 //        /* update our info-panel to reflect the new item */
384 //        [[[VLCMain sharedInstance] info] updatePanelWithItem:p_item->p_input];
385 //    }
386 }
387
388 - (BOOL)isSelectionEmpty
389 {
390     return [o_outline_view selectedRow] == -1;
391 }
392
393 - (void)updateRowSelection
394 {
395     // FIXME: unsafe
396     playlist_t *p_playlist = pl_Get(VLCIntf);
397     playlist_item_t *p_item, *p_temp_item;
398     NSMutableArray *o_array = [NSMutableArray array];
399
400     PL_LOCK;
401     p_item = playlist_CurrentPlayingItem(p_playlist);
402     if (p_item == NULL) {
403         PL_UNLOCK;
404         return;
405     }
406
407     p_temp_item = p_item;
408     while(p_temp_item->p_parent) {
409         [o_array insertObject: [NSValue valueWithPointer: p_temp_item] atIndex: 0];
410         p_temp_item = p_temp_item->p_parent;
411     }
412     PL_UNLOCK;
413
414     NSUInteger count = [o_array count];
415     for (NSUInteger j = 0; j < count - 1; j++) {
416         id o_item;
417         if ((o_item = [o_outline_dict objectForKey:
418                             [NSString stringWithFormat: @"%p",
419                             [[o_array objectAtIndex:j] pointerValue]]]) != nil) {
420             [o_outline_view expandItem: o_item];
421         }
422     }
423
424     id o_item = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_item]];
425     NSInteger i_index = [o_outline_view rowForItem:o_item];
426     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_index] byExtendingSelection:NO];
427     [o_outline_view setNeedsDisplay:YES];
428 }
429
430 /* Check if p_item is a child of p_node recursively. We need to check the item
431    existence first since OSX sometimes tries to redraw items that have been
432    deleted. We don't do it when not required since this verification takes
433    quite a long time on big playlists (yes, pretty hacky). */
434
435 // todo remove useless parameters
436 - (BOOL)isItem: (PLItem *)p_item inNode: (PLItem *)p_node checkItemExistence:(BOOL)b_check locked:(BOOL)b_locked
437 {
438     PLItem *p_temp_item = p_item;
439
440     if ([p_node plItemId] == [p_item plItemId])
441         return YES;
442
443     while(p_temp_item) {
444         p_temp_item = [p_temp_item parent];
445         if ([p_temp_item plItemId] == [p_node plItemId]) {
446             return YES;
447         }
448     }
449
450     return NO;
451 }
452
453 /* This method is useful for instance to remove the selected children of an
454    already selected node */
455 - (void)removeItemsFrom:(id)o_items ifChildrenOf:(id)o_nodes
456 {
457     NSUInteger itemCount = [o_items count];
458     NSUInteger nodeCount = [o_nodes count];
459     for (NSUInteger i = 0 ; i < itemCount ; i++) {
460         for (NSUInteger j = 0 ; j < nodeCount ; j++) {
461             if (o_items == o_nodes) {
462                 if (j == i) continue;
463             }
464             if ([self isItem: [o_items objectAtIndex:i]
465                     inNode: [o_nodes objectAtIndex:j]
466                     checkItemExistence: NO locked:NO]) {
467                 [o_items removeObjectAtIndex:i];
468                 /* We need to execute the next iteration with the same index
469                    since the current item has been deleted */
470                 i--;
471                 break;
472             }
473         }
474     }
475 }
476
477 - (IBAction)savePlaylist:(id)sender
478 {
479     playlist_t * p_playlist = pl_Get(VLCIntf);
480
481     NSSavePanel *o_save_panel = [NSSavePanel savePanel];
482     NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
483
484     [NSBundle loadNibNamed:@"PlaylistAccessoryView" owner:self];
485
486     [o_save_accessory_text setStringValue: _NS("File Format:")];
487     [[o_save_accessory_popup itemAtIndex:0] setTitle: _NS("Extended M3U")];
488     [[o_save_accessory_popup itemAtIndex:1] setTitle: _NS("XML Shareable Playlist Format (XSPF)")];
489     [[o_save_accessory_popup itemAtIndex:2] setTitle: _NS("HTML playlist")];
490
491     [o_save_panel setTitle: _NS("Save Playlist")];
492     [o_save_panel setPrompt: _NS("Save")];
493     [o_save_panel setAccessoryView: o_save_accessory_view];
494     [o_save_panel setNameFieldStringValue: o_name];
495
496     if ([o_save_panel runModal] == NSFileHandlingPanelOKButton) {
497         NSString *o_filename = [[o_save_panel URL] path];
498
499         if ([o_save_accessory_popup indexOfSelectedItem] == 0) {
500             NSString * o_real_filename;
501             NSRange range;
502             range.location = [o_filename length] - [@".m3u" length];
503             range.length = [@".m3u" length];
504
505             if ([o_filename compare:@".m3u" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
506                 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
507             else
508                 o_real_filename = o_filename;
509
510             playlist_Export(p_playlist,
511                 [o_real_filename fileSystemRepresentation],
512                 p_playlist->p_local_category, "export-m3u");
513         } else if ([o_save_accessory_popup indexOfSelectedItem] == 1) {
514             NSString * o_real_filename;
515             NSRange range;
516             range.location = [o_filename length] - [@".xspf" length];
517             range.length = [@".xspf" length];
518
519             if ([o_filename compare:@".xspf" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
520                 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
521             else
522                 o_real_filename = o_filename;
523
524             playlist_Export(p_playlist,
525                 [o_real_filename fileSystemRepresentation],
526                 p_playlist->p_local_category, "export-xspf");
527         } else {
528             NSString * o_real_filename;
529             NSRange range;
530             range.location = [o_filename length] - [@".html" length];
531             range.length = [@".html" length];
532
533             if ([o_filename compare:@".html" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
534                 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
535             else
536                 o_real_filename = o_filename;
537
538             playlist_Export(p_playlist,
539                 [o_real_filename fileSystemRepresentation],
540                 p_playlist->p_local_category, "export-html");
541         }
542     }
543 }
544
545 /* When called retrieves the selected outlineview row and plays that node or item */
546 - (IBAction)playItem:(id)sender
547 {
548     intf_thread_t * p_intf = VLCIntf;
549     playlist_t * p_playlist = pl_Get(p_intf);
550
551     playlist_item_t *p_item;
552     playlist_item_t *p_node = NULL;
553
554     // ignore clicks on column header when handling double action
555     if (sender != nil && [o_outline_view clickedRow] == -1 && sender != o_mi_play)
556         return;
557
558     PL_LOCK;
559     PLItem *o_item = [o_outline_view itemAtRow:[o_outline_view selectedRow]];
560     p_item = playlist_ItemGetById(p_playlist, [o_item plItemId]);
561
562     if (p_item) {
563         if (p_item->i_children == -1) {
564             p_node = p_item->p_parent;
565         } else {
566             p_node = p_item;
567             if (p_node->i_children > 0 && p_node->pp_children[0]->i_children == -1)
568                 p_item = p_node->pp_children[0];
569             else
570                 p_item = NULL;
571         }
572
573         playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
574     }
575     PL_UNLOCK;
576 }
577
578 - (IBAction)revealItemInFinder:(id)sender
579 {
580     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
581     NSUInteger count = [selectedRows count];
582     NSUInteger indexes[count];
583     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
584
585     NSMutableString * o_mrl;
586     for (NSUInteger i = 0; i < count; i++) {
587         PLItem *o_item = [o_outline_view itemAtRow:indexes[i]];
588
589         char * psz_url = decode_URI(input_item_GetURI([o_item input]));
590         o_mrl = [[NSMutableString alloc] initWithString: [NSString stringWithUTF8String:psz_url ? psz_url : ""]];
591         if (psz_url != NULL)
592             free( psz_url );
593
594         /* perform some checks whether it is a file and if it is local at all... */
595         if ([o_mrl length] > 0) {
596             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
597             if (prefix_range.location != NSNotFound)
598                 [o_mrl deleteCharactersInRange: prefix_range];
599
600             if ([o_mrl characterAtIndex:0] == '/')
601                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
602         }
603
604         [o_mrl release];
605     }
606 }
607
608 /* When called retrieves the selected outlineview row and plays that node or item */
609 - (IBAction)preparseItem:(id)sender
610 {
611     int i_count;
612     NSIndexSet *o_selected_indexes;
613     intf_thread_t * p_intf = VLCIntf;
614     playlist_t * p_playlist = pl_Get(p_intf);
615     playlist_item_t *p_item = NULL;
616
617     o_selected_indexes = [o_outline_view selectedRowIndexes];
618     i_count = [o_selected_indexes count];
619
620     NSUInteger indexes[i_count];
621     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
622     for (int i = 0; i < i_count; i++) {
623         PLItem *o_item = [o_outline_view itemAtRow:indexes[i]];
624         [o_outline_view deselectRow: indexes[i]];
625
626         if (![o_item isLeaf]) {
627             msg_Dbg(p_intf, "preparsing nodes not implemented");
628             continue;
629         }
630
631         libvlc_MetaRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
632
633     }
634     [self playlistUpdated];
635 }
636
637 - (IBAction)downloadCoverArt:(id)sender
638 {
639     int i_count;
640     NSIndexSet *o_selected_indexes;
641     intf_thread_t * p_intf = VLCIntf;
642     playlist_t * p_playlist = pl_Get(p_intf);
643     playlist_item_t *p_item = NULL;
644
645     o_selected_indexes = [o_outline_view selectedRowIndexes];
646     i_count = [o_selected_indexes count];
647
648     NSUInteger indexes[i_count];
649     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
650     for (int i = 0; i < i_count; i++) {
651         PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
652
653         if (![o_item isLeaf])
654             continue;
655
656         libvlc_ArtRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
657     }
658     [self playlistUpdated];
659 }
660
661 - (IBAction)selectAll:(id)sender
662 {
663     [o_outline_view selectAll: nil];
664 }
665
666 - (IBAction)showInfoPanel:(id)sender
667 {
668     [[[VLCMain sharedInstance] info] initPanel];
669 }
670
671 - (IBAction)deleteItem:(id)sender
672 {
673     int i_count;
674     NSIndexSet *o_selected_indexes;
675     intf_thread_t * p_intf = VLCIntf;
676     playlist_t * p_playlist = pl_Get(p_intf);
677
678     // check if deletion is allowed
679     if (![[self model] editAllowed])
680         return;
681
682     o_selected_indexes = [o_outline_view selectedRowIndexes];
683     i_count = [o_selected_indexes count];
684     retainedRowSelection = [o_selected_indexes firstIndex];
685     if (retainedRowSelection == NSNotFound)
686         retainedRowSelection = 0;
687
688
689     NSUInteger indexes[i_count];
690 //    if (i_count == [o_outline_view numberOfRows]) {
691 //        PL_LOCK;
692 //        playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
693 //        PL_UNLOCK;
694 //        [self playlistUpdated];
695 //        return;
696 //    }
697     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
698     for (int i = 0; i < i_count; i++) {
699         PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
700         [o_outline_view deselectRow: indexes[i]];
701
702         /// TODO
703 //        if (p_item->i_children != -1) {
704 //        //is a node and not an item
705 //            if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
706 //                [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
707 //                        checkItemExistence: NO locked:YES] == YES)
708 //                // if current item is in selected node and is playing then stop playlist
709 //                playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
710 //
711 //                playlist_NodeDelete(p_playlist, p_item, true, false);
712 //        } else
713
714             playlist_DeleteFromInput(p_playlist, [o_item input], pl_Unlocked);
715 //        [[o_item parent] deleteChild:o_item];
716 //
717 //        [o_outline_view reloadData];
718     }
719
720 //    [self playlistUpdated];
721 }
722
723 - (IBAction)sortNodeByName:(id)sender
724 {
725     [self sortNode: SORT_TITLE];
726 }
727
728 - (IBAction)sortNodeByAuthor:(id)sender
729 {
730     [self sortNode: SORT_ARTIST];
731 }
732
733 - (void)sortNode:(int)i_mode
734 {
735     playlist_t * p_playlist = pl_Get(VLCIntf);
736     playlist_item_t * p_item;
737
738     // TODO why do we need this kind of sort? It looks crap and confusing...
739
740 //    if ([o_outline_view selectedRow] > -1) {
741 //        p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
742 //        if (!p_item)
743 //            return;
744 //    } else
745 //        p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
746 //
747 //    PL_LOCK;
748 //    if (p_item->i_children > -1) // the item is a node
749 //        playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
750 //    else
751 //        playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
752 //
753 //    PL_UNLOCK;
754 //    [self playlistUpdated];
755 }
756
757 - (input_item_t *)createItem:(NSDictionary *)o_one_item
758 {
759     intf_thread_t * p_intf = VLCIntf;
760     playlist_t * p_playlist = pl_Get(p_intf);
761
762     input_item_t *p_input;
763     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
764     NSString *o_uri, *o_name, *o_path;
765     NSURL * o_nsurl;
766     NSArray *o_options;
767     NSURL *o_true_file;
768
769     /* Get the item */
770     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
771     o_nsurl = [NSURL URLWithString: o_uri];
772     o_path = [o_nsurl path];
773     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
774     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
775
776     if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
777         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
778                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
779
780         NSString *diskType = [VLCOpen getVolumeTypeFromMountPath: o_path];
781         msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
782
783         if ([diskType isEqualToString: kVLCMediaDVD])
784             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
785         else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
786             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
787         else if ([diskType isEqualToString: kVLCMediaAudioCD])
788             o_uri = [NSString stringWithFormat: @"cdda://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
789         else if ([diskType isEqualToString: kVLCMediaVCD])
790             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
791         else if ([diskType isEqualToString: kVLCMediaSVCD])
792             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
793         else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
794             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
795         else
796             msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
797
798         p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
799     }
800     else
801         p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
802
803     if (!p_input)
804         return NULL;
805
806     if (o_options) {
807         NSUInteger count = [o_options count];
808         for (NSUInteger i = 0; i < count; i++)
809             input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
810     }
811
812     /* Recent documents menu */
813     if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
814         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
815
816     return p_input;
817 }
818
819 - (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
820 {
821     playlist_t * p_playlist = pl_Get(VLCIntf);
822     NSUInteger count = [o_array count];
823     BOOL b_usingPlaylist = [[self model] currentRootType] == ROOT_TYPE_PLAYLIST;
824
825     PL_LOCK;
826     for (NSUInteger i_item = 0; i_item < count; i_item++) {
827         input_item_t *p_input;
828         NSDictionary *o_one_item;
829
830         /* Get the item */
831         o_one_item = [o_array objectAtIndex:i_item];
832         p_input = [self createItem: o_one_item];
833         if (!p_input)
834             continue;
835
836         /* Add the item */
837         int returnValue = playlist_AddInput(p_playlist, p_input, PLAYLIST_INSERT, i_position == -1 ? PLAYLIST_END : i_position + i_item, b_usingPlaylist, pl_Locked);
838         if (returnValue != VLC_SUCCESS) {
839             vlc_gc_decref(p_input);
840             continue;
841         }
842
843         if (i_item == 0 && !b_enqueue) {
844             playlist_item_t *p_item = playlist_ItemGetByInput(p_playlist, p_input);
845             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_item->p_parent, p_item);
846         }
847
848         vlc_gc_decref(p_input);
849     }
850     PL_UNLOCK;
851     [self playlistUpdated];
852 }
853
854 - (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
855 {
856     playlist_t * p_playlist = pl_Get(VLCIntf);
857     NSUInteger count = [o_array count];
858
859     for (NSUInteger i_item = 0; i_item < count; i_item++) {
860         input_item_t *p_input;
861         NSDictionary *o_one_item;
862
863         /* Get the item */
864         PL_LOCK;
865         o_one_item = [o_array objectAtIndex:i_item];
866         p_input = [self createItem: o_one_item];
867
868         if (!p_input)
869             continue;
870
871         /* Add the item */
872         playlist_NodeAddInput(p_playlist, p_input, p_node,
873                                       PLAYLIST_INSERT,
874                                       i_position == -1 ?
875                                       PLAYLIST_END : i_position + i_item,
876                                       pl_Locked);
877
878
879         if (i_item == 0 && !b_enqueue) {
880             playlist_item_t *p_item;
881             p_item = playlist_ItemGetByInput(p_playlist, p_input);
882             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
883         }
884         PL_UNLOCK;
885         vlc_gc_decref(p_input);
886     }
887 //    [self playlistUpdated];
888 }
889
890 - (IBAction)searchItem:(id)sender
891 {
892     [[self model] searchUpdate:[o_search_field stringValue]];
893 }
894
895 - (IBAction)recursiveExpandNode:(id)sender
896 {
897     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
898     NSUInteger count = [selectedRows count];
899     NSUInteger indexes[count];
900     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
901
902     id o_item;
903     playlist_item_t *p_item;
904     for (NSUInteger i = 0; i < count; i++) {
905         o_item = [o_outline_view itemAtRow: indexes[i]];
906         p_item = (playlist_item_t *)[o_item pointerValue];
907
908         if (![[o_outline_view dataSource] outlineView: o_outline_view isItemExpandable: o_item])
909             o_item = [o_outline_dict objectForKey: [NSString stringWithFormat: @"%p", p_item->p_parent]];
910
911         /* We need to collapse the node first, since OSX refuses to recursively
912          expand an already expanded node, even if children nodes are collapsed. */
913         [o_outline_view collapseItem: o_item collapseChildren: YES];
914         [o_outline_view expandItem: o_item expandChildren: YES];
915
916         selectedRows = [o_outline_view selectedRowIndexes];
917         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
918     }
919 }
920
921 - (NSMenu *)menuForEvent:(NSEvent *)o_event
922 {
923     if (!b_playlistmenu_nib_loaded)
924         b_playlistmenu_nib_loaded = [NSBundle loadNibNamed:@"PlaylistMenu" owner:self];
925
926     NSPoint pt;
927     bool b_rows;
928     bool b_item_sel;
929
930     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
931     int row = [o_outline_view rowAtPoint:pt];
932     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
933         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
934
935     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
936     b_rows = [o_outline_view numberOfRows] != 0;
937
938     playlist_t *p_playlist = pl_Get(VLCIntf);
939     bool b_del_allowed = [[self model] editAllowed];
940
941     [o_mi_play setEnabled: b_item_sel];
942     [o_mi_delete setEnabled: b_item_sel && b_del_allowed];
943     [o_mi_selectall setEnabled: b_rows];
944     [o_mi_info setEnabled: b_item_sel];
945     [o_mi_preparse setEnabled: b_item_sel];
946     [o_mi_recursive_expand setEnabled: b_item_sel];
947     [o_mi_sort_name setEnabled: b_item_sel];
948     [o_mi_sort_author setEnabled: b_item_sel];
949
950     return o_ctx_menu;
951 }
952
953 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
954 {
955     int i_mode, i_type = 0;
956     intf_thread_t *p_intf = VLCIntf;
957     NSString * o_identifier = [o_tc identifier];
958
959     playlist_t *p_playlist = pl_Get(p_intf);
960
961     if (o_tc_sortColumn == o_tc)
962         b_isSortDescending = !b_isSortDescending;
963     else
964         b_isSortDescending = false;
965
966     if (b_isSortDescending)
967         i_type = ORDER_REVERSE;
968     else
969         i_type = ORDER_NORMAL;
970
971     [[self model] sortForColumn:o_identifier withMode:i_type];
972
973     // TODO rework, why do we need a full call here?
974 //    [self playlistUpdated];
975
976     /* Clear indications of any existing column sorting */
977     NSUInteger count = [[o_outline_view tableColumns] count];
978     for (NSUInteger i = 0 ; i < count ; i++)
979         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
980
981     [o_outline_view setHighlightedTableColumn:nil];
982     o_tc_sortColumn = nil;
983
984
985     o_tc_sortColumn = o_tc;
986     [o_outline_view setHighlightedTableColumn:o_tc];
987
988     if (b_isSortDescending)
989         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
990     else
991         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
992 }
993
994
995 - (void)outlineView:(NSOutlineView *)outlineView
996     willDisplayCell:(id)cell
997      forTableColumn:(NSTableColumn *)tableColumn
998                item:(id)item
999 {
1000     /* this method can be called when VLC is already dead, hence the extra checks */
1001     intf_thread_t * p_intf = VLCIntf;
1002     if (!p_intf)
1003         return;
1004     playlist_t *p_playlist = pl_Get(p_intf);
1005
1006     id o_playing_item;
1007
1008     PL_LOCK;
1009     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1010     PL_UNLOCK;
1011
1012     NSFont *fontToUse;
1013     if (config_GetInt(VLCIntf, "macosx-large-text"))
1014         fontToUse = [NSFont systemFontOfSize:13.];
1015     else
1016         fontToUse = [NSFont systemFontOfSize:11.];
1017
1018     BOOL b_is_playing = NO;
1019     PL_LOCK;
1020     playlist_item_t *p_current_item = playlist_CurrentPlayingItem(p_playlist);
1021     if (p_current_item) {
1022         b_is_playing = p_current_item->i_id == [item plItemId];
1023     }
1024     PL_UNLOCK;
1025
1026     /*
1027      TODO: repaint all items bold:
1028      [self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
1029      || [o_playing_item isEqual: item]
1030      */
1031
1032     if (b_is_playing)
1033         [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toHaveTrait:NSBoldFontMask]];
1034     else
1035         [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toNotHaveTrait:NSBoldFontMask]];
1036 }
1037
1038 - (id)playingItem
1039 {
1040     playlist_t *p_playlist = pl_Get(VLCIntf);
1041
1042     id o_playing_item;
1043
1044     PL_LOCK;
1045     o_playing_item = [o_outline_dict objectForKey: [NSString stringWithFormat:@"%p",  playlist_CurrentPlayingItem(p_playlist)]];
1046     PL_UNLOCK;
1047
1048     return o_playing_item;
1049 }
1050
1051 - (NSArray *)draggedItems
1052 {
1053     return [[o_nodes_array arrayByAddingObjectsFromArray: o_items_array] retain];
1054 }
1055
1056 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
1057 {
1058     NSTableColumn * o_work_tc;
1059
1060     if (i_state == NSOnState) {
1061         NSString *o_title = [o_dict objectForKey:o_column];
1062         if (!o_title)
1063             return;
1064
1065         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
1066         [o_work_tc setEditable: NO];
1067         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
1068
1069         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
1070
1071         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
1072             [o_work_tc setWidth: 20.];
1073             [o_work_tc setResizingMask: NSTableColumnNoResizing];
1074             [[o_work_tc headerCell] setStringValue: @"#"];
1075         }
1076
1077         [o_outline_view addTableColumn: o_work_tc];
1078         [o_work_tc release];
1079         [o_outline_view reloadData];
1080         [o_outline_view setNeedsDisplay: YES];
1081     }
1082     else
1083         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
1084
1085     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
1086 }
1087
1088 - (void)saveTableColumns
1089 {
1090     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
1091     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
1092     NSUInteger count = [o_columns count];
1093     NSTableColumn * o_currentColumn;
1094     for (NSUInteger i = 0; i < count; i++) {
1095         o_currentColumn = [o_columns objectAtIndex:i];
1096         [o_arrayToSave addObject:[NSArray arrayWithObjects:[o_currentColumn identifier], [NSNumber numberWithFloat:[o_currentColumn width]], nil]];
1097     }
1098     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
1099     [[NSUserDefaults standardUserDefaults] synchronize];
1100     [o_columns release];
1101     [o_arrayToSave release];
1102 }
1103
1104 - (BOOL)isValidResumeItem:(input_item_t *)p_item
1105 {
1106     char *psz_url = input_item_GetURI(p_item);
1107     NSString *o_url_string = toNSStr(psz_url);
1108     free(psz_url);
1109
1110     if ([o_url_string isEqualToString:@""])
1111         return NO;
1112
1113     NSURL *o_url = [NSURL URLWithString:o_url_string];
1114
1115     if (![o_url isFileURL])
1116         return NO;
1117
1118     BOOL isDir = false;
1119     if (![[NSFileManager defaultManager] fileExistsAtPath:[o_url path] isDirectory:&isDir])
1120         return NO;
1121
1122     if (isDir)
1123         return NO;
1124
1125     return YES;
1126 }
1127
1128 - (void)updateAlertWindow:(NSTimer *)timer
1129 {
1130     NSAlert *alert = [timer userInfo];
1131
1132     --currentResumeTimeout;
1133     if (currentResumeTimeout <= 0) {
1134         [[alert window] close];
1135         [NSApp abortModal];
1136     }
1137
1138     NSString *buttonLabel = _NS("Restart playback");
1139     buttonLabel = [buttonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1140
1141     [[[alert buttons] objectAtIndex:2] setTitle:buttonLabel];
1142 }
1143
1144 - (void)continuePlaybackWhereYouLeftOff:(input_thread_t *)p_input_thread
1145 {
1146     NSDictionary *recentlyPlayedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"recentlyPlayedMedia"];
1147     if (!recentlyPlayedFiles)
1148         return;
1149
1150     input_item_t *p_item = input_GetItem(p_input_thread);
1151     if (!p_item)
1152         return;
1153
1154     /* allow the user to over-write the start/stop/run-time */
1155     if (var_GetFloat(p_input_thread, "run-time") > 0 ||
1156         var_GetFloat(p_input_thread, "start-time") > 0 ||
1157         var_GetFloat(p_input_thread, "stop-time") > 0) {
1158         return;
1159     }
1160
1161     /* check for file existance before resuming */
1162     if (![self isValidResumeItem:p_item])
1163         return;
1164
1165     char *psz_url = decode_URI(input_item_GetURI(p_item));
1166     if (!psz_url)
1167         return;
1168     NSString *url = toNSStr(psz_url);
1169     free(psz_url);
1170
1171     NSNumber *lastPosition = [recentlyPlayedFiles objectForKey:url];
1172     if (!lastPosition || lastPosition.intValue <= 0)
1173         return;
1174
1175     int settingValue = config_GetInt(VLCIntf, "macosx-continue-playback");
1176     if (settingValue == 2) // never resume
1177         return;
1178
1179     NSInteger returnValue = NSAlertErrorReturn;
1180     if (settingValue == 0) { // ask
1181
1182         currentResumeTimeout = 6;
1183         NSString *o_restartButtonLabel = _NS("Restart playback");
1184         o_restartButtonLabel = [o_restartButtonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1185         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]];
1186
1187         NSTimer *timer = [NSTimer timerWithTimeInterval:1
1188                                                  target:self
1189                                                selector:@selector(updateAlertWindow:)
1190                                                userInfo:theAlert
1191                                                 repeats:YES];
1192
1193         [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSModalPanelRunLoopMode];
1194
1195         [[VLCCoreInteraction sharedInstance] pause];
1196         returnValue = [theAlert runModal];
1197         [timer invalidate];
1198         [[VLCCoreInteraction sharedInstance] playOrPause];
1199
1200         // restart button was pressed or timeout happened
1201         if (returnValue == NSAlertAlternateReturn ||
1202             returnValue == NSRunAbortedResponse)
1203             return;
1204     }
1205
1206     mtime_t lastPos = (mtime_t)lastPosition.intValue * 1000000;
1207     msg_Dbg(VLCIntf, "continuing playback at %lld", lastPos);
1208     var_SetTime(p_input_thread, "time", lastPos);
1209
1210     if (returnValue == NSAlertOtherReturn)
1211         config_PutInt(VLCIntf, "macosx-continue-playback", 1);
1212 }
1213
1214 - (void)storePlaybackPositionForItem:(input_thread_t *)p_input_thread
1215 {
1216     if (!var_InheritBool(VLCIntf, "macosx-recentitems"))
1217         return;
1218
1219     input_item_t *p_item = input_GetItem(p_input_thread);
1220     if (!p_item)
1221         return;
1222
1223     if (![self isValidResumeItem:p_item])
1224         return;
1225
1226     char *psz_url = decode_URI(input_item_GetURI(p_item));
1227     if (!psz_url)
1228         return;
1229     NSString *url = toNSStr(psz_url);
1230     free(psz_url);
1231
1232     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
1233     NSMutableDictionary *mutDict = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:@"recentlyPlayedMedia"]];
1234
1235     float relativePos = var_GetFloat(p_input_thread, "position");
1236     mtime_t pos = var_GetTime(p_input_thread, "time") / 1000000;
1237     mtime_t dur = input_item_GetDuration(p_item) / 1000000;
1238
1239     NSMutableArray *mediaList = [[defaults objectForKey:@"recentlyPlayedMediaList"] mutableCopy];
1240
1241     if (relativePos > .05 && relativePos < .95 && dur > 180) {
1242         [mutDict setObject:[NSNumber numberWithInt:pos] forKey:url];
1243
1244         [mediaList removeObject:url];
1245         [mediaList addObject:url];
1246         NSUInteger mediaListCount = mediaList.count;
1247         if (mediaListCount > 30) {
1248             for (NSUInteger x = 0; x < mediaListCount - 30; x++) {
1249                 [mutDict removeObjectForKey:[mediaList objectAtIndex:0]];
1250                 [mediaList removeObjectAtIndex:0];
1251             }
1252         }
1253     } else {
1254         [mutDict removeObjectForKey:url];
1255         [mediaList removeObject:url];
1256     }
1257     [defaults setObject:mutDict forKey:@"recentlyPlayedMedia"];
1258     [defaults setObject:mediaList forKey:@"recentlyPlayedMediaList"];
1259     [defaults synchronize];
1260
1261     [mutDict release];
1262     [mediaList release];
1263 }
1264
1265 @end
1266
1267
1268 @implementation VLCPlaylist (NSOutlineViewDataSource)
1269 /* return the number of children for Obj-C pointer item */ /* DONE */
1270 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1271 {
1272     int i_return = 0;
1273     playlist_item_t *p_item = NULL;
1274     playlist_t * p_playlist = pl_Get(VLCIntf);
1275     //assert(outlineView == o_outline_view);
1276
1277     PL_LOCK;
1278     if (!item)
1279         p_item = p_current_root_item;
1280     else
1281         p_item = (playlist_item_t *)[item pointerValue];
1282
1283     if (p_item)
1284         i_return = p_item->i_children;
1285     PL_UNLOCK;
1286
1287     return i_return > 0 ? i_return : 0;
1288 }
1289
1290 /* return the child at index for the Obj-C pointer item */ /* DONE */
1291 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1292 {
1293     playlist_item_t *p_return = NULL, *p_item = NULL;
1294     NSValue *o_value;
1295     playlist_t * p_playlist = pl_Get(VLCIntf);
1296
1297     PL_LOCK;
1298     if (item == nil)
1299         p_item = p_current_root_item; /* root object */
1300     else
1301         p_item = (playlist_item_t *)[item pointerValue];
1302
1303     if (p_item && index < p_item->i_children && index >= 0)
1304         p_return = p_item->pp_children[index];
1305     PL_UNLOCK;
1306
1307     o_value = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_return]];
1308
1309     if (o_value == nil) {
1310         /* FIXME: Why is there a warning if that happens all the time and seems
1311          * to be normal? Add an assert and fix it.
1312          * msg_Warn(VLCIntf, "playlist item misses pointer value, adding one"); */
1313         o_value = [[NSValue valueWithPointer: p_return] retain];
1314     }
1315
1316     [o_outline_dict setObject:o_value forKey:[NSString stringWithFormat:@"%p", [o_value pointerValue]]];
1317
1318     return o_value;
1319 }
1320
1321 /* is the item expandable */
1322 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1323 {
1324     int i_return = 0;
1325     playlist_t *p_playlist = pl_Get(VLCIntf);
1326
1327     PL_LOCK;
1328     if (item == nil) {
1329         /* root object */
1330         if (p_current_root_item) {
1331             i_return = p_current_root_item->i_children;
1332         }
1333     } else {
1334         playlist_item_t *p_item = (playlist_item_t *)[item pointerValue];
1335         if (p_item)
1336             i_return = p_item->i_children;
1337     }
1338     PL_UNLOCK;
1339
1340     return (i_return >= 0);
1341 }
1342
1343 /* retrieve the string values for the cells */
1344 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)o_tc byItem:(id)item
1345 {
1346     id o_value = nil;
1347     char * psz_value;
1348     playlist_item_t *p_item;
1349
1350     /* For error handling */
1351     static BOOL attempted_reload = NO;
1352
1353     if (item == nil || ![item isKindOfClass: [NSValue class]]) {
1354         /* Attempt to fix the error by asking for a data redisplay
1355          * This might cause infinite loop, so add a small check */
1356         if (!attempted_reload) {
1357             attempted_reload = YES;
1358             [outlineView reloadData];
1359         }
1360         return @"error" ;
1361     }
1362
1363     p_item = (playlist_item_t *)[item pointerValue];
1364     if (!p_item || !p_item->p_input) {
1365         /* Attempt to fix the error by asking for a data redisplay
1366          * This might cause infinite loop, so add a small check */
1367         if (!attempted_reload) {
1368             attempted_reload = YES;
1369             [outlineView reloadData];
1370         }
1371         return @"error";
1372     }
1373
1374     attempted_reload = NO;
1375     NSString * o_identifier = [o_tc identifier];
1376
1377     if ([o_identifier isEqualToString:TRACKNUM_COLUMN]) {
1378         psz_value = input_item_GetTrackNumber(p_item->p_input);
1379         if (psz_value) {
1380             o_value = [NSString stringWithUTF8String:psz_value];
1381             free(psz_value);
1382         }
1383     } else if ([o_identifier isEqualToString:TITLE_COLUMN]) {
1384         /* sanity check to prevent the NSString class from crashing */
1385         char *psz_title =  input_item_GetTitleFbName(p_item->p_input);
1386         if (psz_title) {
1387             o_value = [NSString stringWithUTF8String:psz_title];
1388             free(psz_title);
1389         }
1390     } else if ([o_identifier isEqualToString:ARTIST_COLUMN]) {
1391         psz_value = input_item_GetArtist(p_item->p_input);
1392         if (psz_value) {
1393             o_value = [NSString stringWithUTF8String:psz_value];
1394             free(psz_value);
1395         }
1396     } else if ([o_identifier isEqualToString:@"duration"]) {
1397         char psz_duration[MSTRTIME_MAX_SIZE];
1398         mtime_t dur = input_item_GetDuration(p_item->p_input);
1399         if (dur != -1) {
1400             secstotimestr(psz_duration, dur/1000000);
1401             o_value = [NSString stringWithUTF8String:psz_duration];
1402         }
1403         else
1404             o_value = @"--:--";
1405     } else if ([o_identifier isEqualToString:GENRE_COLUMN]) {
1406         psz_value = input_item_GetGenre(p_item->p_input);
1407         if (psz_value) {
1408             o_value = [NSString stringWithUTF8String:psz_value];
1409             free(psz_value);
1410         }
1411     } else if ([o_identifier isEqualToString:ALBUM_COLUMN]) {
1412         psz_value = input_item_GetAlbum(p_item->p_input);
1413         if (psz_value) {
1414             o_value = [NSString stringWithUTF8String:psz_value];
1415             free(psz_value);
1416         }
1417     } else if ([o_identifier isEqualToString:DESCRIPTION_COLUMN]) {
1418         psz_value = input_item_GetDescription(p_item->p_input);
1419         if (psz_value) {
1420             o_value = [NSString stringWithUTF8String:psz_value];
1421             free(psz_value);
1422         }
1423     } else if ([o_identifier isEqualToString:DATE_COLUMN]) {
1424         psz_value = input_item_GetDate(p_item->p_input);
1425         if (psz_value) {
1426             o_value = [NSString stringWithUTF8String:psz_value];
1427             free(psz_value);
1428         }
1429     } else if ([o_identifier isEqualToString:LANGUAGE_COLUMN]) {
1430         psz_value = input_item_GetLanguage(p_item->p_input);
1431         if (psz_value) {
1432             o_value = [NSString stringWithUTF8String:psz_value];
1433             free(psz_value);
1434         }
1435     }
1436     else if ([o_identifier isEqualToString:URI_COLUMN]) {
1437         psz_value = decode_URI(input_item_GetURI(p_item->p_input));
1438         if (psz_value) {
1439             o_value = [NSString stringWithUTF8String:psz_value];
1440             free(psz_value);
1441         }
1442     }
1443     else if ([o_identifier isEqualToString:FILESIZE_COLUMN]) {
1444         psz_value = input_item_GetURI(p_item->p_input);
1445         o_value = @"";
1446         if (psz_value) {
1447             NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:psz_value]];
1448             if ([url isFileURL]) {
1449                 NSFileManager *fileManager = [NSFileManager defaultManager];
1450                 if ([fileManager fileExistsAtPath:[url path]]) {
1451                     NSError *error;
1452                     NSDictionary *attributes = [fileManager attributesOfItemAtPath:[url path] error:&error];
1453                     o_value = [VLCByteCountFormatter stringFromByteCount:[attributes fileSize] countStyle:NSByteCountFormatterCountStyleDecimal];
1454                 }
1455             }
1456             free(psz_value);
1457         }
1458     }
1459     else if ([o_identifier isEqualToString:@"status"]) {
1460         if (input_item_HasErrorWhenReading(p_item->p_input)) {
1461             o_value = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kAlertCautionIcon)];
1462             [o_value setSize: NSMakeSize(16,16)];
1463         }
1464     }
1465
1466     return o_value;
1467 }
1468
1469
1470 /* Required for drag & drop and reordering */
1471 - (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard
1472 {
1473     playlist_t *p_playlist = pl_Get(VLCIntf);
1474
1475     /* First remove the items that were moved during the last drag & drop
1476        operation */
1477     [o_items_array removeAllObjects];
1478     [o_nodes_array removeAllObjects];
1479
1480     NSUInteger itemCount = [items count];
1481
1482     for (NSUInteger i = 0 ; i < itemCount ; i++) {
1483         id o_item = [items objectAtIndex:i];
1484
1485         /* Fill the items and nodes to move in 2 different arrays */
1486         if (![o_item isLeaf])
1487             [o_nodes_array addObject: o_item];
1488         else
1489             [o_items_array addObject: o_item];
1490     }
1491
1492     /* Now we need to check if there are selected items that are in already
1493        selected nodes. In that case, we only want to move the nodes */
1494     [self removeItemsFrom: o_nodes_array ifChildrenOf: o_nodes_array];
1495     [self removeItemsFrom: o_items_array ifChildrenOf: o_nodes_array];
1496
1497     /* We add the "VLCPlaylistItemPboardType" type to be able to recognize
1498        a Drop operation coming from the playlist. */
1499
1500     [pboard declareTypes: [NSArray arrayWithObject:@"VLCPlaylistItemPboardType"] owner: self];
1501     [pboard setData:[NSData data] forType:@"VLCPlaylistItemPboardType"];
1502
1503     return YES;
1504 }
1505
1506 - (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
1507 {
1508     playlist_t *p_playlist = pl_Get(VLCIntf);
1509     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1510
1511     if (!p_playlist) return NSDragOperationNone;
1512
1513     /* Dropping ON items is not allowed if item is not a node */
1514     if (item) {
1515         if (index == NSOutlineViewDropOnItemIndex &&
1516                 ((playlist_item_t *)[item pointerValue])->i_children == -1) {
1517             return NSDragOperationNone;
1518         }
1519     }
1520
1521     /* We refuse to drop an item in anything else than a child of the General
1522        Node. We still accept items that would be root nodes of the outlineview
1523        however, to allow drop in an empty playlist. */
1524     /// todo
1525     //    if (!(([self isItem: [item pointerValue] inNode: p_playlist->p_local_category checkItemExistence: NO locked: NO] ||
1526 //            (var_CreateGetBool(p_playlist, "media-library") && [self isItem: [item pointerValue] inNode: p_playlist->p_ml_category checkItemExistence: NO locked: NO])) || item == nil)) {
1527 //        return NSDragOperationNone;
1528 //    }
1529
1530     /* Drop from the Playlist */
1531     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1532         NSUInteger count = [o_nodes_array count];
1533         for (NSUInteger i = 0 ; i < count ; i++) {
1534             /* We refuse to Drop in a child of an item we are moving */
1535             if ([self isItem: item inNode: [o_nodes_array objectAtIndex:i] checkItemExistence: NO locked:NO]) {
1536                 return NSDragOperationNone;
1537             }
1538         }
1539         return NSDragOperationMove;
1540     }
1541     /* Drop from the Finder */
1542     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1543         return NSDragOperationGeneric;
1544     }
1545     return NSDragOperationNone;
1546 }
1547
1548 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(NSInteger)index
1549 {
1550     playlist_t * p_playlist =  pl_Get(VLCIntf);
1551     NSPasteboard *o_pasteboard = [info draggingPasteboard];
1552
1553     /* Drag & Drop inside the playlist */
1554     if ([[o_pasteboard types] containsObject: @"VLCPlaylistItemPboardType"]) {
1555         if (index == -1) // this is no valid target, sanitize to top of table
1556             index = 0;
1557
1558         int i_row = 0;
1559         playlist_item_t *p_new_parent, *p_item = NULL;
1560         NSArray *o_all_items = [o_nodes_array arrayByAddingObjectsFromArray: o_items_array];
1561         /* If the item is to be dropped as root item of the outline, make it a
1562            child of the respective general node, if is either the pl or the ml
1563            Else, choose the proposed parent as parent. */
1564         if (item == nil) {
1565             // TODO edit allowed / no drop in other types
1566             if ([[self model] currentRootType] == ROOT_TYPE_PLAYLIST ||
1567                 [[self model] currentRootType] == ROOT_TYPE_MEDIALIBRARY)
1568                 item = [[self model] rootItem];
1569             else
1570                 return NO;
1571         }
1572
1573         /* Make sure the proposed parent is a node.
1574            (This should never be true) */
1575         if (p_new_parent->i_children < 0)
1576             return NO;
1577
1578         NSUInteger count = [o_all_items count];
1579         if (count == 0)
1580             return NO;
1581
1582         playlist_item_t **pp_items = (playlist_item_t **)calloc(count, sizeof(playlist_item_t*));
1583         if (!pp_items)
1584             return NO;
1585
1586         PL_LOCK;
1587         p_new_parent = playlist_ItemGetById(p_playlist, [item plItemId]);
1588         if (!p_new_parent) {
1589             PL_UNLOCK;
1590             return NO;
1591         }
1592
1593         NSUInteger j = 0;
1594         for (NSUInteger i = 0; i < count; i++) {
1595             p_item = playlist_ItemGetById(p_playlist, [[o_all_items objectAtIndex:i] plItemId]);
1596             if (p_item)
1597                 pp_items[j++] = p_item;
1598         }
1599
1600         if (j == 0 || playlist_TreeMoveMany(p_playlist, j, pp_items, p_new_parent, index) != VLC_SUCCESS) {
1601             PL_UNLOCK;
1602             free(pp_items);
1603             return NO;
1604         }
1605
1606         PL_UNLOCK;
1607         free(pp_items);
1608
1609         [self playlistUpdated];
1610         i_row = [o_outline_view rowForItem:[o_all_items objectAtIndex:0]];
1611
1612         if (i_row == -1)
1613             i_row = [o_outline_view rowForItem:item];
1614
1615         [o_outline_view deselectAll: self];
1616         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_row] byExtendingSelection:NO];
1617         [o_outline_view scrollRowToVisible: i_row];
1618
1619         return YES;
1620     }
1621
1622     else if ([[o_pasteboard types] containsObject: NSFilenamesPboardType]) {
1623         // TODO: can this already checked in drop validation?
1624         if (![[self model] editAllowed])
1625             return NO;
1626
1627         playlist_item_t *p_node = [item pointerValue];
1628
1629         NSArray *o_values = [[o_pasteboard propertyListForType: NSFilenamesPboardType]
1630                                 sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)];
1631         NSUInteger count = [o_values count];
1632         NSMutableArray *o_array = [NSMutableArray arrayWithCapacity:count];
1633         input_thread_t * p_input = pl_CurrentInput(VLCIntf);
1634
1635         if (count == 1 && p_input) {
1636             int i_result = input_AddSubtitleOSD(p_input, vlc_path2uri([[o_values objectAtIndex:0] UTF8String], NULL), true, true);
1637             vlc_object_release(p_input);
1638             if (i_result == VLC_SUCCESS)
1639                 return YES;
1640         }
1641         else if (p_input)
1642             vlc_object_release(p_input);
1643
1644         for (NSUInteger i = 0; i < count; i++) {
1645             NSDictionary *o_dic;
1646             char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
1647             if (!psz_uri)
1648                 continue;
1649
1650             o_dic = [NSDictionary dictionaryWithObject:[NSString stringWithCString:psz_uri encoding:NSUTF8StringEncoding] forKey:@"ITEM_URL"];
1651
1652             free(psz_uri);
1653
1654             [o_array addObject: o_dic];
1655         }
1656
1657         if (item == nil)
1658             [self appendArray:o_array atPos:index enqueue: YES];
1659         else {
1660             assert(p_node->i_children != -1);
1661             [self appendNodeArray:o_array inNode: p_node atPos:index enqueue:YES];
1662         }
1663         return YES;
1664     }
1665     return NO;
1666 }
1667
1668 @end