VLCPlaylist.m 33.3 KB
Newer Older
1
/*****************************************************************************
2
 * VLCPlaylist.m: MacOS X interface module
3
 *****************************************************************************
4
* Copyright (C) 2002-2015 VLC authors and VideoLAN
bigben's avatar
...    
bigben committed
5
 * $Id$
6
 *
7
8
 * Authors: Derk-Jan Hartman <hartman at videola/n dot org>
 *          Benjamin Pracht <bigben at videolan dot org>
9
 *          Felix Paul Kühne <fkuehne at videolan dot org>
10
 *          David Fuhrmann <david dot fuhrmann at googlemail dot com>
11
12
13
14
15
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
bigben's avatar
bigben committed
16
 *
17
18
19
20
21
22
23
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
dionoea's avatar
dionoea committed
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
30
31
32
 * reimplement enable/disable item
 */


33
34
35
36
37
38
/*****************************************************************************
 * Preamble
 *****************************************************************************/
#include <stdlib.h>                                      /* malloc(), free() */
#include <sys/param.h>                                    /* for MAXPATHLEN */
#include <string.h>
39
#include <math.h>
hartman's avatar
hartman committed
40
#include <sys/mount.h>
41

42
43
#import "CompatibilityFixes.h"

44
#import "intf.h"
45
#import "VLCPlaylist.h"
46
#import "MainMenu.h"
47
#import "VLCPlaylistInfo.h"
48
#import "ResumeDialogController.h"
49
#import "open.h"
50

Pierre's avatar
Pierre committed
51
52
#include <vlc_keys.h>
#import <vlc_interface.h>
53
54
#include <vlc_url.h>

55
/*****************************************************************************
56
57
58
59
60
 * An extension to NSOutlineView's interface to fix compilation warnings
 * and let us access these 2 functions properly.
 * This uses a private API, but works fine on all current OSX releases.
 * Radar ID 11739459 request a public API for this. However, it is probably
 * easier and faster to recreate similar looking bitmaps ourselves.
61
62
63
64
65
66
67
 *****************************************************************************/

@interface NSOutlineView (UndocumentedSortImages)
+ (NSImage *)_defaultTableHeaderSortImage;
+ (NSImage *)_defaultTableHeaderReverseSortImage;
@end

68
69
@interface VLCPlaylist ()
{
70
71
    NSImage *_descendingSortingImage;
    NSImage *_ascendingSortingImage;
72
73
74

    BOOL b_selected_item_met;
    BOOL b_isSortDescending;
75
    NSTableColumn *_sortTableColumn;
76
77

    BOOL b_playlistmenu_nib_loaded;
78

79
    PLModel *_model;
David's avatar
David committed
80
81
82
83
84

    // information for playlist table columns menu

    NSDictionary *_translationsForPlaylistTableColumns;
    NSArray *_menuOrderOfPlaylistTableColumns;
85
86
}

87
88
89
- (void)saveTableColumns;
@end

90
91
@implementation VLCPlaylist

92
93
94
95
96
97
98
99
100
101
102
103
- (id)init
{
    self = [super init];
    if (self) {
        /* This uses a private API, but works fine on all current OSX releases.
         * Radar ID 11739459 request a public API for this. However, it is probably
         * easier and faster to recreate similar looking bitmaps ourselves. */
        _ascendingSortingImage = [[NSOutlineView class] _defaultTableHeaderSortImage];
        _descendingSortingImage = [[NSOutlineView class] _defaultTableHeaderReverseSortImage];

        [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(applicationWillTerminate:) name: NSApplicationWillTerminateNotification object: nil];

David's avatar
David committed
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

        _translationsForPlaylistTableColumns = [[NSDictionary alloc] initWithObjectsAndKeys:
                                                _NS("Track Number"),  TRACKNUM_COLUMN,
                                                _NS("Title"),         TITLE_COLUMN,
                                                _NS("Author"),        ARTIST_COLUMN,
                                                _NS("Duration"),      DURATION_COLUMN,
                                                _NS("Genre"),         GENRE_COLUMN,
                                                _NS("Album"),         ALBUM_COLUMN,
                                                _NS("Description"),   DESCRIPTION_COLUMN,
                                                _NS("Date"),          DATE_COLUMN,
                                                _NS("Language"),      LANGUAGE_COLUMN,
                                                _NS("URI"),           URI_COLUMN,
                                                _NS("File Size"),     FILESIZE_COLUMN,
                                                nil];
        // this array also assigns tags (index) to type of menu item
        _menuOrderOfPlaylistTableColumns = [[NSArray alloc] initWithObjects: TRACKNUM_COLUMN, TITLE_COLUMN,
                                            ARTIST_COLUMN, DURATION_COLUMN, GENRE_COLUMN, ALBUM_COLUMN,
                                            DESCRIPTION_COLUMN, DATE_COLUMN, LANGUAGE_COLUMN, URI_COLUMN,
                                            FILESIZE_COLUMN,nil];

124
125
126
127
    }
    return self;
}

