66d72a133efd0ff1d1bdbea772bf07c9f030f03f
[vlc.git] / 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     NSImage *o_descendingSortingImage;
163     NSImage *o_ascendingSortingImage;
164
165     BOOL b_selected_item_met;
166     BOOL b_isSortDescending;
167     id o_tc_sortColumn;
168     NSUInteger retainedRowSelection;
169
170     BOOL b_playlistmenu_nib_loaded;
171     BOOL b_view_setup;
172 }
173
174 - (void)saveTableColumns;
175 @end
176
177 @implementation VLCPlaylist
178
179 + (void)initialize
180 {
181     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
182     NSMutableArray *o_columnArray = [[NSMutableArray alloc] init];
183     [o_columnArray addObject: [NSArray arrayWithObjects:TITLE_COLUMN, [NSNumber numberWithFloat:190.], nil]];
184     [o_columnArray addObject: [NSArray arrayWithObjects:ARTIST_COLUMN, [NSNumber numberWithFloat:95.], nil]];
185     [o_columnArray addObject: [NSArray arrayWithObjects:DURATION_COLUMN, [NSNumber numberWithFloat:95.], nil]];
186
187     NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
188                                  [NSArray arrayWithArray:o_columnArray], @"PlaylistColumnSelection",
189                                  [NSArray array], @"recentlyPlayedMediaList",
190                                  [NSDictionary dictionary], @"recentlyPlayedMedia", nil];
191
192     [defaults registerDefaults:appDefaults];
193     [o_columnArray release];
194 }
195
196 - (PLModel *)model
197 {
198     return o_model;
199 }
200
201 - (void)reloadStyles
202 {
203     NSFont *fontToUse;
204     CGFloat rowHeight;
205     if (config_GetInt(VLCIntf, "macosx-large-text")) {
206         fontToUse = [NSFont systemFontOfSize:13.];
207         rowHeight = 21.;
208     } else {
209         fontToUse = [NSFont systemFontOfSize:11.];
210         rowHeight = 16.;
211     }
212
213     NSArray *columns = [o_outline_view tableColumns];
214     NSUInteger count = columns.count;
215     for (NSUInteger x = 0; x < count; x++)
216         [[[columns objectAtIndex:x] dataCell] setFont:fontToUse];
217     [o_outline_view setRowHeight:rowHeight];
218 }
219
220 - (void)dealloc
221 {
222     [super dealloc];
223 }
224
225 - (void)awakeFromNib
226 {
227     if (b_view_setup)
228         return;
229
230     playlist_t * p_playlist = pl_Get(VLCIntf);
231
232     [self reloadStyles];
233     [self initStrings];
234
235     o_model = [[PLModel alloc] initWithOutlineView:o_outline_view playlist:p_playlist rootItem:p_playlist->p_playing playlistObject:self];
236     [o_outline_view setDataSource:o_model];
237     [o_outline_view reloadData];
238
239     [o_outline_view setTarget: self];
240     [o_outline_view setDoubleAction: @selector(playItem:)];
241
242     [o_outline_view setAllowsEmptySelection: NO];
243     [o_outline_view registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
244     [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
245
246     /* This uses a private API, but works fine on all current OSX releases.
247      * Radar ID 11739459 request a public API for this. However, it is probably
248      * easier and faster to recreate similar looking bitmaps ourselves. */
249     o_ascendingSortingImage = [[NSOutlineView class] _defaultTableHeaderSortImage];
250     o_descendingSortingImage = [[NSOutlineView class] _defaultTableHeaderReverseSortImage];
251
252     o_tc_sortColumn = nil;
253
254     NSArray * o_columnArray = [[NSUserDefaults standardUserDefaults] arrayForKey:@"PlaylistColumnSelection"];
255     NSUInteger count = [o_columnArray count];
256
257     id o_menu = [[VLCMain sharedInstance] mainMenu];
258     NSString * o_column;
259
260     NSMenu *o_context_menu = [o_menu setupPlaylistTableColumnsMenu];
261     [o_playlist_header setMenu: o_context_menu];
262
263     for (NSUInteger i = 0; i < count; i++) {
264         o_column = [[o_columnArray objectAtIndex:i] objectAtIndex:0];
265         if ([o_column isEqualToString:@"status"])
266             continue;
267
268         if(![o_menu setPlaylistColumnTableState: NSOnState forColumn: o_column])
269             continue;
270
271         [[o_outline_view tableColumnWithIdentifier: o_column] setWidth: [[[o_columnArray objectAtIndex:i] objectAtIndex:1] floatValue]];
272     }
273
274     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];
275
276     b_view_setup = YES;
277 }
278
279 - (void)applicationWillTerminate:(NSNotification *)notification
280 {
281     /* 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 */
282     [self saveTableColumns];
283 }
284
285 - (void)initStrings
286 {
287     [o_mi_play setTitle: _NS("Play")];
288     [o_mi_delete setTitle: _NS("Delete")];
289     [o_mi_recursive_expand setTitle: _NS("Expand Node")];
290     [o_mi_selectall setTitle: _NS("Select All")];
291     [o_mi_info setTitle: _NS("Media Information...")];
292     [o_mi_dl_cover_art setTitle: _NS("Download Cover Art")];
293     [o_mi_preparse setTitle: _NS("Fetch Meta Data")];
294     [o_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
295     [o_mm_mi_revealInFinder setTitle: _NS("Reveal in Finder")];
296     [[o_mm_mi_revealInFinder menu] setAutoenablesItems: NO];
297     [o_mi_sort_name setTitle: _NS("Sort Node by Name")];
298     [o_mi_sort_author setTitle: _NS("Sort Node by Author")];
299
300     [o_search_field setToolTip: _NS("Search in Playlist")];
301 }
302
303 - (void)playlistUpdated
304 {
305     /* Clear indications of any existing column sorting */
306     NSUInteger count = [[o_outline_view tableColumns] count];
307     for (NSUInteger i = 0 ; i < count ; i++)
308         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
309
310     [o_outline_view setHighlightedTableColumn:nil];
311     o_tc_sortColumn = nil;
312
313     [o_outline_view reloadData];
314     [[[[VLCMain sharedInstance] wizard] playlistWizard] reloadOutlineView];
315
316     [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:retainedRowSelection] byExtendingSelection:NO];
317
318     [self outlineViewSelectionDidChange: nil];
319     [[VLCMain sharedInstance] updateMainWindow];
320 }
321
322 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
323 {
324 //    // FIXME: unsafe
325 //    playlist_item_t * p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
326 //
327 //    if (p_item) {
328 //        /* update the state of our Reveal-in-Finder menu items */
329 //        NSMutableString *o_mrl;
330 //        char *psz_uri = input_item_GetURI(p_item->p_input);
331 //
332 //        [o_mi_revealInFinder setEnabled: NO];
333 //        [o_mm_mi_revealInFinder setEnabled: NO];
334 //        if (psz_uri) {
335 //            o_mrl = [NSMutableString stringWithUTF8String: psz_uri];
336 //
337 //            /* perform some checks whether it is a file and if it is local at all... */
338 //            NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
339 //            if (prefix_range.location != NSNotFound)
340 //                [o_mrl deleteCharactersInRange: prefix_range];
341 //
342 //            if ([o_mrl characterAtIndex:0] == '/') {
343 //                [o_mi_revealInFinder setEnabled: YES];
344 //                [o_mm_mi_revealInFinder setEnabled: YES];
345 //            }
346 //            free(psz_uri);
347 //        }
348 //
349 //        /* update our info-panel to reflect the new item */
350 //        [[[VLCMain sharedInstance] info] updatePanelWithItem:p_item->p_input];
351 //    }
352 }
353
354 - (BOOL)isSelectionEmpty
355 {
356     return [o_outline_view selectedRow] == -1;
357 }
358
359 - (void)updateRowSelection
360 {
361     // FIXME: unsafe
362     playlist_t *p_playlist = pl_Get(VLCIntf);
363     playlist_item_t *p_item, *p_temp_item;
364     NSMutableArray *o_array = [NSMutableArray array];
365
366     // TODO Rework
367 //    PL_LOCK;
368 //    p_item = playlist_CurrentPlayingItem(p_playlist);
369 //    if (p_item == NULL) {
370 //        PL_UNLOCK;
371 //        return;
372 //    }
373 //
374 //    p_temp_item = p_item;
375 //    while(p_temp_item->p_parent) {
376 //        [o_array insertObject: [NSValue valueWithPointer: p_temp_item] atIndex: 0];
377 //        p_temp_item = p_temp_item->p_parent;
378 //    }
379 //    PL_UNLOCK;
380 //
381 //    NSUInteger count = [o_array count];
382 //    for (NSUInteger j = 0; j < count - 1; j++) {
383 //        id o_item;
384 //        if ((o_item = [o_outline_dict objectForKey:
385 //                            [NSString stringWithFormat: @"%p",
386 //                            [[o_array objectAtIndex:j] pointerValue]]]) != nil) {
387 //            [o_outline_view expandItem: o_item];
388 //        }
389 //    }
390 //
391 //    id o_item = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_item]];
392 //    NSInteger i_index = [o_outline_view rowForItem:o_item];
393 //    [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:i_index] byExtendingSelection:NO];
394 //    [o_outline_view setNeedsDisplay:YES];
395 }
396
397 - (IBAction)savePlaylist:(id)sender
398 {
399     playlist_t * p_playlist = pl_Get(VLCIntf);
400
401     NSSavePanel *o_save_panel = [NSSavePanel savePanel];
402     NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
403
404     [NSBundle loadNibNamed:@"PlaylistAccessoryView" owner:self];
405
406     [o_save_accessory_text setStringValue: _NS("File Format:")];
407     [[o_save_accessory_popup itemAtIndex:0] setTitle: _NS("Extended M3U")];
408     [[o_save_accessory_popup itemAtIndex:1] setTitle: _NS("XML Shareable Playlist Format (XSPF)")];
409     [[o_save_accessory_popup itemAtIndex:2] setTitle: _NS("HTML playlist")];
410
411     [o_save_panel setTitle: _NS("Save Playlist")];
412     [o_save_panel setPrompt: _NS("Save")];
413     [o_save_panel setAccessoryView: o_save_accessory_view];
414     [o_save_panel setNameFieldStringValue: o_name];
415
416     if ([o_save_panel runModal] == NSFileHandlingPanelOKButton) {
417         NSString *o_filename = [[o_save_panel URL] path];
418
419         if ([o_save_accessory_popup indexOfSelectedItem] == 0) {
420             NSString * o_real_filename;
421             NSRange range;
422             range.location = [o_filename length] - [@".m3u" length];
423             range.length = [@".m3u" length];
424
425             if ([o_filename compare:@".m3u" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
426                 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
427             else
428                 o_real_filename = o_filename;
429
430             playlist_Export(p_playlist,
431                 [o_real_filename fileSystemRepresentation],
432                 p_playlist->p_local_category, "export-m3u");
433         } else if ([o_save_accessory_popup indexOfSelectedItem] == 1) {
434             NSString * o_real_filename;
435             NSRange range;
436             range.location = [o_filename length] - [@".xspf" length];
437             range.length = [@".xspf" length];
438
439             if ([o_filename compare:@".xspf" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
440                 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
441             else
442                 o_real_filename = o_filename;
443
444             playlist_Export(p_playlist,
445                 [o_real_filename fileSystemRepresentation],
446                 p_playlist->p_local_category, "export-xspf");
447         } else {
448             NSString * o_real_filename;
449             NSRange range;
450             range.location = [o_filename length] - [@".html" length];
451             range.length = [@".html" length];
452
453             if ([o_filename compare:@".html" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
454                 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
455             else
456                 o_real_filename = o_filename;
457
458             playlist_Export(p_playlist,
459                 [o_real_filename fileSystemRepresentation],
460                 p_playlist->p_local_category, "export-html");
461         }
462     }
463 }
464
465 /* When called retrieves the selected outlineview row and plays that node or item */
466 - (IBAction)playItem:(id)sender
467 {
468     playlist_t *p_playlist = pl_Get(VLCIntf);
469
470     // ignore clicks on column header when handling double action
471     if (sender == o_outline_view && [o_outline_view clickedRow] == -1)
472         return;
473
474     PLItem *o_item = [o_outline_view itemAtRow:[o_outline_view selectedRow]];
475     if (!o_item)
476         return;
477
478     PL_LOCK;
479     playlist_item_t *p_item = playlist_ItemGetById(p_playlist, [o_item plItemId]);
480     playlist_item_t *p_node = playlist_ItemGetById(p_playlist, [[[self model] rootItem] plItemId]);
481
482     if (p_item && p_node) {
483         playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
484     }
485     PL_UNLOCK;
486 }
487
488 - (IBAction)revealItemInFinder:(id)sender
489 {
490     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
491     NSUInteger count = [selectedRows count];
492     NSUInteger indexes[count];
493     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
494
495     NSMutableString * o_mrl;
496     for (NSUInteger i = 0; i < count; i++) {
497         PLItem *o_item = [o_outline_view itemAtRow:indexes[i]];
498
499         char * psz_url = decode_URI(input_item_GetURI([o_item input]));
500         o_mrl = [[NSMutableString alloc] initWithString: [NSString stringWithUTF8String:psz_url ? psz_url : ""]];
501         if (psz_url != NULL)
502             free( psz_url );
503
504         /* perform some checks whether it is a file and if it is local at all... */
505         if ([o_mrl length] > 0) {
506             NSRange prefix_range = [o_mrl rangeOfString: @"file:"];
507             if (prefix_range.location != NSNotFound)
508                 [o_mrl deleteCharactersInRange: prefix_range];
509
510             if ([o_mrl characterAtIndex:0] == '/')
511                 [[NSWorkspace sharedWorkspace] selectFile: o_mrl inFileViewerRootedAtPath: o_mrl];
512         }
513
514         [o_mrl release];
515     }
516 }
517
518 /* When called retrieves the selected outlineview row and plays that node or item */
519 - (IBAction)preparseItem:(id)sender
520 {
521     int i_count;
522     NSIndexSet *o_selected_indexes;
523     intf_thread_t * p_intf = VLCIntf;
524     playlist_t * p_playlist = pl_Get(p_intf);
525     playlist_item_t *p_item = NULL;
526
527     o_selected_indexes = [o_outline_view selectedRowIndexes];
528     i_count = [o_selected_indexes count];
529
530     NSUInteger indexes[i_count];
531     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
532     for (int i = 0; i < i_count; i++) {
533         PLItem *o_item = [o_outline_view itemAtRow:indexes[i]];
534         [o_outline_view deselectRow: indexes[i]];
535
536         if (![o_item isLeaf]) {
537             msg_Dbg(p_intf, "preparsing nodes not implemented");
538             continue;
539         }
540
541         libvlc_MetaRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
542
543     }
544     [self playlistUpdated];
545 }
546
547 - (IBAction)downloadCoverArt:(id)sender
548 {
549     int i_count;
550     NSIndexSet *o_selected_indexes;
551     intf_thread_t * p_intf = VLCIntf;
552     playlist_t * p_playlist = pl_Get(p_intf);
553     playlist_item_t *p_item = NULL;
554
555     o_selected_indexes = [o_outline_view selectedRowIndexes];
556     i_count = [o_selected_indexes count];
557
558     NSUInteger indexes[i_count];
559     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
560     for (int i = 0; i < i_count; i++) {
561         PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
562
563         if (![o_item isLeaf])
564             continue;
565
566         libvlc_ArtRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
567     }
568     [self playlistUpdated];
569 }
570
571 - (IBAction)selectAll:(id)sender
572 {
573     [o_outline_view selectAll: nil];
574 }
575
576 - (IBAction)showInfoPanel:(id)sender
577 {
578     [[[VLCMain sharedInstance] info] initPanel];
579 }
580
581 - (IBAction)deleteItem:(id)sender
582 {
583     int i_count;
584     NSIndexSet *o_selected_indexes;
585     intf_thread_t * p_intf = VLCIntf;
586     playlist_t * p_playlist = pl_Get(p_intf);
587
588     // check if deletion is allowed
589     if (![[self model] editAllowed])
590         return;
591
592     o_selected_indexes = [o_outline_view selectedRowIndexes];
593     i_count = [o_selected_indexes count];
594     retainedRowSelection = [o_selected_indexes firstIndex];
595     if (retainedRowSelection == NSNotFound)
596         retainedRowSelection = 0;
597
598
599     NSUInteger indexes[i_count];
600 //    if (i_count == [o_outline_view numberOfRows]) {
601 //        PL_LOCK;
602 //        playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
603 //        PL_UNLOCK;
604 //        [self playlistUpdated];
605 //        return;
606 //    }
607     [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
608     for (int i = 0; i < i_count; i++) {
609         PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
610         [o_outline_view deselectRow: indexes[i]];
611
612         /// TODO
613 //        if (p_item->i_children != -1) {
614 //        //is a node and not an item
615 //            if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
616 //                [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
617 //                        checkItemExistence: NO locked:YES] == YES)
618 //                // if current item is in selected node and is playing then stop playlist
619 //                playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
620 //
621 //                playlist_NodeDelete(p_playlist, p_item, true, false);
622 //        } else
623
624             playlist_DeleteFromInput(p_playlist, [o_item input], pl_Unlocked);
625 //        [[o_item parent] deleteChild:o_item];
626 //
627 //        [o_outline_view reloadData];
628     }
629
630 //    [self playlistUpdated];
631 }
632
633 - (IBAction)sortNodeByName:(id)sender
634 {
635     [self sortNode: SORT_TITLE];
636 }
637
638 - (IBAction)sortNodeByAuthor:(id)sender
639 {
640     [self sortNode: SORT_ARTIST];
641 }
642
643 - (void)sortNode:(int)i_mode
644 {
645     playlist_t * p_playlist = pl_Get(VLCIntf);
646     playlist_item_t * p_item;
647
648     // TODO why do we need this kind of sort? It looks crap and confusing...
649
650 //    if ([o_outline_view selectedRow] > -1) {
651 //        p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
652 //        if (!p_item)
653 //            return;
654 //    } else
655 //        p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
656 //
657 //    PL_LOCK;
658 //    if (p_item->i_children > -1) // the item is a node
659 //        playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
660 //    else
661 //        playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
662 //
663 //    PL_UNLOCK;
664 //    [self playlistUpdated];
665 }
666
667 - (input_item_t *)createItem:(NSDictionary *)o_one_item
668 {
669     intf_thread_t * p_intf = VLCIntf;
670     playlist_t * p_playlist = pl_Get(p_intf);
671
672     input_item_t *p_input;
673     BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
674     NSString *o_uri, *o_name, *o_path;
675     NSURL * o_nsurl;
676     NSArray *o_options;
677     NSURL *o_true_file;
678
679     /* Get the item */
680     o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
681     o_nsurl = [NSURL URLWithString: o_uri];
682     o_path = [o_nsurl path];
683     o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
684     o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
685
686     if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
687         [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
688                                                      isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
689
690         NSString *diskType = [VLCOpen getVolumeTypeFromMountPath: o_path];
691         msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
692
693         if ([diskType isEqualToString: kVLCMediaDVD])
694             o_uri = [NSString stringWithFormat: @"dvdnav://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
695         else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
696             o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
697         else if ([diskType isEqualToString: kVLCMediaAudioCD])
698             o_uri = [NSString stringWithFormat: @"cdda://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
699         else if ([diskType isEqualToString: kVLCMediaVCD])
700             o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
701         else if ([diskType isEqualToString: kVLCMediaSVCD])
702             o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
703         else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
704             o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
705         else
706             msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
707
708         p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
709     }
710     else
711         p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
712
713     if (!p_input)
714         return NULL;
715
716     if (o_options) {
717         NSUInteger count = [o_options count];
718         for (NSUInteger i = 0; i < count; i++)
719             input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
720     }
721
722     /* Recent documents menu */
723     if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
724         [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
725
726     return p_input;
727 }
728
729 - (void)addPlaylistItems:(NSArray*)o_array
730 {
731
732     int i_plItemId = -1;
733
734     // add items directly to media library if this is the current root
735     if ([[self model] currentRootType] == ROOT_TYPE_MEDIALIBRARY)
736         i_plItemId = [[[self model] rootItem] plItemId];
737
738     BOOL b_autoplay = var_InheritBool(VLCIntf, "macosx-autoplay");
739
740     [self addPlaylistItems:o_array withParentItemId:i_plItemId atPos:-1 startPlayback:b_autoplay];
741 }
742
743 - (void)addPlaylistItems:(NSArray*)o_array withParentItemId:(int)i_plItemId atPos:(int)i_position startPlayback:(BOOL)b_start
744 {
745     playlist_t * p_playlist = pl_Get(VLCIntf);
746     PL_LOCK;
747
748     playlist_item_t *p_parent = NULL;
749     if (i_plItemId >= 0)
750         p_parent = playlist_ItemGetById(p_playlist, i_plItemId);
751     else
752         p_parent = p_playlist->p_playing;
753
754     if (!p_parent) {
755         PL_UNLOCK;
756         return;
757     }
758
759     NSUInteger count = [o_array count];
760     int i_current_offset = 0;
761     for (NSUInteger i = 0; i < count; ++i) {
762
763         NSDictionary *o_current_item = [o_array objectAtIndex:i];
764         input_item_t *p_input = [self createItem: o_current_item];
765         if (!p_input)
766             continue;
767
768         int i_pos = (i_position == -1) ? PLAYLIST_END : i_position + i_current_offset++;
769         playlist_item_t *p_item = playlist_NodeAddInput(p_playlist, p_input, p_parent,
770                                                         PLAYLIST_INSERT, i_pos, pl_Locked);
771         if (!p_item)
772             continue;
773
774         if (i == 0 && b_start) {
775             playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_parent, p_item);
776         }
777         input_item_Release(p_input);
778     }
779     PL_UNLOCK;
780 }
781
782
783 - (IBAction)searchItem:(id)sender
784 {
785     [[self model] searchUpdate:[o_search_field stringValue]];
786 }
787
788 - (IBAction)recursiveExpandNode:(id)sender
789 {
790     NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
791     NSUInteger count = [selectedRows count];
792     NSUInteger indexes[count];
793     [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
794
795     id o_item;
796     playlist_item_t *p_item;
797     for (NSUInteger i = 0; i < count; i++) {
798         o_item = [o_outline_view itemAtRow: indexes[i]];
799
800         /* We need to collapse the node first, since OSX refuses to recursively
801          expand an already expanded node, even if children nodes are collapsed. */
802         if ([o_outline_view isExpandable:o_item]) {
803             [o_outline_view collapseItem: o_item collapseChildren: YES];
804             [o_outline_view expandItem: o_item expandChildren: YES];
805         }
806
807         selectedRows = [o_outline_view selectedRowIndexes];
808         [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
809     }
810 }
811
812 - (NSMenu *)menuForEvent:(NSEvent *)o_event
813 {
814     if (!b_playlistmenu_nib_loaded)
815         b_playlistmenu_nib_loaded = [NSBundle loadNibNamed:@"PlaylistMenu" owner:self];
816
817     NSPoint pt;
818     bool b_rows;
819     bool b_item_sel;
820
821     pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
822     int row = [o_outline_view rowAtPoint:pt];
823     if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
824         [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
825
826     b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
827     b_rows = [o_outline_view numberOfRows] != 0;
828
829     playlist_t *p_playlist = pl_Get(VLCIntf);
830     bool b_del_allowed = [[self model] editAllowed];
831
832     [o_mi_play setEnabled: b_item_sel];
833     [o_mi_delete setEnabled: b_item_sel && b_del_allowed];
834     [o_mi_selectall setEnabled: b_rows];
835     [o_mi_info setEnabled: b_item_sel];
836     [o_mi_preparse setEnabled: b_item_sel];
837     [o_mi_recursive_expand setEnabled: b_item_sel];
838     [o_mi_sort_name setEnabled: b_item_sel];
839     [o_mi_sort_author setEnabled: b_item_sel];
840
841     return o_ctx_menu;
842 }
843
844 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
845 {
846     int i_mode, i_type = 0;
847     intf_thread_t *p_intf = VLCIntf;
848     NSString * o_identifier = [o_tc identifier];
849
850     playlist_t *p_playlist = pl_Get(p_intf);
851
852     if (o_tc_sortColumn == o_tc)
853         b_isSortDescending = !b_isSortDescending;
854     else
855         b_isSortDescending = false;
856
857     if (b_isSortDescending)
858         i_type = ORDER_REVERSE;
859     else
860         i_type = ORDER_NORMAL;
861
862     [[self model] sortForColumn:o_identifier withMode:i_type];
863
864     // TODO rework, why do we need a full call here?
865 //    [self playlistUpdated];
866
867     /* Clear indications of any existing column sorting */
868     NSUInteger count = [[o_outline_view tableColumns] count];
869     for (NSUInteger i = 0 ; i < count ; i++)
870         [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
871
872     [o_outline_view setHighlightedTableColumn:nil];
873     o_tc_sortColumn = nil;
874
875
876     o_tc_sortColumn = o_tc;
877     [o_outline_view setHighlightedTableColumn:o_tc];
878
879     if (b_isSortDescending)
880         [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
881     else
882         [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
883 }
884
885
886 - (void)outlineView:(NSOutlineView *)outlineView
887     willDisplayCell:(id)cell
888      forTableColumn:(NSTableColumn *)tableColumn
889                item:(id)item
890 {
891     /* this method can be called when VLC is already dead, hence the extra checks */
892     intf_thread_t * p_intf = VLCIntf;
893     if (!p_intf)
894         return;
895     playlist_t *p_playlist = pl_Get(p_intf);
896
897     NSFont *fontToUse;
898     if (config_GetInt(VLCIntf, "macosx-large-text"))
899         fontToUse = [NSFont systemFontOfSize:13.];
900     else
901         fontToUse = [NSFont systemFontOfSize:11.];
902
903     BOOL b_is_playing = NO;
904     PL_LOCK;
905     playlist_item_t *p_current_item = playlist_CurrentPlayingItem(p_playlist);
906     if (p_current_item) {
907         b_is_playing = p_current_item->i_id == [item plItemId];
908     }
909     PL_UNLOCK;
910
911     /*
912      TODO: repaint all items bold:
913      [self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
914      || [o_playing_item isEqual: item]
915      */
916
917     if (b_is_playing)
918         [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toHaveTrait:NSBoldFontMask]];
919     else
920         [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toNotHaveTrait:NSBoldFontMask]];
921 }
922
923 // TODO remove method
924 - (NSArray *)draggedItems
925 {
926     return [[self model] draggedItems];
927 }
928
929 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
930 {
931     NSTableColumn * o_work_tc;
932
933     if (i_state == NSOnState) {
934         NSString *o_title = [o_dict objectForKey:o_column];
935         if (!o_title)
936             return;
937
938         o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
939         [o_work_tc setEditable: NO];
940         [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
941
942         [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
943
944         if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
945             [o_work_tc setWidth: 20.];
946             [o_work_tc setResizingMask: NSTableColumnNoResizing];
947             [[o_work_tc headerCell] setStringValue: @"#"];
948         }
949
950         [o_outline_view addTableColumn: o_work_tc];
951         [o_work_tc release];
952         [o_outline_view reloadData];
953         [o_outline_view setNeedsDisplay: YES];
954     }
955     else
956         [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
957
958     [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
959 }
960
961 - (void)saveTableColumns
962 {
963     NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
964     NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
965     NSUInteger count = [o_columns count];
966     NSTableColumn * o_currentColumn;
967     for (NSUInteger i = 0; i < count; i++) {
968         o_currentColumn = [o_columns objectAtIndex:i];
969         [o_arrayToSave addObject:[NSArray arrayWithObjects:[o_currentColumn identifier], [NSNumber numberWithFloat:[o_currentColumn width]], nil]];
970     }
971     [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
972     [[NSUserDefaults standardUserDefaults] synchronize];
973     [o_columns release];
974     [o_arrayToSave release];
975 }
976
977 - (BOOL)isValidResumeItem:(input_item_t *)p_item
978 {
979     char *psz_url = input_item_GetURI(p_item);
980     NSString *o_url_string = toNSStr(psz_url);
981     free(psz_url);
982
983     if ([o_url_string isEqualToString:@""])
984         return NO;
985
986     NSURL *o_url = [NSURL URLWithString:o_url_string];
987
988     if (![o_url isFileURL])
989         return NO;
990
991     BOOL isDir = false;
992     if (![[NSFileManager defaultManager] fileExistsAtPath:[o_url path] isDirectory:&isDir])
993         return NO;
994
995     if (isDir)
996         return NO;
997
998     return YES;
999 }
1000
1001 - (void)updateAlertWindow:(NSTimer *)timer
1002 {
1003     NSAlert *alert = [timer userInfo];
1004
1005     --currentResumeTimeout;
1006     if (currentResumeTimeout <= 0) {
1007         [[alert window] close];
1008         [NSApp abortModal];
1009     }
1010
1011     NSString *buttonLabel = _NS("Restart playback");
1012     buttonLabel = [buttonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1013
1014     [[[alert buttons] objectAtIndex:2] setTitle:buttonLabel];
1015 }
1016
1017 - (void)continuePlaybackWhereYouLeftOff:(input_thread_t *)p_input_thread
1018 {
1019     NSDictionary *recentlyPlayedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"recentlyPlayedMedia"];
1020     if (!recentlyPlayedFiles)
1021         return;
1022
1023     input_item_t *p_item = input_GetItem(p_input_thread);
1024     if (!p_item)
1025         return;
1026
1027     /* allow the user to over-write the start/stop/run-time */
1028     if (var_GetFloat(p_input_thread, "run-time") > 0 ||
1029         var_GetFloat(p_input_thread, "start-time") > 0 ||
1030         var_GetFloat(p_input_thread, "stop-time") > 0) {
1031         return;
1032     }
1033
1034     /* check for file existance before resuming */
1035     if (![self isValidResumeItem:p_item])
1036         return;
1037
1038     char *psz_url = decode_URI(input_item_GetURI(p_item));
1039     if (!psz_url)
1040         return;
1041     NSString *url = toNSStr(psz_url);
1042     free(psz_url);
1043
1044     NSNumber *lastPosition = [recentlyPlayedFiles objectForKey:url];
1045     if (!lastPosition || lastPosition.intValue <= 0)
1046         return;
1047
1048     int settingValue = config_GetInt(VLCIntf, "macosx-continue-playback");
1049     if (settingValue == 2) // never resume
1050         return;
1051
1052     NSInteger returnValue = NSAlertErrorReturn;
1053     if (settingValue == 0) { // ask
1054
1055         char *psz_title_name = input_item_GetTitleFbName(p_item);
1056         NSString *o_title = toNSStr(psz_title_name);
1057         free(psz_title_name);
1058
1059         currentResumeTimeout = 6;
1060         NSString *o_restartButtonLabel = _NS("Restart playback");
1061         o_restartButtonLabel = [o_restartButtonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1062         NSAlert *theAlert = [NSAlert alertWithMessageText:_NS("Continue playback?") defaultButton:_NS("Continue") alternateButton:o_restartButtonLabel otherButton:_NS("Always continue") informativeTextWithFormat:_NS("Playback of \"%@\" will continue at %@"), o_title, [[VLCStringUtility sharedInstance] stringForTime:lastPosition.intValue]];
1063
1064         NSTimer *timer = [NSTimer timerWithTimeInterval:1
1065                                                  target:self
1066                                                selector:@selector(updateAlertWindow:)
1067                                                userInfo:theAlert
1068                                                 repeats:YES];
1069
1070         [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSModalPanelRunLoopMode];
1071
1072         [[VLCCoreInteraction sharedInstance] pause];
1073         returnValue = [theAlert runModal];
1074         [timer invalidate];
1075         [[VLCCoreInteraction sharedInstance] playOrPause];
1076
1077         // restart button was pressed or timeout happened
1078         if (returnValue == NSAlertAlternateReturn ||
1079             returnValue == NSRunAbortedResponse)
1080             return;
1081     }
1082
1083     mtime_t lastPos = (mtime_t)lastPosition.intValue * 1000000;
1084     msg_Dbg(VLCIntf, "continuing playback at %lld", lastPos);
1085     var_SetTime(p_input_thread, "time", lastPos);
1086
1087     if (returnValue == NSAlertOtherReturn)
1088         config_PutInt(VLCIntf, "macosx-continue-playback", 1);
1089 }
1090
1091 - (void)storePlaybackPositionForItem:(input_thread_t *)p_input_thread
1092 {
1093     if (!var_InheritBool(VLCIntf, "macosx-recentitems"))
1094         return;
1095
1096     input_item_t *p_item = input_GetItem(p_input_thread);
1097     if (!p_item)
1098         return;
1099
1100     if (![self isValidResumeItem:p_item])
1101         return;
1102
1103     char *psz_url = decode_URI(input_item_GetURI(p_item));
1104     if (!psz_url)
1105         return;
1106     NSString *url = toNSStr(psz_url);
1107     free(psz_url);
1108
1109     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
1110     NSMutableDictionary *mutDict = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:@"recentlyPlayedMedia"]];
1111
1112     float relativePos = var_GetFloat(p_input_thread, "position");
1113     mtime_t pos = var_GetTime(p_input_thread, "time") / 1000000;
1114     mtime_t dur = input_item_GetDuration(p_item) / 1000000;
1115
1116     NSMutableArray *mediaList = [[defaults objectForKey:@"recentlyPlayedMediaList"] mutableCopy];
1117
1118     if (relativePos > .05 && relativePos < .95 && dur > 180) {
1119         [mutDict setObject:[NSNumber numberWithInt:pos] forKey:url];
1120
1121         [mediaList removeObject:url];
1122         [mediaList addObject:url];
1123         NSUInteger mediaListCount = mediaList.count;
1124         if (mediaListCount > 30) {
1125             for (NSUInteger x = 0; x < mediaListCount - 30; x++) {
1126                 [mutDict removeObjectForKey:[mediaList objectAtIndex:0]];
1127                 [mediaList removeObjectAtIndex:0];
1128             }
1129         }
1130     } else {
1131         [mutDict removeObjectForKey:url];
1132         [mediaList removeObject:url];
1133     }
1134     [defaults setObject:mutDict forKey:@"recentlyPlayedMedia"];
1135     [defaults setObject:mediaList forKey:@"recentlyPlayedMediaList"];
1136     [defaults synchronize];
1137
1138     [mutDict release];
1139     [mediaList release];
1140 }
1141
1142 @end