1 /*****************************************************************************
2 * playlist.m: MacOS X interface module
3 *****************************************************************************
4 * Copyright (C) 2002-2014 VLC authors and VideoLAN
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>
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.
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.
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 *****************************************************************************/
28 * add 'icons' for different types of nodes? (http://www.cocoadev.com/index.pl?IconAndTextInTableCell)
29 * reimplement enable/disable item
33 /*****************************************************************************
35 *****************************************************************************/
36 #include <stdlib.h> /* malloc(), free() */
37 #include <sys/param.h> /* for MAXPATHLEN */
40 #include <sys/mount.h>
42 #import "CompatibilityFixes.h"
47 #import "playlistinfo.h"
53 #import "CoreInteraction.h"
56 #import <vlc_interface.h>
59 /*****************************************************************************
60 * VLCPlaylistView implementation
61 *****************************************************************************/
62 @implementation VLCPlaylistView
64 - (NSMenu *)menuForEvent:(NSEvent *)o_event
66 return([(VLCPlaylist *)[self delegate] menuForEvent: o_event]);
69 - (void)keyDown:(NSEvent *)o_event
73 if ([[o_event characters] length])
74 key = [[o_event characters] characterAtIndex: 0];
77 case NSDeleteCharacter:
78 case NSDeleteFunctionKey:
79 case NSDeleteCharFunctionKey:
80 case NSBackspaceCharacter:
81 [(VLCPlaylist *)[self delegate] deleteItem:self];
84 case NSEnterCharacter:
85 case NSCarriageReturnCharacter:
86 [(VLCPlaylist *)[[VLCMain sharedInstance] playlist] playItem:nil];
90 [super keyDown: o_event];
95 - (BOOL)validateMenuItem:(NSMenuItem *)item
97 if (([self numberOfSelectedRows] >= 1 && [item action] == @selector(delete:)) || [item action] == @selector(selectAll:))
103 - (BOOL)acceptsFirstResponder
108 - (BOOL)becomeFirstResponder
110 [self setNeedsDisplay:YES];
114 - (BOOL)resignFirstResponder
116 [self setNeedsDisplay:YES];
120 - (IBAction)delete:(id)sender
122 [[[VLCMain sharedInstance] playlist] deleteItem: sender];
127 /*****************************************************************************
128 * VLCPlaylistWizard implementation
129 *****************************************************************************/
130 @implementation VLCPlaylistWizard
132 - (IBAction)reloadOutlineView
134 /* Only reload the outlineview if the wizard window is open since this can
135 be quite long on big playlists */
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 *****************************************************************************/
151 @interface NSOutlineView (UndocumentedSortImages)
152 + (NSImage *)_defaultTableHeaderSortImage;
153 + (NSImage *)_defaultTableHeaderReverseSortImage;
157 /*****************************************************************************
158 * VLCPlaylist implementation
159 *****************************************************************************/
160 @interface VLCPlaylist ()
162 NSImage *o_descendingSortingImage;
163 NSImage *o_ascendingSortingImage;
165 BOOL b_selected_item_met;
166 BOOL b_isSortDescending;
168 NSUInteger retainedRowSelection;
170 BOOL b_playlistmenu_nib_loaded;
174 - (void)saveTableColumns;
177 @implementation VLCPlaylist
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]];
187 NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
188 [NSArray arrayWithArray:o_columnArray], @"PlaylistColumnSelection",
189 [NSArray array], @"recentlyPlayedMediaList",
190 [NSDictionary dictionary], @"recentlyPlayedMedia", nil];
192 [defaults registerDefaults:appDefaults];
193 [o_columnArray release];
205 if (config_GetInt(VLCIntf, "macosx-large-text")) {
206 fontToUse = [NSFont systemFontOfSize:13.];
209 fontToUse = [NSFont systemFontOfSize:11.];
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];
230 playlist_t * p_playlist = pl_Get(VLCIntf);
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];
239 [o_outline_view setTarget: self];
240 [o_outline_view setDoubleAction: @selector(playItem:)];
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)];
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];
252 o_tc_sortColumn = nil;
254 NSArray * o_columnArray = [[NSUserDefaults standardUserDefaults] arrayForKey:@"PlaylistColumnSelection"];
255 NSUInteger count = [o_columnArray count];
257 id o_menu = [[VLCMain sharedInstance] mainMenu];
260 NSMenu *o_context_menu = [o_menu setupPlaylistTableColumnsMenu];
261 [o_playlist_header setMenu: o_context_menu];
263 for (NSUInteger i = 0; i < count; i++) {
264 o_column = [[o_columnArray objectAtIndex:i] objectAtIndex:0];
265 if ([o_column isEqualToString:@"status"])
268 if(![o_menu setPlaylistColumnTableState: NSOnState forColumn: o_column])
271 [[o_outline_view tableColumnWithIdentifier: o_column] setWidth: [[[o_columnArray objectAtIndex:i] objectAtIndex:1] floatValue]];
274 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];
279 - (void)applicationWillTerminate:(NSNotification *)notification
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];
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")];
300 [o_search_field setToolTip: _NS("Search in Playlist")];
303 - (void)playlistUpdated
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]];
310 [o_outline_view setHighlightedTableColumn:nil];
311 o_tc_sortColumn = nil;
313 [o_outline_view reloadData];
314 [[[[VLCMain sharedInstance] wizard] playlistWizard] reloadOutlineView];
316 [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:retainedRowSelection] byExtendingSelection:NO];
318 [self outlineViewSelectionDidChange: nil];
319 [[VLCMain sharedInstance] updateMainWindow];
322 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
325 // playlist_item_t * p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
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);
332 // [o_mi_revealInFinder setEnabled: NO];
333 // [o_mm_mi_revealInFinder setEnabled: NO];
335 // o_mrl = [NSMutableString stringWithUTF8String: psz_uri];
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];
342 // if ([o_mrl characterAtIndex:0] == '/') {
343 // [o_mi_revealInFinder setEnabled: YES];
344 // [o_mm_mi_revealInFinder setEnabled: YES];
349 // /* update our info-panel to reflect the new item */
350 // [[[VLCMain sharedInstance] info] updatePanelWithItem:p_item->p_input];
354 - (BOOL)isSelectionEmpty
356 return [o_outline_view selectedRow] == -1;
359 - (void)updateRowSelection
362 playlist_t *p_playlist = pl_Get(VLCIntf);
363 playlist_item_t *p_item, *p_temp_item;
364 NSMutableArray *o_array = [NSMutableArray array];
368 // p_item = playlist_CurrentPlayingItem(p_playlist);
369 // if (p_item == NULL) {
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;
381 // NSUInteger count = [o_array count];
382 // for (NSUInteger j = 0; j < count - 1; j++) {
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];
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];
397 - (IBAction)savePlaylist:(id)sender
399 playlist_t * p_playlist = pl_Get(VLCIntf);
401 NSSavePanel *o_save_panel = [NSSavePanel savePanel];
402 NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];
404 [NSBundle loadNibNamed:@"PlaylistAccessoryView" owner:self];
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")];
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];
416 if ([o_save_panel runModal] == NSFileHandlingPanelOKButton) {
417 NSString *o_filename = [[o_save_panel URL] path];
419 if ([o_save_accessory_popup indexOfSelectedItem] == 0) {
420 NSString * o_real_filename;
422 range.location = [o_filename length] - [@".m3u" length];
423 range.length = [@".m3u" length];
425 if ([o_filename compare:@".m3u" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
426 o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
428 o_real_filename = o_filename;
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;
436 range.location = [o_filename length] - [@".xspf" length];
437 range.length = [@".xspf" length];
439 if ([o_filename compare:@".xspf" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
440 o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
442 o_real_filename = o_filename;
444 playlist_Export(p_playlist,
445 [o_real_filename fileSystemRepresentation],
446 p_playlist->p_local_category, "export-xspf");
448 NSString * o_real_filename;
450 range.location = [o_filename length] - [@".html" length];
451 range.length = [@".html" length];
453 if ([o_filename compare:@".html" options: NSCaseInsensitiveSearch range: range] != NSOrderedSame)
454 o_real_filename = [NSString stringWithFormat: @"%@.html", o_filename];
456 o_real_filename = o_filename;
458 playlist_Export(p_playlist,
459 [o_real_filename fileSystemRepresentation],
460 p_playlist->p_local_category, "export-html");
465 /* When called retrieves the selected outlineview row and plays that node or item */
466 - (IBAction)playItem:(id)sender
468 playlist_t *p_playlist = pl_Get(VLCIntf);
470 // ignore clicks on column header when handling double action
471 if (sender == o_outline_view && [o_outline_view clickedRow] == -1)
474 PLItem *o_item = [o_outline_view itemAtRow:[o_outline_view selectedRow]];
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]);
482 if (p_item && p_node) {
483 playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
488 - (IBAction)revealItemInFinder:(id)sender
490 NSIndexSet *selectedRows = [o_outline_view selectedRowIndexes];
491 [selectedRows enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
493 PLItem *o_item = [o_outline_view itemAtRow:idx];
495 /* perform some checks whether it is a file and if it is local at all... */
496 char *psz_url = input_item_GetURI([o_item input]);
497 NSURL *url = [NSURL URLWithString:toNSStr(psz_url)];
499 if (![url isFileURL])
501 if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]])
504 msg_Dbg(VLCIntf, "Reveal url %s in finder", [[url path] UTF8String]);
505 [[NSWorkspace sharedWorkspace] selectFile: [url path] inFileViewerRootedAtPath: [url path]];
510 /* When called retrieves the selected outlineview row and plays that node or item */
511 - (IBAction)preparseItem:(id)sender
514 NSIndexSet *o_selected_indexes;
515 intf_thread_t * p_intf = VLCIntf;
516 playlist_t * p_playlist = pl_Get(p_intf);
517 playlist_item_t *p_item = NULL;
519 o_selected_indexes = [o_outline_view selectedRowIndexes];
520 i_count = [o_selected_indexes count];
522 NSUInteger indexes[i_count];
523 [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
524 for (int i = 0; i < i_count; i++) {
525 PLItem *o_item = [o_outline_view itemAtRow:indexes[i]];
526 [o_outline_view deselectRow: indexes[i]];
528 if (![o_item isLeaf]) {
529 msg_Dbg(p_intf, "preparsing nodes not implemented");
533 libvlc_MetaRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
536 [self playlistUpdated];
539 - (IBAction)downloadCoverArt:(id)sender
542 NSIndexSet *o_selected_indexes;
543 intf_thread_t * p_intf = VLCIntf;
544 playlist_t * p_playlist = pl_Get(p_intf);
545 playlist_item_t *p_item = NULL;
547 o_selected_indexes = [o_outline_view selectedRowIndexes];
548 i_count = [o_selected_indexes count];
550 NSUInteger indexes[i_count];
551 [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
552 for (int i = 0; i < i_count; i++) {
553 PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
555 if (![o_item isLeaf])
558 libvlc_ArtRequest(p_intf->p_libvlc, [o_item input], META_REQUEST_OPTION_NONE);
560 [self playlistUpdated];
563 - (IBAction)selectAll:(id)sender
565 [o_outline_view selectAll: nil];
568 - (IBAction)showInfoPanel:(id)sender
570 [[[VLCMain sharedInstance] info] initPanel];
573 - (IBAction)deleteItem:(id)sender
576 NSIndexSet *o_selected_indexes;
577 intf_thread_t * p_intf = VLCIntf;
578 playlist_t * p_playlist = pl_Get(p_intf);
580 // check if deletion is allowed
581 if (![[self model] editAllowed])
584 o_selected_indexes = [o_outline_view selectedRowIndexes];
585 i_count = [o_selected_indexes count];
586 retainedRowSelection = [o_selected_indexes firstIndex];
587 if (retainedRowSelection == NSNotFound)
588 retainedRowSelection = 0;
591 NSUInteger indexes[i_count];
592 // if (i_count == [o_outline_view numberOfRows]) {
594 // playlist_NodeDelete(p_playlist, [self currentPlaylistRoot], true, false);
596 // [self playlistUpdated];
599 [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
600 for (int i = 0; i < i_count; i++) {
601 PLItem *o_item = [o_outline_view itemAtRow: indexes[i]];
602 [o_outline_view deselectRow: indexes[i]];
605 // if (p_item->i_children != -1) {
606 // //is a node and not an item
607 // if (playlist_Status(p_playlist) != PLAYLIST_STOPPED &&
608 // [self isItem: playlist_CurrentPlayingItem(p_playlist) inNode: ((playlist_item_t *)[o_item pointerValue])
609 // checkItemExistence: NO locked:YES] == YES)
610 // // if current item is in selected node and is playing then stop playlist
611 // playlist_Control(p_playlist, PLAYLIST_STOP, pl_Locked);
613 // playlist_NodeDelete(p_playlist, p_item, true, false);
616 playlist_DeleteFromInput(p_playlist, [o_item input], pl_Unlocked);
617 // [[o_item parent] deleteChild:o_item];
619 // [o_outline_view reloadData];
622 // [self playlistUpdated];
625 - (IBAction)sortNodeByName:(id)sender
627 [self sortNode: SORT_TITLE];
630 - (IBAction)sortNodeByAuthor:(id)sender
632 [self sortNode: SORT_ARTIST];
635 - (void)sortNode:(int)i_mode
637 playlist_t * p_playlist = pl_Get(VLCIntf);
638 playlist_item_t * p_item;
640 // TODO why do we need this kind of sort? It looks crap and confusing...
642 // if ([o_outline_view selectedRow] > -1) {
643 // p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
647 // p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
650 // if (p_item->i_children > -1) // the item is a node
651 // playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
653 // playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
656 // [self playlistUpdated];
659 - (input_item_t *)createItem:(NSDictionary *)o_one_item
661 intf_thread_t * p_intf = VLCIntf;
662 playlist_t * p_playlist = pl_Get(p_intf);
664 input_item_t *p_input;
665 BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
666 NSString *o_uri, *o_name, *o_path;
672 o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
673 o_nsurl = [NSURL URLWithString: o_uri];
674 o_path = [o_nsurl path];
675 o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
676 o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];
678 if ([[NSFileManager defaultManager] fileExistsAtPath:o_path isDirectory:&b_dir] && b_dir &&
679 [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:o_path isRemovable: &b_rem
680 isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [o_nsurl isFileURL]) {
682 NSString *diskType = [VLCOpen getVolumeTypeFromMountPath: o_path];
683 msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
685 if ([diskType isEqualToString: kVLCMediaDVD])
686 o_uri = [NSString stringWithFormat: @"dvdnav://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
687 else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
688 o_uri = [NSString stringWithFormat: @"dvdnav://%@", o_path];
689 else if ([diskType isEqualToString: kVLCMediaAudioCD])
690 o_uri = [NSString stringWithFormat: @"cdda://%@", [VLCOpen getBSDNodeFromMountPath: o_path]];
691 else if ([diskType isEqualToString: kVLCMediaVCD])
692 o_uri = [NSString stringWithFormat: @"vcd://%@#0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
693 else if ([diskType isEqualToString: kVLCMediaSVCD])
694 o_uri = [NSString stringWithFormat: @"vcd://%@@0:0", [VLCOpen getBSDNodeFromMountPath: o_path]];
695 else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
696 o_uri = [NSString stringWithFormat: @"bluray://%@", o_path];
698 msg_Warn(VLCIntf, "unknown disk type, treating %s as regular input", [o_path UTF8String]);
700 p_input = input_item_New([o_uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath: o_path] UTF8String]);
703 p_input = input_item_New([o_uri fileSystemRepresentation], o_name ? [o_name UTF8String] : NULL);
709 NSUInteger count = [o_options count];
710 for (NSUInteger i = 0; i < count; i++)
711 input_item_AddOption(p_input, [[o_options objectAtIndex:i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
714 /* Recent documents menu */
715 if (o_nsurl != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
716 [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL: o_nsurl];
721 - (void)addPlaylistItems:(NSArray*)o_array
726 // add items directly to media library if this is the current root
727 if ([[self model] currentRootType] == ROOT_TYPE_MEDIALIBRARY)
728 i_plItemId = [[[self model] rootItem] plItemId];
730 BOOL b_autoplay = var_InheritBool(VLCIntf, "macosx-autoplay");
732 [self addPlaylistItems:o_array withParentItemId:i_plItemId atPos:-1 startPlayback:b_autoplay];
735 - (void)addPlaylistItems:(NSArray*)o_array withParentItemId:(int)i_plItemId atPos:(int)i_position startPlayback:(BOOL)b_start
737 playlist_t * p_playlist = pl_Get(VLCIntf);
740 playlist_item_t *p_parent = NULL;
742 p_parent = playlist_ItemGetById(p_playlist, i_plItemId);
744 p_parent = p_playlist->p_playing;
751 NSUInteger count = [o_array count];
752 int i_current_offset = 0;
753 for (NSUInteger i = 0; i < count; ++i) {
755 NSDictionary *o_current_item = [o_array objectAtIndex:i];
756 input_item_t *p_input = [self createItem: o_current_item];
760 int i_pos = (i_position == -1) ? PLAYLIST_END : i_position + i_current_offset++;
761 playlist_item_t *p_item = playlist_NodeAddInput(p_playlist, p_input, p_parent,
762 PLAYLIST_INSERT, i_pos, pl_Locked);
766 if (i == 0 && b_start) {
767 playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_parent, p_item);
769 input_item_Release(p_input);
775 - (IBAction)searchItem:(id)sender
777 [[self model] searchUpdate:[o_search_field stringValue]];
780 - (IBAction)recursiveExpandNode:(id)sender
782 NSIndexSet * selectedRows = [o_outline_view selectedRowIndexes];
783 NSUInteger count = [selectedRows count];
784 NSUInteger indexes[count];
785 [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
788 playlist_item_t *p_item;
789 for (NSUInteger i = 0; i < count; i++) {
790 o_item = [o_outline_view itemAtRow: indexes[i]];
792 /* We need to collapse the node first, since OSX refuses to recursively
793 expand an already expanded node, even if children nodes are collapsed. */
794 if ([o_outline_view isExpandable:o_item]) {
795 [o_outline_view collapseItem: o_item collapseChildren: YES];
796 [o_outline_view expandItem: o_item expandChildren: YES];
799 selectedRows = [o_outline_view selectedRowIndexes];
800 [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
804 - (NSMenu *)menuForEvent:(NSEvent *)o_event
806 if (!b_playlistmenu_nib_loaded)
807 b_playlistmenu_nib_loaded = [NSBundle loadNibNamed:@"PlaylistMenu" owner:self];
813 pt = [o_outline_view convertPoint: [o_event locationInWindow] fromView: nil];
814 int row = [o_outline_view rowAtPoint:pt];
815 if (row != -1 && ![[o_outline_view selectedRowIndexes] containsIndex: row])
816 [o_outline_view selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
818 b_item_sel = (row != -1 && [o_outline_view selectedRow] != -1);
819 b_rows = [o_outline_view numberOfRows] != 0;
821 playlist_t *p_playlist = pl_Get(VLCIntf);
822 bool b_del_allowed = [[self model] editAllowed];
824 [o_mi_play setEnabled: b_item_sel];
825 [o_mi_delete setEnabled: b_item_sel && b_del_allowed];
826 [o_mi_selectall setEnabled: b_rows];
827 [o_mi_info setEnabled: b_item_sel];
828 [o_mi_preparse setEnabled: b_item_sel];
829 [o_mi_recursive_expand setEnabled: b_item_sel];
830 [o_mi_sort_name setEnabled: b_item_sel];
831 [o_mi_sort_author setEnabled: b_item_sel];
832 [o_mi_dl_cover_art setEnabled: b_item_sel];
837 - (void)outlineView: (NSOutlineView *)o_tv didClickTableColumn:(NSTableColumn *)o_tc
839 int i_mode, i_type = 0;
840 intf_thread_t *p_intf = VLCIntf;
841 NSString * o_identifier = [o_tc identifier];
843 playlist_t *p_playlist = pl_Get(p_intf);
845 if (o_tc_sortColumn == o_tc)
846 b_isSortDescending = !b_isSortDescending;
848 b_isSortDescending = false;
850 if (b_isSortDescending)
851 i_type = ORDER_REVERSE;
853 i_type = ORDER_NORMAL;
855 [[self model] sortForColumn:o_identifier withMode:i_type];
857 // TODO rework, why do we need a full call here?
858 // [self playlistUpdated];
860 /* Clear indications of any existing column sorting */
861 NSUInteger count = [[o_outline_view tableColumns] count];
862 for (NSUInteger i = 0 ; i < count ; i++)
863 [o_outline_view setIndicatorImage:nil inTableColumn: [[o_outline_view tableColumns] objectAtIndex:i]];
865 [o_outline_view setHighlightedTableColumn:nil];
866 o_tc_sortColumn = nil;
869 o_tc_sortColumn = o_tc;
870 [o_outline_view setHighlightedTableColumn:o_tc];
872 if (b_isSortDescending)
873 [o_outline_view setIndicatorImage:o_descendingSortingImage inTableColumn:o_tc];
875 [o_outline_view setIndicatorImage:o_ascendingSortingImage inTableColumn:o_tc];
879 - (void)outlineView:(NSOutlineView *)outlineView
880 willDisplayCell:(id)cell
881 forTableColumn:(NSTableColumn *)tableColumn
884 /* this method can be called when VLC is already dead, hence the extra checks */
885 intf_thread_t * p_intf = VLCIntf;
888 playlist_t *p_playlist = pl_Get(p_intf);
891 if (config_GetInt(VLCIntf, "macosx-large-text"))
892 fontToUse = [NSFont systemFontOfSize:13.];
894 fontToUse = [NSFont systemFontOfSize:11.];
896 BOOL b_is_playing = NO;
898 playlist_item_t *p_current_item = playlist_CurrentPlayingItem(p_playlist);
899 if (p_current_item) {
900 b_is_playing = p_current_item->i_id == [item plItemId];
905 TODO: repaint all items bold:
906 [self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
907 || [o_playing_item isEqual: item]
911 [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toHaveTrait:NSBoldFontMask]];
913 [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toNotHaveTrait:NSBoldFontMask]];
916 // TODO remove method
917 - (NSArray *)draggedItems
919 return [[self model] draggedItems];
922 - (void)setColumn: (NSString *)o_column state: (NSInteger)i_state translationDict:(NSDictionary *)o_dict
924 NSTableColumn * o_work_tc;
926 if (i_state == NSOnState) {
927 NSString *o_title = [o_dict objectForKey:o_column];
931 o_work_tc = [[NSTableColumn alloc] initWithIdentifier: o_column];
932 [o_work_tc setEditable: NO];
933 [[o_work_tc dataCell] setFont: [NSFont controlContentFontOfSize:11.]];
935 [[o_work_tc headerCell] setStringValue: [o_dict objectForKey:o_column]];
937 if ([o_column isEqualToString: TRACKNUM_COLUMN]) {
938 [o_work_tc setWidth: 20.];
939 [o_work_tc setResizingMask: NSTableColumnNoResizing];
940 [[o_work_tc headerCell] setStringValue: @"#"];
943 [o_outline_view addTableColumn: o_work_tc];
945 [o_outline_view reloadData];
946 [o_outline_view setNeedsDisplay: YES];
949 [o_outline_view removeTableColumn: [o_outline_view tableColumnWithIdentifier: o_column]];
951 [o_outline_view setOutlineTableColumn: [o_outline_view tableColumnWithIdentifier:TITLE_COLUMN]];
954 - (void)saveTableColumns
956 NSMutableArray * o_arrayToSave = [[NSMutableArray alloc] init];
957 NSArray * o_columns = [[NSArray alloc] initWithArray:[o_outline_view tableColumns]];
958 NSUInteger count = [o_columns count];
959 NSTableColumn * o_currentColumn;
960 for (NSUInteger i = 0; i < count; i++) {
961 o_currentColumn = [o_columns objectAtIndex:i];
962 [o_arrayToSave addObject:[NSArray arrayWithObjects:[o_currentColumn identifier], [NSNumber numberWithFloat:[o_currentColumn width]], nil]];
964 [[NSUserDefaults standardUserDefaults] setObject: o_arrayToSave forKey:@"PlaylistColumnSelection"];
965 [[NSUserDefaults standardUserDefaults] synchronize];
967 [o_arrayToSave release];
970 - (BOOL)isValidResumeItem:(input_item_t *)p_item
972 char *psz_url = input_item_GetURI(p_item);
973 NSString *o_url_string = toNSStr(psz_url);
976 if ([o_url_string isEqualToString:@""])
979 NSURL *o_url = [NSURL URLWithString:o_url_string];
981 if (![o_url isFileURL])
985 if (![[NSFileManager defaultManager] fileExistsAtPath:[o_url path] isDirectory:&isDir])
994 - (void)updateAlertWindow:(NSTimer *)timer
996 NSAlert *alert = [timer userInfo];
998 --currentResumeTimeout;
999 if (currentResumeTimeout <= 0) {
1000 [[alert window] close];
1004 NSString *buttonLabel = _NS("Restart playback");
1005 buttonLabel = [buttonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1007 [[[alert buttons] objectAtIndex:2] setTitle:buttonLabel];
1010 - (void)continuePlaybackWhereYouLeftOff:(input_thread_t *)p_input_thread
1012 NSDictionary *recentlyPlayedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"recentlyPlayedMedia"];
1013 if (!recentlyPlayedFiles)
1016 input_item_t *p_item = input_GetItem(p_input_thread);
1020 /* allow the user to over-write the start/stop/run-time */
1021 if (var_GetFloat(p_input_thread, "run-time") > 0 ||
1022 var_GetFloat(p_input_thread, "start-time") > 0 ||
1023 var_GetFloat(p_input_thread, "stop-time") > 0) {
1027 /* check for file existance before resuming */
1028 if (![self isValidResumeItem:p_item])
1031 char *psz_url = decode_URI(input_item_GetURI(p_item));
1034 NSString *url = toNSStr(psz_url);
1037 NSNumber *lastPosition = [recentlyPlayedFiles objectForKey:url];
1038 if (!lastPosition || lastPosition.intValue <= 0)
1041 int settingValue = config_GetInt(VLCIntf, "macosx-continue-playback");
1042 if (settingValue == 2) // never resume
1045 NSInteger returnValue = NSAlertErrorReturn;
1046 if (settingValue == 0) { // ask
1048 char *psz_title_name = input_item_GetTitleFbName(p_item);
1049 NSString *o_title = toNSStr(psz_title_name);
1050 free(psz_title_name);
1052 currentResumeTimeout = 6;
1053 NSString *o_restartButtonLabel = _NS("Restart playback");
1054 o_restartButtonLabel = [o_restartButtonLabel stringByAppendingFormat:@" (%d)", currentResumeTimeout];
1055 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]];
1057 NSTimer *timer = [NSTimer timerWithTimeInterval:1
1059 selector:@selector(updateAlertWindow:)
1063 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSModalPanelRunLoopMode];
1065 [[VLCCoreInteraction sharedInstance] pause];
1066 returnValue = [theAlert runModal];
1068 [[VLCCoreInteraction sharedInstance] playOrPause];
1070 // restart button was pressed or timeout happened
1071 if (returnValue == NSAlertAlternateReturn ||
1072 returnValue == NSRunAbortedResponse)
1076 mtime_t lastPos = (mtime_t)lastPosition.intValue * 1000000;
1077 msg_Dbg(VLCIntf, "continuing playback at %lld", lastPos);
1078 var_SetTime(p_input_thread, "time", lastPos);
1080 if (returnValue == NSAlertOtherReturn)
1081 config_PutInt(VLCIntf, "macosx-continue-playback", 1);
1084 - (void)storePlaybackPositionForItem:(input_thread_t *)p_input_thread
1086 if (!var_InheritBool(VLCIntf, "macosx-recentitems"))
1089 input_item_t *p_item = input_GetItem(p_input_thread);
1093 if (![self isValidResumeItem:p_item])
1096 char *psz_url = decode_URI(input_item_GetURI(p_item));
1099 NSString *url = toNSStr(psz_url);
1102 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
1103 NSMutableDictionary *mutDict = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:@"recentlyPlayedMedia"]];
1105 float relativePos = var_GetFloat(p_input_thread, "position");
1106 mtime_t pos = var_GetTime(p_input_thread, "time") / 1000000;
1107 mtime_t dur = input_item_GetDuration(p_item) / 1000000;
1109 NSMutableArray *mediaList = [[defaults objectForKey:@"recentlyPlayedMediaList"] mutableCopy];
1111 if (relativePos > .05 && relativePos < .95 && dur > 180) {
1112 [mutDict setObject:[NSNumber numberWithInt:pos] forKey:url];
1114 [mediaList removeObject:url];
1115 [mediaList addObject:url];
1116 NSUInteger mediaListCount = mediaList.count;
1117 if (mediaListCount > 30) {
1118 for (NSUInteger x = 0; x < mediaListCount - 30; x++) {
1119 [mutDict removeObjectForKey:[mediaList objectAtIndex:0]];
1120 [mediaList removeObjectAtIndex:0];
1124 [mutDict removeObjectForKey:url];
1125 [mediaList removeObject:url];
1127 [defaults setObject:mutDict forKey:@"recentlyPlayedMedia"];
1128 [defaults setObject:mediaList forKey:@"recentlyPlayedMediaList"];
1129 [defaults synchronize];
1132 [mediaList release];