David's avatar
David committed
128
129
+ (void)initialize
{
130
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
131
132
133
134
    NSMutableArray *columnArray = [[NSMutableArray alloc] init];
    [columnArray addObject: [NSArray arrayWithObjects:TITLE_COLUMN, [NSNumber numberWithFloat:190.], nil]];
    [columnArray addObject: [NSArray arrayWithObjects:ARTIST_COLUMN, [NSNumber numberWithFloat:95.], nil]];
    [columnArray addObject: [NSArray arrayWithObjects:DURATION_COLUMN, [NSNumber numberWithFloat:95.], nil]];
135
136

    NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
137
                                 [NSArray arrayWithArray:columnArray], @"PlaylistColumnSelection",
138
139
                                 [NSArray array], @"recentlyPlayedMediaList",
                                 [NSDictionary dictionary], @"recentlyPlayedMedia", nil];
140
141
142
143

    [defaults registerDefaults:appDefaults];
}

David's avatar
David committed
144
- (PLModel *)model
145
{
146
    return _model;
147
148
149
150
151
152
}

- (void)reloadStyles
{
    NSFont *fontToUse;
    CGFloat rowHeight;
David's avatar
David committed
153
    if (var_InheritBool(getIntf(), "macosx-large-text")) {
154
155
156
157
158
159
160
        fontToUse = [NSFont systemFontOfSize:13.];
        rowHeight = 21.;
    } else {
        fontToUse = [NSFont systemFontOfSize:11.];
        rowHeight = 16.;
    }

161
    NSArray *columns = [_outlineView tableColumns];
162
163
    NSUInteger count = columns.count;
    for (NSUInteger x = 0; x < count; x++)
164
        [[columns[x] dataCell] setFont:fontToUse];
165
    [_outlineView setRowHeight:rowHeight];
166
167
}

168
169
- (void)awakeFromNib
{
170
    // This is only called for the playlist popup menu
David Fuhrmann's avatar
David Fuhrmann committed
171
    [self initStrings];
172
173
}

174
175
176
177
178
- (void)setOutlineView:(VLCPlaylistView * __nullable)outlineView
{
    _outlineView = outlineView;
    [_outlineView setDelegate:self];

David's avatar
David committed
179
    playlist_t * p_playlist = pl_Get(getIntf());
180

181
    _model = [[PLModel alloc] initWithOutlineView:_outlineView playlist:p_playlist rootItem:p_playlist->p_playing];
182
183
184
185
186
187
188
189
190
    [_outlineView setDataSource:_model];
    [_outlineView reloadData];

    [_outlineView setTarget: self];
    [_outlineView setDoubleAction: @selector(playItem:)];

    [_outlineView setAllowsEmptySelection: NO];
    [_outlineView registerForDraggedTypes: [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil]];
    [_outlineView setIntercellSpacing: NSMakeSize (0.0, 1.0)];
191
192

    [self reloadStyles];
193
194
}

195
196
197
198
- (void)setPlaylistHeaderView:(NSTableHeaderView * __nullable)playlistHeaderView
{
    VLCMainMenu *mainMenu = [[VLCMain sharedInstance] mainMenu];
    _playlistHeaderView = playlistHeaderView;
David's avatar
David committed
199
200
201
202

    // Setup playlist table column selection for both context and main menu
    NSMenu *contextMenu = [[NSMenu alloc] init];
    [self setupPlaylistTableColumnsForMenu:contextMenu];
203
    [_playlistHeaderView setMenu: contextMenu];
David's avatar
David committed
204
    [self setupPlaylistTableColumnsForMenu:[[[VLCMain sharedInstance] mainMenu] playlistTableColumnsMenu]];
205

206
207
208
    NSArray * columnArray = [[NSUserDefaults standardUserDefaults] arrayForKey:@"PlaylistColumnSelection"];
    NSUInteger columnCount = [columnArray count];
    NSString * column;
209

210
211
212
    for (NSUInteger i = 0; i < columnCount; i++) {
        column = [columnArray[i] firstObject];
        if ([column isEqualToString:@"status"])
213
214
            continue;

David's avatar
David committed
215
        if(![self setPlaylistColumnTableState: NSOnState forColumn:column])
216
217
            continue;

218
        [[_outlineView tableColumnWithIdentifier: column] setWidth: [columnArray[i][1] floatValue]];
219
220
221
222
223
224
225
    }
}

- (void)applicationWillTerminate:(NSNotification *)notification
{
    /* 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 */
    [self saveTableColumns];
226
}
hartman's avatar
hartman committed
227

228
229
- (void)initStrings
{
230
231
232
233
234
235
236
237
238
239
    [_playPlaylistMenuItem setTitle: _NS("Play")];
    [_deletePlaylistMenuItem setTitle: _NS("Delete")];
    [_recursiveExpandPlaylistMenuItem setTitle: _NS("Expand Node")];
    [_selectAllPlaylistMenuItem setTitle: _NS("Select All")];
    [_infoPlaylistMenuItem setTitle: _NS("Media Information...")];
    [_downloadCoverArtPlaylistMenuItem setTitle: _NS("Download Cover Art")];
    [_preparsePlaylistMenuItem setTitle: _NS("Fetch Meta Data")];
    [_revealInFinderPlaylistMenuItem setTitle: _NS("Reveal in Finder")];
    [_sortNamePlaylistMenuItem setTitle: _NS("Sort Node by Name")];
    [_sortAuthorPlaylistMenuItem setTitle: _NS("Sort Node by Author")];
240
    [_addFilesToPlaylistMenuItem setTitle: _NS("Add File...")];
241
242
}

243
244
- (void)playlistUpdated
{
245
    [_outlineView reloadData];
246
}
247

248
249
- (void)playbackModeUpdated
{
250
    [_model playbackModeUpdated];
251
252
}

253
254
255

- (BOOL)isSelectionEmpty
{
256
    return [_outlineView selectedRow] == -1;
257
258
}

David's avatar
David committed
259
- (void)currentlyPlayingItemChanged
260
{
David's avatar
David committed
261
262
263
264
265
    PLItem *item = [[self model] currentlyPlayingItem];
    if (!item)
        return;

    // select item
266
    NSInteger itemIndex = [_outlineView rowForItem:item];
David's avatar
David committed
267
268
269
270
271
    if (itemIndex < 0) {
        // expand if needed
        while (item != nil) {
            PLItem *parent = [item parent];

272
            if (![_outlineView isExpandable: parent])
David's avatar
David committed
273
                break;
274
275
            if (![_outlineView isItemExpanded: parent])
                [_outlineView expandItem: parent];
David's avatar
David committed
276
277
278
279
            item = parent;
        }

        // search for row again
280
        itemIndex = [_outlineView rowForItem:item];
David's avatar
David committed
281
282
283
284
285
        if (itemIndex < 0) {
            return;
        }
    }

286
    [_outlineView selectRowIndexes: [NSIndexSet indexSetWithIndex: itemIndex] byExtendingSelection: NO];
287
    [_outlineView scrollRowToVisible: itemIndex];
288
289
}

290
291
292
#pragma mark -
#pragma mark Playlist actions

hartman's avatar
hartman committed
293
/* When called retrieves the selected outlineview row and plays that node or item */
294
295
- (IBAction)playItem:(id)sender
{
David's avatar
David committed
296
    playlist_t *p_playlist = pl_Get(getIntf());
bigben's avatar
   
bigben committed
297

298
    // ignore clicks on column header when handling double action
299
    if (sender == _outlineView && [_outlineView clickedRow] == -1)
300
301
        return;

302
    PLItem *o_item = [_outlineView itemAtRow:[_outlineView selectedRow]];
303
304
    if (!o_item)
        return;
David's avatar
David committed
305

306
307
308
    PL_LOCK;
    playlist_item_t *p_item = playlist_ItemGetById(p_playlist, [o_item plItemId]);
    playlist_item_t *p_node = playlist_ItemGetById(p_playlist, [[[self model] rootItem] plItemId]);
309

310
    if (p_item && p_node) {
311
        playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_node, p_item);
312
    }
313
    PL_UNLOCK;
314
}
315

316
317
- (IBAction)revealItemInFinder:(id)sender
{
318
    NSIndexSet *selectedRows = [_outlineView selectedRowIndexes];
319
320
    if (selectedRows.count < 1)
        return;
321

322
    PLItem *o_item = [_outlineView itemAtRow:selectedRows.firstIndex];
David's avatar
David committed
323

324
325
326
327
328
329
330
    char *psz_url = input_item_GetURI([o_item input]);
    if (!psz_url)
        return;
    char *psz_path = vlc_uri2path(psz_url);
    NSString *path = toNSStr(psz_path);
    free(psz_url);
    free(psz_path);
331

David's avatar
David committed
332
    msg_Dbg(getIntf(), "Reveal url %s in finder", [path UTF8String]);
333
    [[NSWorkspace sharedWorkspace] selectFile: path inFileViewerRootedAtPath: path];
334
}
335

336
337
338
339
/* When called retrieves the selected outlineview row and plays that node or item */
- (IBAction)preparseItem:(id)sender
{
    int i_count;
340
    NSIndexSet *o_selected_indexes;
David's avatar
David committed
341
    intf_thread_t * p_intf = getIntf();
342
    playlist_t * p_playlist = pl_Get(p_intf);
343
344
    playlist_item_t *p_item = NULL;

345
    o_selected_indexes = [_outlineView selectedRowIndexes];
346
347
348
349
    i_count = [o_selected_indexes count];

    NSUInteger indexes[i_count];
    [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
350
    for (int i = 0; i < i_count; i++) {
351
352
        PLItem *o_item = [_outlineView itemAtRow:indexes[i]];
        [_outlineView deselectRow: indexes[i]];
353

David's avatar
David committed
354
355
356
        if (![o_item isLeaf]) {
            msg_Dbg(p_intf, "preparsing nodes not implemented");
            continue;
357
        }
David's avatar
David committed
358

359
        libvlc_MetadataRequest(p_intf->obj.libvlc, [o_item input], META_REQUEST_OPTION_NONE, -1, NULL);
David's avatar
David committed
360

361
362
363
364
    }
    [self playlistUpdated];
}

365
366
367
- (IBAction)downloadCoverArt:(id)sender
{
    int i_count;
368
    NSIndexSet *o_selected_indexes;
David's avatar
David committed
369
    intf_thread_t * p_intf = getIntf();
370
    playlist_t * p_playlist = pl_Get(p_intf);
371
372
    playlist_item_t *p_item = NULL;

373
    o_selected_indexes = [_outlineView selectedRowIndexes];
374
375
376
377
    i_count = [o_selected_indexes count];

    NSUInteger indexes[i_count];
    [o_selected_indexes getIndexes:indexes maxCount:i_count inIndexRange:nil];
378
    for (int i = 0; i < i_count; i++) {
379
        PLItem *o_item = [_outlineView itemAtRow: indexes[i]];
380

David's avatar
David committed
381
382
383
        if (![o_item isLeaf])
            continue;

384
        libvlc_ArtRequest(p_intf->obj.libvlc, [o_item input], META_REQUEST_OPTION_NONE);
385
386
387
388
    }
    [self playlistUpdated];
}

bigben's avatar
bigben committed
389
390
- (IBAction)selectAll:(id)sender
{
391
    [_outlineView selectAll: nil];
bigben's avatar
bigben committed
392
393
}

394
395
- (IBAction)showInfoPanel:(id)sender
{
396
    [[[VLCMain sharedInstance] currentMediaInfoPanel] toggleWindow:sender];
397
398
}

399
400
401
402
403
- (IBAction)addFilesToPlaylist:(id)sender
{
    [[[VLCMain sharedInstance] open] openFile];
}

bigben's avatar
bigben committed
404
405
- (IBAction)deleteItem:(id)sender
{
406
    [_model deleteSelectedItem];
bigben's avatar
bigben committed
407
}
bigben's avatar
bigben committed
408

409
410
411
412
413
414
415
- (IBAction)sortNodeByName:(id)sender
{
    [self sortNode: SORT_TITLE];
}

- (IBAction)sortNodeByAuthor:(id)sender
{
zorglub's avatar
zorglub committed
416
    [self sortNode: SORT_ARTIST];
417
418
419
420
}

- (void)sortNode:(int)i_mode
{
David's avatar
David committed
421
    playlist_t * p_playlist = pl_Get(getIntf());
422
423
    playlist_item_t * p_item;

David's avatar
David committed
424
425
    // TODO why do we need this kind of sort? It looks crap and confusing...

426
427
//    if ([_outlineView selectedRow] > -1) {
//        p_item = [[_outlineView itemAtRow: [_outlineView selectedRow]] pointerValue];
David's avatar
David committed
428
429
430
431
432
433
434
435
436
437
438
439
440
//        if (!p_item)
//            return;
//    } else
//        p_item = [self currentPlaylistRoot]; // If no item is selected, sort the whole playlist
//
//    PL_LOCK;
//    if (p_item->i_children > -1) // the item is a node
//        playlist_RecursiveNodeSort(p_playlist, p_item, i_mode, ORDER_NORMAL);
//    else
//        playlist_RecursiveNodeSort(p_playlist, p_item->p_parent, i_mode, ORDER_NORMAL);
//
//    PL_UNLOCK;
//    [self playlistUpdated];
441
442
}

David's avatar
David committed
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
// Actions for playlist column selections


- (void)togglePlaylistColumnTable:(id)sender
{
    NSInteger i_new_state = ![sender state];
    NSInteger i_tag = [sender tag];

    NSString *column = [_menuOrderOfPlaylistTableColumns objectAtIndex:i_tag];

    [self setPlaylistColumnTableState:i_new_state forColumn:column];
}

- (BOOL)setPlaylistColumnTableState:(NSInteger)i_state forColumn:(NSString *)columnId
{
    NSUInteger i_tag = [_menuOrderOfPlaylistTableColumns indexOfObject: columnId];
    // prevent setting unknown columns
    if(i_tag == NSNotFound)
        return NO;

    // update state of menu items
    [[[_playlistHeaderView menu] itemWithTag: i_tag] setState: i_state];
    [[[[[VLCMain sharedInstance] mainMenu] playlistTableColumnsMenu] itemWithTag: i_tag] setState: i_state];

    // Change outline view
    if (i_state == NSOnState) {
        NSString *title = [_translationsForPlaylistTableColumns objectForKey:columnId];
        if (!title)
            return NO;

        NSTableColumn *tableColumn = [[NSTableColumn alloc] initWithIdentifier:columnId];
        [tableColumn setEditable:NO];
        [[tableColumn dataCell] setFont:[NSFont controlContentFontOfSize:11.]];

        [[tableColumn headerCell] setStringValue:[_translationsForPlaylistTableColumns objectForKey:columnId]];

        if ([columnId isEqualToString: TRACKNUM_COLUMN]) {
480
481
            [tableColumn setMinWidth:20.];
            [tableColumn setMaxWidth:70.];
David's avatar
David committed
482
            [[tableColumn headerCell] setStringValue:@"#"];
483
484
485

        } else {
            [tableColumn setMinWidth:42.];
David's avatar
David committed
486
487
488
489
490
491
492
493
494
495
496
497
498
499
        }

        [_outlineView addTableColumn:tableColumn];
        [_outlineView reloadData];
        [_outlineView setNeedsDisplay: YES];
    }
    else
        [_outlineView removeTableColumn: [_outlineView tableColumnWithIdentifier:columnId]];

    [_outlineView setOutlineTableColumn: [_outlineView tableColumnWithIdentifier:TITLE_COLUMN]];

    return YES;
}

500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
    if ([item action] == @selector(revealItemInFinder:)) {
        NSIndexSet *selectedRows = [_outlineView selectedRowIndexes];
        if (selectedRows.count != 1)
            return NO;

        PLItem *o_item = [_outlineView itemAtRow:selectedRows.firstIndex];

        // Check if item exists in file system
        char *psz_url = input_item_GetURI([o_item input]);
        NSURL *url = [NSURL URLWithString:toNSStr(psz_url)];
        free(psz_url);
        if (![url isFileURL])
            return NO;
        if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]])
            return NO;
517
518
519
520
521
522
523

    } else if ([item action] == @selector(deleteItem:)) {
        return [_outlineView numberOfSelectedRows] > 0 && _model.editAllowed;
    } else if ([item action] == @selector(selectAll:)) {
        return [_outlineView numberOfRows] >= 0;
    } else if ([item action] == @selector(playItem:)) {
        return [_outlineView numberOfSelectedRows] > 0;
524
525
526
527
528
    }

    return YES;
}

David's avatar
David committed
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
#pragma mark -
#pragma mark Helper for playlist table columns

- (void)setupPlaylistTableColumnsForMenu:(NSMenu *)menu
{
    NSMenuItem *menuItem;
    NSUInteger count = [_menuOrderOfPlaylistTableColumns count];
    for (NSUInteger i = 0; i < count; i++) {
        NSString *columnId = [_menuOrderOfPlaylistTableColumns objectAtIndex:i];
        NSString *title = [_translationsForPlaylistTableColumns objectForKey:columnId];
        menuItem = [menu addItemWithTitle:title
                                   action:@selector(togglePlaylistColumnTable:)
                            keyEquivalent:@""];
        [menuItem setTarget:self];
        [menuItem setTag:i];

        /* don't set a valid action for the title column selector, since we want it to be disabled */
        if ([columnId isEqualToString: TITLE_COLUMN])
            [menuItem setAction:nil];

    }
}

- (void)saveTableColumns
{
    NSMutableArray *arrayToSave = [[NSMutableArray alloc] init];
    NSArray *columns = [[NSArray alloc] initWithArray:[_outlineView tableColumns]];
    NSUInteger columnCount = [columns count];
    NSTableColumn *currentColumn;
    for (NSUInteger i = 0; i < columnCount; i++) {
        currentColumn = columns[i];
        [arrayToSave addObject:[NSArray arrayWithObjects:[currentColumn identifier], [NSNumber numberWithFloat:[currentColumn width]], nil]];
    }
    [[NSUserDefaults standardUserDefaults] setObject:arrayToSave forKey:@"PlaylistColumnSelection"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

566
567
568
#pragma mark -
#pragma mark Item helpers

569
- (input_item_t *)createItem:(NSDictionary *)itemToCreateDict
570
{
David's avatar
David committed
571
    intf_thread_t *p_intf = getIntf();
572
    playlist_t *p_playlist = pl_Get(p_intf);
573

574
    input_item_t *p_input;
575
    BOOL b_rem = FALSE, b_dir = FALSE, b_writable = FALSE;
576
577
578
    NSString *uri, *name, *path;
    NSURL * url;
    NSArray *optionsArray;
579
580

    /* Get the item */
581
582
583
584
585
    uri = (NSString *)[itemToCreateDict objectForKey: @"ITEM_URL"];
    url = [NSURL URLWithString: uri];
    path = [url path];
    name = (NSString *)[itemToCreateDict objectForKey: @"ITEM_NAME"];
    optionsArray = (NSArray *)[itemToCreateDict objectForKey: @"ITEM_OPTIONS"];
586

587
588
589
    if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&b_dir] && b_dir &&
        [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath:path isRemovable: &b_rem
                                                     isWritable:&b_writable isUnmountable:NULL description:NULL type:NULL] && b_rem && !b_writable && [url isFileURL]) {
590

591
        NSString *diskType = [[VLCStringUtility sharedInstance] getVolumeTypeFromMountPath: path];
592
        msg_Dbg(p_intf, "detected optical media of type %s in the file input", [diskType UTF8String]);
593

594
        if ([diskType isEqualToString: kVLCMediaDVD])
595
            uri = [NSString stringWithFormat: @"dvdnav://%@", [[VLCStringUtility sharedInstance] getBSDNodeFromMountPath: path]];
596
        else if ([diskType isEqualToString: kVLCMediaVideoTSFolder])
597
            uri = [NSString stringWithFormat: @"dvdnav://%@", path];
598
        else if ([diskType isEqualToString: kVLCMediaAudioCD])
599
            uri = [NSString stringWithFormat: @"cdda://%@", [[VLCStringUtility sharedInstance] getBSDNodeFromMountPath: path]];
600
        else if ([diskType isEqualToString: kVLCMediaVCD])
601
            uri = [NSString stringWithFormat: @"vcd://%@#0:0", [[VLCStringUtility sharedInstance] getBSDNodeFromMountPath: path]];
602
        else if ([diskType isEqualToString: kVLCMediaSVCD])
603
            uri = [NSString stringWithFormat: @"vcd://%@@0:0", [[VLCStringUtility sharedInstance] getBSDNodeFromMountPath: path]];
604
        else if ([diskType isEqualToString: kVLCMediaBD] || [diskType isEqualToString: kVLCMediaBDMVFolder])
605
            uri = [NSString stringWithFormat: @"bluray://%@", path];
606
        else
David's avatar
David committed
607
            msg_Warn(getIntf(), "unknown disk type, treating %s as regular input", [path UTF8String]);
bigben's avatar
*all :    
bigben committed
608

609
        p_input = input_item_New([uri UTF8String], [[[NSFileManager defaultManager] displayNameAtPath:path] UTF8String]);
610
    }
611
    else
612
        p_input = input_item_New([uri fileSystemRepresentation], name ? [name UTF8String] : NULL);
613

614
    if (!p_input)
615
        return NULL;
616

617
618
    if (optionsArray) {
        NSUInteger count = [optionsArray count];
619
        for (NSUInteger i = 0; i < count; i++)
620
            input_item_AddOption(p_input, [optionsArray[i] UTF8String], VLC_INPUT_OPTION_TRUSTED);
621
    }
hartman's avatar
hartman committed
622

623
    /* Recent documents menu */
624
    if (url != nil && (BOOL)config_GetInt(p_playlist, "macosx-recentitems") == YES)
625
        [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:url];
626

627
    return p_input;
628
629
}

630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
- (NSArray *)createItemsFromExternalPasteboard:(NSPasteboard *)pasteboard
{
    NSArray *o_array = [NSArray array];
    if (![[pasteboard types] containsObject: NSFilenamesPboardType])
        return o_array;

    NSArray *o_values = [[pasteboard propertyListForType: NSFilenamesPboardType] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
    NSUInteger count = [o_values count];

    for (NSUInteger i = 0; i < count; i++) {
        NSDictionary *o_dic;
        char *psz_uri = vlc_path2uri([[o_values objectAtIndex:i] UTF8String], NULL);
        if (!psz_uri)
            continue;

        o_dic = [NSDictionary dictionaryWithObject:toNSStr(psz_uri) forKey:@"ITEM_URL"];
        free(psz_uri);

        o_array = [o_array arrayByAddingObject: o_dic];
    }

    return o_array;
}

654
- (void)addPlaylistItems:(NSArray*)array
655
656
{

657
    int i_plItemId = -1;
658

659
660
661
    // add items directly to media library if this is the current root
    if ([[self model] currentRootType] == ROOT_TYPE_MEDIALIBRARY)
        i_plItemId = [[[self model] rootItem] plItemId];
hartman's avatar
hartman committed
662

David's avatar
David committed
663
    BOOL b_autoplay = var_InheritBool(getIntf(), "macosx-autoplay");
hartman's avatar
hartman committed
664

665
    [self addPlaylistItems:array withParentItemId:i_plItemId atPos:-1 startPlayback:b_autoplay];
666
667
}

668
- (void)addPlaylistItems:(NSArray*)array withParentItemId:(int)i_plItemId atPos:(int)i_position startPlayback:(BOOL)b_start
669
{
David's avatar
David committed
670
    playlist_t * p_playlist = pl_Get(getIntf());
671
    PL_LOCK;
672

673
674
675
676
677
    playlist_item_t *p_parent = NULL;
    if (i_plItemId >= 0)
        p_parent = playlist_ItemGetById(p_playlist, i_plItemId);
    else
        p_parent = p_playlist->p_playing;
678

679
680
681
682
683
    if (!p_parent) {
        PL_UNLOCK;
        return;
    }

684
    NSUInteger count = [array count];
685
686
    int i_current_offset = 0;
    for (NSUInteger i = 0; i < count; ++i) {
687

688
        NSDictionary *o_current_item = array[i];
689
        input_item_t *p_input = [self createItem: o_current_item];
690
691
        if (!p_input)
            continue;
bigben's avatar
*all :    
bigben committed
692

693
694
695
696
697
        int i_pos = (i_position == -1) ? PLAYLIST_END : i_position + i_current_offset++;
        playlist_item_t *p_item = playlist_NodeAddInput(p_playlist, p_input, p_parent,
                                                        PLAYLIST_INSERT, i_pos, pl_Locked);
        if (!p_item)
            continue;
698

699
700
        if (i == 0 && b_start) {
            playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, p_parent, p_item);
hartman's avatar
hartman committed
701
        }
702
        input_item_Release(p_input);
703
    }
704
    PL_UNLOCK;
Jon Lech Johansen's avatar
Jon Lech Johansen committed
705
}
706

707
708
- (IBAction)recursiveExpandNode:(id)sender
{
709
    NSIndexSet * selectedRows = [_outlineView selectedRowIndexes];
710
711
712
    NSUInteger count = [selectedRows count];
    NSUInteger indexes[count];
    [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
713

714
    id item;
715
716
    playlist_item_t *p_item;
    for (NSUInteger i = 0; i < count; i++) {
717
        item = [_outlineView itemAtRow: indexes[i]];
718
719
720

        /* We need to collapse the node first, since OSX refuses to recursively
         expand an already expanded node, even if children nodes are collapsed. */
721
722
723
        if ([_outlineView isExpandable:item]) {
            [_outlineView collapseItem: item collapseChildren: YES];
            [_outlineView expandItem: item expandChildren: YES];
724
        }
725

726
        selectedRows = [_outlineView selectedRowIndexes];
727
728
        [selectedRows getIndexes:indexes maxCount:count inIndexRange:nil];
    }
729
730
}

bigben's avatar
bigben committed
731
732
- (NSMenu *)menuForEvent:(NSEvent *)o_event
{
733
734
735
    if (!b_playlistmenu_nib_loaded)
        b_playlistmenu_nib_loaded = [NSBundle loadNibNamed:@"PlaylistMenu" owner:self];

bigben's avatar
bigben committed
736
    NSPoint pt;
737
738
    bool b_rows;
    bool b_item_sel;
bigben's avatar
bigben committed
739

740
741
742
743
    pt = [_outlineView convertPoint: [o_event locationInWindow] fromView: nil];
    int row = [_outlineView rowAtPoint:pt];
    if (row != -1 && ![[_outlineView selectedRowIndexes] containsIndex: row])
        [_outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
744

745
746
    b_item_sel = (row != -1 && [_outlineView selectedRow] != -1);
    b_rows = [_outlineView numberOfRows] != 0;
bigben's avatar
bigben committed
747

David's avatar
David committed
748
    playlist_t *p_playlist = pl_Get(getIntf());
David's avatar
David committed
749
    bool b_del_allowed = [[self model] editAllowed];
750

751
    // TODO move other items to menu validation protocol
752
753
754
755
756
757
758
759
    [_infoPlaylistMenuItem setEnabled: b_item_sel];
    [_preparsePlaylistMenuItem setEnabled: b_item_sel];
    [_recursiveExpandPlaylistMenuItem setEnabled: b_item_sel];
    [_sortNamePlaylistMenuItem setEnabled: b_item_sel];
    [_sortAuthorPlaylistMenuItem setEnabled: b_item_sel];
    [_downloadCoverArtPlaylistMenuItem setEnabled: b_item_sel];

    return _playlistMenu;
bigben's avatar
bigben committed
760
}
761

762
- (void)outlineView:(NSOutlineView *)outlineView didClickTableColumn:(NSTableColumn *)aTableColumn
763
{
764
    int type = 0;
David's avatar
David committed
765
    intf_thread_t *p_intf = getIntf();
766
    NSString * identifier = [aTableColumn identifier];
767

768
    playlist_t *p_playlist = pl_Get(p_intf);
769

770
    if (_sortTableColumn == aTableColumn)
771
772
        b_isSortDescending = !b_isSortDescending;
    else
773
        b_isSortDescending = false;
774

775
    if (b_isSortDescending)
776
        type = ORDER_REVERSE;
777
    else
778
        type = ORDER_NORMAL;
779

780
    [[self model] sortForColumn:identifier withMode:type];
David's avatar
David committed
781
782
783
784
785

    // TODO rework, why do we need a full call here?
//    [self playlistUpdated];

    /* Clear indications of any existing column sorting */
786
    NSUInteger count = [[_outlineView tableColumns] count];
David's avatar
David committed
787
    for (NSUInteger i = 0 ; i < count ; i++)
788
        [_outlineView setIndicatorImage:nil inTableColumn: [_outlineView tableColumns][i]];
David's avatar
David committed
789

790
    [_outlineView setHighlightedTableColumn:nil];
791
792
    _sortTableColumn = aTableColumn;
    [_outlineView setHighlightedTableColumn:aTableColumn];
793

794
    if (b_isSortDescending)
795
        [_outlineView setIndicatorImage:_descendingSortingImage inTableColumn:aTableColumn];
796
    else
797
        [_outlineView setIndicatorImage:_ascendingSortingImage inTableColumn:aTableColumn];
798
799
}

800
801

- (void)outlineView:(NSOutlineView *)outlineView
David's avatar
David committed
802
803
804
    willDisplayCell:(id)cell
     forTableColumn:(NSTableColumn *)tableColumn
               item:(id)item
805
{
806
    /* this method can be called when VLC is already dead, hence the extra checks */
David's avatar
David committed
807
    intf_thread_t * p_intf = getIntf();
808
809
    if (!p_intf)
        return;
810
    playlist_t *p_playlist = pl_Get(p_intf);
811

812
    NSFont *fontToUse;
David's avatar
David committed
813
    if (var_InheritBool(getIntf(), "macosx-large-text"))
814
815
816
817
        fontToUse = [NSFont systemFontOfSize:13.];
    else
        fontToUse = [NSFont systemFontOfSize:11.];

David's avatar
David committed
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
    BOOL b_is_playing = NO;
    PL_LOCK;
    playlist_item_t *p_current_item = playlist_CurrentPlayingItem(p_playlist);
    if (p_current_item) {
        b_is_playing = p_current_item->i_id == [item plItemId];
    }
    PL_UNLOCK;

    /*
     TODO: repaint all items bold:
     [self isItem: [o_playing_item pointerValue] inNode: [item pointerValue] checkItemExistence:YES locked:NO]
     || [o_playing_item isEqual: item]
     */

    if (b_is_playing)
833
        [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toHaveTrait:NSBoldFontMask]];
834
    else
835
        [cell setFont: [[NSFontManager sharedFontManager] convertFont:fontToUse toNotHaveTrait:NSBoldFontMask]];
836
837
}

838
// TODO remove method
839
840
- (NSArray *)draggedItems
{
841
    return [[self model] draggedItems];
842
}
843

844
845
846
- (BOOL)isValidResumeItem:(input_item_t *)p_item
{
    char *psz_url = input_item_GetURI(p_item);
847
    NSString *urlString = toNSStr(psz_url);
848
849
    free(psz_url);

850
    if ([urlString isEqualToString:@""])
851
852
        return NO;

853
    NSURL *url = [NSURL URLWithString:urlString];
854

855
    if (![url isFileURL])
856
857
858
        return NO;

    BOOL isDir = false;
859
    if (![[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDir])
860
861
862
863
864
865
866
867
        return NO;

    if (isDir)
        return NO;

    return YES;
}

868
- (void)continuePlaybackWhereYouLeftOff:(input_thread_t *)p_input_thread
869
870
{
    NSDictionary *recentlyPlayedFiles = [[NSUserDefaults standardUserDefaults] objectForKey:@"recentlyPlayedMedia"];
871
872
    if (!recentlyPlayedFiles)
        return;
873

874
875
876
    input_item_t *p_item = input_GetItem(p_input_thread);
    if (!p_item)
        return;
877

878
879
880
    /* allow the user to over-write the start/stop/run-time */
    if (var_GetFloat(p_input_thread, "run-time") > 0 ||
        var_GetFloat(p_input_thread, "start-time") > 0 ||
881
        var_GetFloat(p_input_thread, "stop-time") != 0) {
882
883
        return;
    }
884

885
886
887
    /* check for file existance before resuming */
    if (![self isValidResumeItem:p_item])
        return;
888

889
    char *psz_url = vlc_uri_decode(input_item_GetURI(p_item));
890
891
892
893
    if (!psz_url)
        return;
    NSString *url = toNSStr(psz_url);
    free(psz_url);
894

895
896
897
    NSNumber *lastPosition = [recentlyPlayedFiles objectForKey:url];
    if (!lastPosition || lastPosition.intValue <= 0)
        return;
898

David's avatar
David committed
899
    int settingValue = config_GetInt(getIntf(), "macosx-continue-playback");
900
901
    if (settingValue == 2) // never resume
        return;
902

903
    CompletionBlock completionBlock = ^(enum ResumeResult result) {
904

905
906
        if (result == RESUME_RESTART)
            return;
907

908
        mtime_t lastPos = (mtime_t)lastPosition.intValue * 1000000;
David's avatar
David committed
909
        msg_Dbg(getIntf(), "continuing playback at %lld", lastPos);
910
911
        var_SetInteger(p_input_thread, "time", lastPos);
    };
912

913
914
915
    if (settingValue == 1) { // always
        completionBlock(RESUME_NOW);
        return;
916
    }
917

918
919
920
    [[[VLCMain sharedInstance] resumeDialog] showWindowWithItem:p_item
                                               withLastPosition:lastPosition.intValue
                                                completionBlock:completionBlock];
921

922
923
}

924
925
- (void)storePlaybackPositionForItem:(input_thread_t *)p_input_thread
{
David's avatar
David committed
926
    if (!var_InheritBool(getIntf(), "macosx-recentitems"))
927
928
        return;

929
930
931
932
    input_item_t *p_item = input_GetItem(p_input_thread);
    if (!p_item)
        return;

933
    if (![self isValidResumeItem:p_item])
934
935
        return;

936
    char *psz_url = vlc_uri_decode(input_item_GetURI(p_item));
937
    if (!psz_url)
938
        return;
939
940
    NSString *url = toNSStr(psz_url);
    free(psz_url);
941
942
943
944

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *mutDict = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:@"recentlyPlayedMedia"]];

945
    float relativePos = var_GetFloat(p_input_thread, "position");
946
    mtime_t pos = var_GetInteger(p_input_thread, "time") / CLOCK_FREQ;
947
948
    mtime_t dur = input_item_GetDuration(p_item) / 1000000;

949
    NSMutableArray *mediaList = [[defaults objectForKey:@"recentlyPlayedMediaList"] mutableCopy];
950

951
952
    if (relativePos > .05 && relativePos < .95 && dur > 180) {
        [mutDict setObject:[NSNumber numberWithInt:pos] forKey:url];
953
954
955
956
957
958

        [mediaList removeObject:url];
        [mediaList addObject:url];
        NSUInteger mediaListCount = mediaList.count;
        if (mediaListCount > 30) {
            for (NSUInteger x = 0; x < mediaListCount - 30; x++) {
959
                [mutDict removeObjectForKey:[mediaList firstObject]];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
960
                [mediaList removeObjectAtIndex:0];
961
962
963
964
965
966
967
968
969
970
971
            }
        }
    } else {
        [mutDict removeObjectForKey:url];
        [mediaList removeObject:url];
    }
    [defaults setObject:mutDict forKey:@"recentlyPlayedMedia"];
    [defaults setObject:mediaList forKey:@"recentlyPlayedMediaList"];
    [defaults synchronize];
}

hartman's avatar
hartman committed
972
@end