playlist.m 52.4 KB
Newer Older
1
/*****************************************************************************
2
 * playlist.m: MacOS X interface module
3
 *****************************************************************************
4
* Copyright (C) 2002-2008 the VideoLAN team
Benjamin Pracht's avatar
Benjamin Pracht committed
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 11 12 13 14
 *
 * 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.
Benjamin Pracht's avatar
Benjamin Pracht committed
15
 *
16 17 18 19 20 21 22
 * 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
Antoine Cellerier's avatar
Antoine Cellerier committed
23
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24 25
 *****************************************************************************/

26
/* TODO
27
 * add 'icons' for different types of nodes? (http://www.cocoadev.com/index.pl?IconAndTextInTableCell)
28 29 30 31 32 33
 * reimplement enable/disable item
 * create a new 'tool' button (see the gear button in the Finder window) for 'actions'
   (adding service discovery, other views, new node/playlist, save node/playlist) stuff like that
 */


34 35 36 37 38 39
/*****************************************************************************
 * Preamble
 *****************************************************************************/
#include <stdlib.h>                                      /* malloc(), free() */
#include <sys/param.h>                                    /* for MAXPATHLEN */
#include <string.h>
40
#include <math.h>
41
#include <sys/mount.h>
42
#include <vlc_keys.h>
43

44
#import "intf.h"
45 46
#import "wizard.h"
#import "bookmarks.h"
47
#import "playlistinfo.h"
48 49 50 51
#import "playlist.h"
#import "controls.h"
#import "vlc_osd.h"
#import "misc.h"
52
#import <vlc_interface.h>
53

54
/*****************************************************************************
55
 * VLCPlaylistView implementation
56 57 58 59 60
 *****************************************************************************/
@implementation VLCPlaylistView

- (NSMenu *)menuForEvent:(NSEvent *)o_event
{
Jon Lech Johansen's avatar
Jon Lech Johansen committed
61
    return( [[self delegate] menuForEvent: o_event] );
62 63
}

64 65 66
- (void)keyDown:(NSEvent *)o_event
{
    unichar key = 0;
Benjamin Pracht's avatar
Benjamin Pracht committed
67

68 69 70 71 72 73 74
    if( [[o_event characters] length] )
    {
        key = [[o_event characters] characterAtIndex: 0];
    }

    switch( key )
    {
75 76 77 78
        case NSDeleteCharacter:
        case NSDeleteFunctionKey:
        case NSDeleteCharFunctionKey:
        case NSBackspaceCharacter:
Benjamin Pracht's avatar
Benjamin Pracht committed
79
            [[self delegate] deleteItem:self];
80
            break;
Benjamin Pracht's avatar
Benjamin Pracht committed
81

82 83
        case NSEnterCharacter:
        case NSCarriageReturnCharacter:
84
            [(VLCPlaylist *)[[VLCMain sharedInstance] getPlaylist] playItem:self];
85 86
            break;

87 88 89 90 91 92
        default:
            [super keyDown: o_event];
            break;
    }
}

93 94 95
@end

/*****************************************************************************
96 97 98 99 100 101 102
 * VLCPlaylistCommon implementation
 *
 * This class the superclass of the VLCPlaylist and VLCPlaylistWizard.
 * It contains the common methods and elements of these 2 entities.
 *****************************************************************************/
@implementation VLCPlaylistCommon

103 104 105 106 107 108 109 110
- (id)init
{
    self = [super init];
    if ( self != nil )
    {
        o_outline_dict = [[NSMutableDictionary alloc] init];
    }
    return self;
111
}
112 113
- (void)awakeFromNib
{
114
    playlist_t * p_playlist = pl_Yield( VLCIntf );
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
    [o_outline_view setTarget: self];
    [o_outline_view setDelegate: self];
    [o_outline_view setDataSource: self];

    vlc_object_release( p_playlist );
    [self initStrings];
}

- (void)initStrings
{
    [[o_tc_name headerCell] setStringValue:_NS("Name")];
    [[o_tc_author headerCell] setStringValue:_NS("Author")];
    [[o_tc_duration headerCell] setStringValue:_NS("Duration")];
}

- (NSOutlineView *)outlineView
{
    return o_outline_view;
}

- (playlist_item_t *)selectedPlaylistItem
{
    return [[o_outline_view itemAtRow: [o_outline_view selectedRow]]
                                                                pointerValue];
}

@end

@implementation VLCPlaylistCommon (NSOutlineViewDataSource)

/* return the number of children for Obj-C pointer item */ /* DONE */
- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
    int i_return = 0;
149
    playlist_item_t *p_item = NULL;
150
    playlist_t * p_playlist = pl_Yield( VLCIntf );
151
    NSAssert( outlineView != o_outline_view )
152

153
    if( !item )
154
        p_item = p_playlist->p_root_category;
155
    else
156
        p_item = (playlist_item_t *)[item pointerValue];
157

158
    if( p_item )
159
        i_return = p_item->i_children;
160 161

    pl_Release( VLCIntf );
162

163
    return i_return > 0 ? i_return : 0;
164 165 166 167 168
}

/* return the child at index for the Obj-C pointer item */ /* DONE */
- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
{
169 170
    playlist_item_t *p_return = NULL, *p_item = NULL;
    NSValue *o_value;
171
    playlist_t * p_playlist = pl_Yield( VLCIntf );
172 173 174 175

    if( item == nil )
    {
        /* root object */
176
        p_item = p_playlist->p_root_category;
177 178 179
    }
    else
    {
180
        p_item = (playlist_item_t *)[item pointerValue];
181
    }
182 183
    if( p_item && index < p_item->i_children && index >= 0 )
        p_return = p_item->pp_children[index];
184
 
185 186
    vlc_object_release( p_playlist );

187
    o_value = [o_outline_dict objectForKey:[NSString stringWithFormat: @"%p", p_return]];
188

189 190
    if( o_value == nil )
    {
191
        msg_Warn( VLCIntf, "playlist item misses pointer value, adding one" );
192 193
        o_value = [[NSValue valueWithPointer: p_return] retain];
    }
194 195 196 197 198 199 200
    return o_value;
}

/* is the item expandable */
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
    int i_return = 0;
201
    playlist_t *p_playlist = pl_Yield( VLCIntf );
202 203 204 205

    if( item == nil )
    {
        /* root object */
206
        if( p_playlist->p_root_category )
207
        {
208
            i_return = p_playlist->p_root_category->i_children;
209 210 211 212 213 214 215 216 217 218
        }
    }
    else
    {
        playlist_item_t *p_item = (playlist_item_t *)[item pointerValue];
        if( p_item )
            i_return = p_item->i_children;
    }
    vlc_object_release( p_playlist );

219
    return (i_return > 0);
220 221 222 223 224 225 226
}

/* retrieve the string values for the cells */
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)o_tc byItem:(id)item
{
    id o_value = nil;
    playlist_item_t *p_item;
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241

    /* For error handling */
    static BOOL attempted_reload = NO;

    if( item == nil || ![item isKindOfClass: [NSValue class]] )
    {
        /* Attempt to fix the error by asking for a data redisplay
         * This might cause infinite loop, so add a small check */
        if( !attempted_reload )
        {
            attempted_reload = YES;
            [outlineView reloadData];
        }
        return @"error" ;
    }
242
 
243
    p_item = (playlist_item_t *)[item pointerValue];
244
    if( !p_item || !p_item->p_input )
245
    {
246 247 248 249 250 251 252 253
        /* Attempt to fix the error by asking for a data redisplay
         * This might cause infinite loop, so add a small check */
        if( !attempted_reload )
        {
            attempted_reload = YES;
            [outlineView reloadData];
        }
        return @"error";
254
    }
255
 
256 257
    attempted_reload = NO;

258 259
    if( [[o_tc identifier] isEqualToString:@"1"] )
    {
260
        /* sanity check to prevent the NSString class from crashing */
261
        char *psz_title =  input_item_GetTitle( p_item->p_input );
262
        if( !EMPTY_STR( psz_title ) )
263
        {
264
            o_value = [NSString stringWithUTF8String: psz_title];
265
        }
266
        else
267
        {
268
            char *psz_name = input_item_GetName( p_item->p_input );
269
            if( !EMPTY_STR( psz_name ) )
270 271 272
            {
                o_value = [NSString stringWithUTF8String: psz_name];
            }
273
            free( psz_name );
274
        }
275
        free( psz_title );
276
    }
277
    else
278
    {
279 280
        char *psz_artist = input_item_GetArtist( p_item->p_input );
        if( [[o_tc identifier] isEqualToString:@"2"] && !EMPTY_STR( psz_artist ) )
281
        {
282
            o_value = [NSString stringWithUTF8String: psz_artist];
283
        }
284
        else if( [[o_tc identifier] isEqualToString:@"3"] )
285
        {
286
            char psz_duration[MSTRTIME_MAX_SIZE];
287
            mtime_t dur = input_item_GetDuration( p_item->p_input );
288 289 290 291 292 293 294 295 296
            if( dur != -1 )
            {
                secstotimestr( psz_duration, dur/1000000 );
                o_value = [NSString stringWithUTF8String: psz_duration];
            }
            else
            {
                o_value = @"-:--:--";
            }
297
        }
298
        free( psz_artist );
299 300 301 302 303 304 305 306 307 308 309 310 311 312
    }

    return( o_value );
}

@end

/*****************************************************************************
 * VLCPlaylistWizard implementation
 *****************************************************************************/
@implementation VLCPlaylistWizard

- (IBAction)reloadOutlineView
{
313 314 315 316 317 318
    /* Only reload the outlineview if the wizard window is open since this can
       be quite long on big playlists */
    if( [[o_outline_view window] isVisible] )
    {
        [o_outline_view reloadData];
    }
319 320 321 322
}

@end

323 324 325 326 327 328 329 330 331 332 333 334 335
/*****************************************************************************
 * extension to NSOutlineView's interface to fix compilation warnings
 * and let us access these 2 functions properly
 * this uses a private Apple-API, but works fine on all current OSX releases
 * keep checking for compatiblity with future releases though
 *****************************************************************************/

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


336 337
/*****************************************************************************
 * VLCPlaylist implementation
338 339 340
 *****************************************************************************/
@implementation VLCPlaylist

341 342 343
- (id)init
{
    self = [super init];
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
344
    if ( self != nil )
345
    {
Benjamin Pracht's avatar
Benjamin Pracht committed
346 347
        o_nodes_array = [[NSMutableArray alloc] init];
        o_items_array = [[NSMutableArray alloc] init];
348 349 350 351
    }
    return self;
}

352 353
- (void)awakeFromNib
{
354
    playlist_t * p_playlist = pl_Yield( VLCIntf );
355

356
    int i;
357

358
    [super awakeFromNib];
359

360
    [o_outline_view setDoubleAction: @selector(playItem:)];
361

362
    [o_outline_view registerForDraggedTypes:
Benjamin Pracht's avatar
Benjamin Pracht committed
363 364
        [NSArray arrayWithObjects: NSFilenamesPboardType,
        @"VLCPlaylistItemPboardType", nil]];
365
    [o_outline_view setIntercellSpacing: NSMakeSize (0.0, 1.0)];
366

367
    /* This uses private Apple API which works fine until 10.5.
368
     * We need to keep checking in the future!
369 370 371
     * These methods are being added artificially to NSOutlineView's interface above */
    o_ascendingSortingImage = [[NSOutlineView class] _defaultTableHeaderSortImage];
    o_descendingSortingImage = [[NSOutlineView class] _defaultTableHeaderReverseSortImage];
Benjamin Pracht's avatar
Benjamin Pracht committed
372

373
    o_tc_sortColumn = nil;
374

375 376
    char ** ppsz_name;
    char ** ppsz_services = services_discovery_GetServicesNames( p_playlist, &ppsz_name );
377 378 379 380 381
    if( !ppsz_services )
    {
        vlc_object_release( p_playlist );
        return;
    }
382 383
    
    for( i = 0; ppsz_services[i]; i++ )
384
    {
385
        bool  b_enabled;
386 387
        char        *objectname;
        NSMenuItem  *o_lmi;
388

389 390 391 392 393 394 395 396 397 398
        char * name = ppsz_name[i] ? ppsz_name[i] : ppsz_services[i];
        /* Check whether to enable these menuitems */
        b_enabled = playlist_IsServicesDiscoveryLoaded( p_playlist, objectname );

        /* Create the menu entries used in the playlist menu */
        o_lmi = [[o_mi_services submenu] addItemWithTitle:
                 [NSString stringWithUTF8String: name]
                                         action: @selector(servicesChange:)
                                         keyEquivalent: @""];
        [o_lmi setTarget: self];
399
        [o_lmi setRepresentedObject: [NSString stringWithUTF8String: ppsz_services[i]]];
400 401 402 403 404 405 406 407
        if( b_enabled ) [o_lmi setState: NSOnState];

        /* Create the menu entries for the main menu */
        o_lmi = [[o_mm_mi_services submenu] addItemWithTitle:
                 [NSString stringWithUTF8String: name]
                                         action: @selector(servicesChange:)
                                         keyEquivalent: @""];
        [o_lmi setTarget: self];
408
        [o_lmi setRepresentedObject: [NSString stringWithUTF8String: ppsz_services[i]]];
409 410
        if( b_enabled ) [o_lmi setState: NSOnState];

411 412
        free( ppsz_services[i] );
        free( ppsz_name[i] );
413
    }
414 415 416
    free( ppsz_services );
    free( ppsz_name );

417
    vlc_object_release( p_playlist );
418
}
419

420 421 422 423 424
- (void)searchfieldChanged:(NSNotification *)o_notification
{
    [o_search_field setStringValue:[[o_notification object] stringValue]];
}

425 426
- (void)initStrings
{
427 428
    [super initStrings];

429
    [o_mi_save_playlist setTitle: _NS("Save Playlist...")];
Jon Lech Johansen's avatar
Jon Lech Johansen committed
430 431
    [o_mi_play setTitle: _NS("Play")];
    [o_mi_delete setTitle: _NS("Delete")];
432
    [o_mi_recursive_expand setTitle: _NS("Expand Node")];
Jon Lech Johansen's avatar
Jon Lech Johansen committed
433
    [o_mi_selectall setTitle: _NS("Select All")];
434
    [o_mi_info setTitle: _NS("Information")];
435
    [o_mi_preparse setTitle: _NS("Get Stream Information")];
436 437
    [o_mi_sort_name setTitle: _NS("Sort Node by Name")];
    [o_mi_sort_author setTitle: _NS("Sort Node by Author")];
438
    [o_mi_services setTitle: _NS("Services discovery")];
439
    [o_status_field setStringValue: _NS("No items in the playlist")];
Benjamin Pracht's avatar
Benjamin Pracht committed
440

441
    [o_search_field setToolTip: _NS("Search in Playlist")];
442
    [o_mi_addNode setTitle: _NS("Add Folder to Playlist")];
443 444 445 446

    [o_save_accessory_text setStringValue: _NS("File Format:")];
    [[o_save_accessory_popup itemAtIndex:0] setTitle: _NS("Extended M3U")];
    [[o_save_accessory_popup itemAtIndex:1] setTitle: _NS("XML Shareable Playlist Format (XSPF)")];
447 448
}

449 450
- (void)playlistUpdated
{
451
    /* Clear indications of any existing column sorting */
452
    for( unsigned int i = 0 ; i < [[o_outline_view tableColumns] count] ; i++ )
453 454 455 456 457 458 459
    {
        [o_outline_view setIndicatorImage:nil inTableColumn:
                            [[o_outline_view tableColumns] objectAtIndex:i]];
    }

    [o_outline_view setHighlightedTableColumn:nil];
    o_tc_sortColumn = nil;
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
460 461
    // TODO Find a way to keep the dict size to a minimum
    //[o_outline_dict removeAllObjects];
462
    [o_outline_view reloadData];
463
    [[[[VLCMain sharedInstance] getWizard] getPlaylistWizard] reloadOutlineView];
464
    [[[[VLCMain sharedInstance] getBookmarks] getDataTable] reloadData];
465

466
    playlist_t *p_playlist = pl_Yield( VLCIntf );
467

468
    if( playlist_CurrentSize( p_playlist ) >= 2 )
469 470
    {
        [o_status_field setStringValue: [NSString stringWithFormat:
471
                    _NS("%i items in the playlist"),
472
                playlist_CurrentSize( p_playlist )]];
473 474 475
    }
    else
    {
Clément Stenac's avatar
Clément Stenac committed
476
        if( playlist_IsEmpty( p_playlist ) )
477 478 479 480 481
            [o_status_field setStringValue: _NS("No items in the playlist")];
        else
            [o_status_field setStringValue: _NS("1 item in the playlist")];
    }
    vlc_object_release( p_playlist );
482
}
483

484 485
- (void)playModeUpdated
{
486
    playlist_t *p_playlist = pl_Yield( VLCIntf );
487

488 489 490
    bool loop = var_GetBool( p_playlist, "loop" );
    bool repeat = var_GetBool( p_playlist, "repeat" );
    if( repeat )
491
        [[[VLCMain sharedInstance] getControls] repeatOne];
492
    else if( loop )
493
        [[[VLCMain sharedInstance] getControls] repeatAll];
494
    else
495
        [[[VLCMain sharedInstance] getControls] repeatOff];
496

497
    [[[VLCMain sharedInstance] getControls] shuffle];
498 499 500 501

    vlc_object_release( p_playlist );
}

502 503
- (void)updateRowSelection
{
504
    int i_row;
505
    unsigned int j;
506

507
    playlist_t *p_playlist = pl_Yield( VLCIntf );
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
508 509
    playlist_item_t *p_item, *p_temp_item;
    NSMutableArray *o_array = [NSMutableArray array];
510 511

    p_item = p_playlist->status.p_item;
512 513 514 515 516
    if( p_item == NULL )
    {
        vlc_object_release(p_playlist);
        return;
    }
517

518
    p_temp_item = p_item;
519
    while( p_temp_item->p_parent )
520 521
    {
        [o_array insertObject: [NSValue valueWithPointer: p_temp_item] atIndex: 0];
522
        p_temp_item = p_temp_item->p_parent;
523 524
    }

525
    for( j = 0; j < [o_array count] - 1; j++ )
526
    {
527 528
        id o_item;
        if( ( o_item = [o_outline_dict objectForKey:
529
                            [NSString stringWithFormat: @"%p",
530
                            [[o_array objectAtIndex:j] pointerValue]]] ) != nil )
531
        {
532
            [o_outline_view expandItem: o_item];
533
        }
534 535 536

    }

537
    vlc_object_release( p_playlist );
538 539 540

    /* update our info-panel to reflect the new item */
    [[[VLCMain sharedInstance] getInfo] updatePanel];
541 542
}

543 544
/* Check if p_item is a child of p_node recursively. We need to check the item
   existence first since OSX sometimes tries to redraw items that have been
545 546 547 548 549 550
   deleted. We don't do it when not required  since this verification takes
   quite a long time on big playlists (yes, pretty hacky). */
- (BOOL)isItem: (playlist_item_t *)p_item
                    inNode: (playlist_item_t *)p_node
                    checkItemExistence:(BOOL)b_check

551
{
552
    playlist_t * p_playlist = pl_Yield( VLCIntf );
553 554
    playlist_item_t *p_temp_item = p_item;

555 556 557 558 559 560 561 562 563 564 565
    if( p_node == p_item )
    {
        vlc_object_release(p_playlist);
        return YES;
    }

    if( p_node->i_children < 1)
    {
        vlc_object_release(p_playlist);
        return NO;
    }
Benjamin Pracht's avatar
Benjamin Pracht committed
566

567 568
    if ( p_temp_item )
    {
569
        int i;
570
        PL_LOCK;
571

572 573
        if( b_check )
        {
574 575 576
        /* Since outlineView: willDisplayCell:... may call this function with
           p_items that don't exist anymore, first check if the item is still
           in the playlist. Any cleaner solution welcomed. */
577
            for( i = 0; i < p_playlist->all_items.i_size; i++ )
578
            {
579 580
                if( ARRAY_VAL( p_playlist->all_items, i) == p_item ) break;
                else if ( i == p_playlist->all_items.i_size - 1 )
581 582
                {
                    vlc_object_release( p_playlist );
583
                    PL_UNLOCK;
584 585
                    return NO;
                }
586 587
            }
        }
588

589
        while( p_temp_item )
590
        {
591
            p_temp_item = p_temp_item->p_parent;
592 593
            if( p_temp_item == p_node )
            {
594 595 596
                PL_UNLOCK;
                vlc_object_release( p_playlist );
                return YES;
597
            }
598
        }
599
        PL_UNLOCK;
600 601 602 603 604 605
    }

    vlc_object_release( p_playlist );
    return NO;
}

606
/* This method is usefull for instance to remove the selected children of an
607
   already selected node */
608 609 610 611 612 613 614 615 616 617 618 619
- (void)removeItemsFrom:(id)o_items ifChildrenOf:(id)o_nodes
{
    unsigned int i, j;
    for( i = 0 ; i < [o_items count] ; i++ )
    {
        for ( j = 0 ; j < [o_nodes count] ; j++ )
        {
            if( o_items == o_nodes)
            {
                if( j == i ) continue;
            }
            if( [self isItem: [[o_items objectAtIndex:i] pointerValue]
620 621
                    inNode: [[o_nodes objectAtIndex:j] pointerValue]
                    checkItemExistence: NO] )
622
            {
623
                [o_items removeObjectAtIndex:i];
624 625 626 627 628 629 630 631 632 633
                /* We need to execute the next iteration with the same index
                   since the current item has been deleted */
                i--;
                break;
            }
        }
    }

}

634 635
- (IBAction)savePlaylist:(id)sender
{
636
    playlist_t * p_playlist = pl_Yield( VLCIntf );
637 638

    NSSavePanel *o_save_panel = [NSSavePanel savePanel];
639 640 641
    NSString * o_name = [NSString stringWithFormat: @"%@", _NS("Untitled")];

    //[o_save_panel setAllowedFileTypes: [NSArray arrayWithObjects: @"m3u", @"xpf", nil] ];
642 643
    [o_save_panel setTitle: _NS("Save Playlist")];
    [o_save_panel setPrompt: _NS("Save")];
644
    [o_save_panel setAccessoryView: o_save_accessory_view];
645 646 647 648

    if( [o_save_panel runModalForDirectory: nil
            file: o_name] == NSOKButton )
    {
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666
        NSString *o_filename = [o_save_panel filename];

        if( [o_save_accessory_popup indexOfSelectedItem] == 1 )
        {
            NSString * o_real_filename;
            NSRange range;
            range.location = [o_filename length] - [@".xspf" length];
            range.length = [@".xspf" length];

            if( [o_filename compare:@".xspf" options: NSCaseInsensitiveSearch
                                             range: range] != NSOrderedSame )
            {
                o_real_filename = [NSString stringWithFormat: @"%@.xspf", o_filename];
            }
            else
            {
                o_real_filename = o_filename;
            }
667 668
            playlist_Export( p_playlist,
                [o_real_filename fileSystemRepresentation],
669
                p_playlist->p_local_category, "export-xspf" );
670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
        }
        else
        {
            NSString * o_real_filename;
            NSRange range;
            range.location = [o_filename length] - [@".m3u" length];
            range.length = [@".m3u" length];

            if( [o_filename compare:@".m3u" options: NSCaseInsensitiveSearch
                                             range: range] != NSOrderedSame )
            {
                o_real_filename = [NSString stringWithFormat: @"%@.m3u", o_filename];
            }
            else
            {
                o_real_filename = o_filename;
            }
687
            playlist_Export( p_playlist,
688 689
                [o_real_filename fileSystemRepresentation],
                p_playlist->p_local_category, "export-m3u" );
690
        }
691
    }
692
    vlc_object_release( p_playlist );
693 694
}

Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
695
/* When called retrieves the selected outlineview row and plays that node or item */
696 697 698
- (IBAction)playItem:(id)sender
{
    intf_thread_t * p_intf = VLCIntf;
699
    playlist_t * p_playlist = pl_Yield( p_intf );
700

701 702
    playlist_item_t *p_item;
    playlist_item_t *p_node = NULL;
703

704
    p_item = [[o_outline_view itemAtRow:[o_outline_view selectedRow]] pointerValue];
705

706 707 708
    if( p_item )
    {
        if( p_item->i_children == -1 )
709
        {
710
            p_node = p_item->p_parent;
711

712 713 714 715 716 717 718
        }
        else
        {
            p_node = p_item;
            if( p_node->i_children > 0 && p_node->pp_children[0]->i_children == -1 )
            {
                p_item = p_node->pp_children[0];
719 720 721
            }
            else
            {
722
                p_item = NULL;
723 724
            }
        }
725
        playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, true, p_node, p_item );
726
    }
727
    vlc_object_release( p_playlist );
728
}
729

730 731 732 733 734 735
/* When called retrieves the selected outlineview row and plays that node or item */
- (IBAction)preparseItem:(id)sender
{
    int i_count;
    NSMutableArray *o_to_preparse;
    intf_thread_t * p_intf = VLCIntf;
736
    playlist_t * p_playlist = pl_Yield( p_intf );
737
 
738 739 740
    o_to_preparse = [NSMutableArray arrayWithArray:[[o_outline_view selectedRowEnumerator] allObjects]];
    i_count = [o_to_preparse count];

741 742 743 744 745
    int i, i_row;
    NSNumber *o_number;
    playlist_item_t *p_item = NULL;

    for( i = 0; i < i_count; i++ )
746
    {
747 748 749 750 751
        o_number = [o_to_preparse lastObject];
        i_row = [o_number intValue];
        p_item = [[o_outline_view itemAtRow:i_row] pointerValue];
        [o_to_preparse removeObject: o_number];
        [o_outline_view deselectRow: i_row];
752

753
        if( p_item )
754
        {
755
            if( p_item->i_children == -1 )
756
            {
757 758 759 760
                playlist_PreparseEnqueue( p_playlist, p_item->p_input );
            }
            else
            {
761
                msg_Dbg( p_intf, "preparsing nodes not implemented" );
762 763 764
            }
        }
    }
765
    vlc_object_release( p_playlist );
766 767 768
    [self playlistUpdated];
}

769 770 771 772
- (IBAction)servicesChange:(id)sender
{
    NSMenuItem *o_mi = (NSMenuItem *)sender;
    NSString *o_string = [o_mi representedObject];
773
    playlist_t * p_playlist = pl_Yield( VLCIntf );
774 775
    if( !playlist_IsServicesDiscoveryLoaded( p_playlist, [o_string UTF8String] ) )
        playlist_ServicesDiscoveryAdd( p_playlist, [o_string UTF8String] );
776
    else
777
        playlist_ServicesDiscoveryRemove( p_playlist, [o_string UTF8String] );
778 779

    [o_mi setState: playlist_IsServicesDiscoveryLoaded( p_playlist,
780
                                          [o_string UTF8String] ) ? YES : NO];
781

782
    vlc_object_release( p_playlist );
783 784 785 786
    [self playlistUpdated];
    return;
}

787 788 789 790 791
- (IBAction)selectAll:(id)sender
{
    [o_outline_view selectAll: nil];
}

Benjamin Pracht's avatar
Benjamin Pracht committed
792 793
- (IBAction)deleteItem:(id)sender
{
794
    int i_count, i_row;
Benjamin Pracht's avatar
Benjamin Pracht committed
795 796 797 798 799 800 801
    NSMutableArray *o_to_delete;
    NSNumber *o_number;

    playlist_t * p_playlist;
    intf_thread_t * p_intf = VLCIntf;

    o_to_delete = [NSMutableArray arrayWithArray:[[o_outline_view selectedRowEnumerator] allObjects]];
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
802
    i_count = [o_to_delete count];
Benjamin Pracht's avatar
Benjamin Pracht committed
803

804 805 806 807
    p_playlist = pl_Yield( p_intf );

    PL_LOCK;
    for( int i = 0; i < i_count; i++ )
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
808
    {
Benjamin Pracht's avatar
Benjamin Pracht committed
809 810
        o_number = [o_to_delete lastObject];
        i_row = [o_number intValue];
811 812
        id o_item = [o_outline_view itemAtRow: i_row];
        playlist_item_t *p_item = [o_item pointerValue];
813 814 815 816
#ifndef NDEBUG
        msg_Dbg( p_intf, "deleting item %i (of %i) with id \"%i\", pointerValue \"%p\" and %i children", i+1, i_count, 
                p_item->p_input->i_id, [o_item pointerValue], p_item->i_children +1 );
#endif
Benjamin Pracht's avatar
Benjamin Pracht committed
817 818
        [o_to_delete removeObject: o_number];
        [o_outline_view deselectRow: i_row];
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
819

820
        if( p_item->i_children != -1 )
821
        //is a node and not an item
Benjamin Pracht's avatar
Benjamin Pracht committed
822
        {
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
823
            if( p_playlist->status.i_status != PLAYLIST_STOPPED &&
824
                [self isItem: p_playlist->status.p_item inNode:
825 826
                        ((playlist_item_t *)[o_item pointerValue])
                        checkItemExistence: NO] == YES )
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
827 828
                // if current item is in selected node and is playing then stop playlist
                playlist_Stop( p_playlist );
829
    
830
            playlist_NodeDelete( p_playlist, p_item, true, false );
Benjamin Pracht's avatar
Benjamin Pracht committed
831 832
        }
        else
833
            playlist_DeleteFromInput( p_playlist, p_item->p_input->i_id, true );
Benjamin Pracht's avatar
Benjamin Pracht committed
834
    }
835 836
    PL_UNLOCK;

Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
837 838
    [self playlistUpdated];
    vlc_object_release( p_playlist );
Benjamin Pracht's avatar
Benjamin Pracht committed
839
}
840

841 842 843 844 845 846 847
- (IBAction)sortNodeByName:(id)sender
{
    [self sortNode: SORT_TITLE];
}

- (IBAction)sortNodeByAuthor:(id)sender
{
Clément Stenac's avatar
Clément Stenac committed
848
    [self sortNode: SORT_ARTIST];
849 850 851 852
}

- (void)sortNode:(int)i_mode
{
853
    playlist_t * p_playlist = pl_Yield( VLCIntf );
854 855
    playlist_item_t * p_item;

Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
856
    if( [o_outline_view selectedRow] > -1 )
857
    {
858
        p_item = [[o_outline_view itemAtRow: [o_outline_view selectedRow]] pointerValue];
859 860 861 862
    }
    else
    /*If no item is selected, sort the whole playlist*/
    {
863
        p_item = p_playlist->p_root_category;
864 865
    }

Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
866
    if( p_item->i_children > -1 ) // the item is a node
867
    {
868
        PL_LOCK;
869
        playlist_RecursiveNodeSort( p_playlist, p_item, i_mode, ORDER_NORMAL );
870
        PL_UNLOCK;
871 872 873
    }
    else
    {
874
        PL_LOCK;
875 876
        playlist_RecursiveNodeSort( p_playlist,
                p_item->p_parent, i_mode, ORDER_NORMAL );
877
        PL_UNLOCK;
878
    }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
879
    vlc_object_release( p_playlist );
880 881 882
    [self playlistUpdated];
}

883
- (input_item_t *)createItem:(NSDictionary *)o_one_item
884
{
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
885
    intf_thread_t * p_intf = VLCIntf;
886
    playlist_t * p_playlist = pl_Yield( p_intf );
887

888
    input_item_t *p_input;
889 890 891 892 893 894 895 896 897 898 899
    int i;
    BOOL b_rem = FALSE, b_dir = FALSE;
    NSString *o_uri, *o_name;
    NSArray *o_options;
    NSURL *o_true_file;

    /* Get the item */
    o_uri = (NSString *)[o_one_item objectForKey: @"ITEM_URL"];
    o_name = (NSString *)[o_one_item objectForKey: @"ITEM_NAME"];
    o_options = (NSArray *)[o_one_item objectForKey: @"ITEM_OPTIONS"];

900
    /* Find the name for a disc entry (i know, can you believe the trouble?) */
901
    if( ( !o_name || [o_name isEqualToString:@""] ) && [o_uri rangeOfString: @"/dev/"].location != NSNotFound )
902
    {
903 904
        int i_count, i_index;
        struct statfs *mounts = NULL;
Benjamin Pracht's avatar
Benjamin Pracht committed
905

906 907 908
        i_count = getmntinfo (&mounts, MNT_NOWAIT);
        /* getmntinfo returns a pointer to static data. Do not free. */
        for( i_index = 0 ; i_index < i_count; i_index++ )
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
909
        {
910 911
            NSMutableString *o_temp, *o_temp2;
            o_temp = [NSMutableString stringWithString: o_uri];
912
            o_temp2 = [NSMutableString stringWithUTF8String: mounts[i_index].f_mntfromname];
913 914 915
            [o_temp replaceOccurrencesOfString: @"/dev/rdisk" withString: @"/dev/disk" options:NSLiteralSearch range:NSMakeRange(0, [o_temp length]) ];
            [o_temp2 replaceOccurrencesOfString: @"s0" withString: @"" options:NSLiteralSearch range:NSMakeRange(0, [o_temp2 length]) ];
            [o_temp2 replaceOccurrencesOfString: @"s1" withString: @"" options:NSLiteralSearch range:NSMakeRange(0, [o_temp2 length]) ];
916 917

            if( strstr( [o_temp fileSystemRepresentation], [o_temp2 fileSystemRepresentation] ) != NULL )
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
918
            {
919
                o_name = [[NSFileManager defaultManager] displayNameAtPath: [NSString stringWithUTF8String:mounts[i_index].f_mntonname]];
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
920 921
            }
        }
922 923 924
    }
    /* If no name, then make a guess */
    if( !o_name) o_name = [[NSFileManager defaultManager] displayNameAtPath: o_uri];
Benjamin Pracht's avatar
Benjamin Pracht committed
925

926 927 928 929 930 931 932 933 934 935 936 937 938
    if( [[NSFileManager defaultManager] fileExistsAtPath:o_uri isDirectory:&b_dir] && b_dir &&
        [[NSWorkspace sharedWorkspace] getFileSystemInfoForPath: o_uri isRemovable: &b_rem
                isWritable:NULL isUnmountable:NULL description:NULL type:NULL] && b_rem   )
    {
        /* All of this is to make sure CD's play when you D&D them on VLC */
        /* Converts mountpoint to a /dev file */
        struct statfs *buf;
        char *psz_dev;
        NSMutableString *o_temp;

        buf = (struct statfs *) malloc (sizeof(struct statfs));
        statfs( [o_uri fileSystemRepresentation], buf );
        psz_dev = strdup(buf->f_mntfromname);
939
        o_temp = [NSMutableString stringWithUTF8String: psz_dev ];
940 941 942
        [o_temp replaceOccurrencesOfString: @"/dev/disk" withString: @"/dev/rdisk" options:NSLiteralSearch range:NSMakeRange(0, [o_temp length]) ];
        [o_temp replaceOccurrencesOfString: @"s0" withString: @"" options:NSLiteralSearch range:NSMakeRange(0, [o_temp length]) ];
        [o_temp replaceOccurrencesOfString: @"s1" withString: @"" options:NSLiteralSearch range:NSMakeRange(0, [o_temp length]) ];
943 944 945
        o_uri = o_temp;
    }

946 947
    p_input = input_ItemNew( p_playlist, [o_uri fileSystemRepresentation], [o_name UTF8String] );
    if( !p_input )
948 949 950 951 952
       return NULL;

    if( o_options )
    {
        for( i = 0; i < (int)[o_options count]; i++ )
953
        {
954
            input_ItemAddOption( p_input, strdup( [[o_options objectAtIndex:i] UTF8String] ) );
955
        }
956
    }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
957

958 959
    /* Recent documents menu */
    o_true_file = [NSURL fileURLWithPath: o_uri];
960
    if( o_true_file != nil && (BOOL)config_GetInt( p_playlist, "macosx-recentitems" ) == YES )
961 962 963 964 965 966
    {
        [[NSDocumentController sharedDocumentController]
            noteNewRecentDocumentURL: o_true_file];
    }

    vlc_object_release( p_playlist );
967
    return p_input;
968 969 970 971 972
}

- (void)appendArray:(NSArray*)o_array atPos:(int)i_position enqueue:(BOOL)b_enqueue
{
    int i_item;
973
    playlist_t * p_playlist = pl_Yield( VLCIntf );
974 975 976

    for( i_item = 0; i_item < (int)[o_array count]; i_item++ )
    {
977
        input_item_t *p_input;
978 979 980 981
        NSDictionary *o_one_item;

        /* Get the item */
        o_one_item = [o_array objectAtIndex: i_item];
982 983
        p_input = [self createItem: o_one_item];
        if( !p_input )
984
        {
985
            continue;
986
        }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
987 988

        /* Add the item */
989
        /* FIXME: playlist_AddInput() can fail */
990
        playlist_AddInput( p_playlist, p_input, PLAYLIST_INSERT,
991 992
             i_position == -1 ? PLAYLIST_END : i_position + i_item, true,
         false );
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
993

994 995
        if( i_item == 0 && !b_enqueue )
        {
996
            playlist_item_t *p_item;
997 998
            p_item = playlist_ItemGetByInput( p_playlist, p_input, true );
            playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, true, NULL, p_item );
999
        }
1000 1001 1002
        else
        {
            playlist_item_t *p_item;
1003 1004
            p_item = playlist_ItemGetByInput( p_playlist, p_input, true );
            playlist_Control( p_playlist, PLAYLIST_SKIP, true, p_item );
1005
        }
1006
        vlc_gc_decref( p_input );
1007
    }
1008
    [self playlistUpdated];
1009 1010 1011
    vlc_object_release( p_playlist );
}

1012
- (void)appendNodeArray:(NSArray*)o_array inNode:(playlist_item_t *)p_node atPos:(int)i_position enqueue:(BOOL)b_enqueue
1013 1014
{
    int i_item;
1015
    playlist_t * p_playlist = pl_Yield( VLCIntf );
1016 1017 1018

    for( i_item = 0; i_item < (int)[o_array count]; i_item++ )
    {
1019
        input_item_t *p_input;
1020 1021 1022 1023
        NSDictionary *o_one_item;

        /* Get the item */
        o_one_item = [o_array objectAtIndex: i_item];
1024 1025
        p_input = [self createItem: o_one_item];
        if( !p_input )
Benjamin Pracht's avatar
Benjamin Pracht committed
1026
        {
1027
            continue;
1028
        }
Benjamin Pracht's avatar
Benjamin Pracht committed
1029

1030
        /* Add the item */
1031
        /* FIXME: playlist_NodeAddInput() can fail */
1032 1033 1034
       playlist_NodeAddInput( p_playlist, p_input, p_node,
                                      PLAYLIST_INSERT,
                                      i_position == -1 ?
1035
                                      PLAYLIST_END : i_position + i_item, false );
1036

1037

Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1038 1039
        if( i_item == 0 && !b_enqueue )
        {
1040
            playlist_item_t *p_item;
1041 1042
            p_item = playlist_ItemGetByInput( p_playlist, p_input, true );
            playlist_Control( p_playlist, PLAYLIST_VIEWPLAY, true, NULL, p_item );
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1043
        }
1044 1045 1046
        else
        {
            playlist_item_t *p_item;
1047 1048
            p_item = playlist_ItemGetByInput( p_playlist, p_input, true );
            playlist_Control( p_playlist, PLAYLIST_SKIP, true, p_item );
1049
        }
1050
        vlc_gc_decref( p_input );
1051
    }
1052
    [self playlistUpdated];
1053
    vlc_object_release( p_playlist );
Jon Lech Johansen's avatar
Jon Lech Johansen committed
1054
}
1055

1056 1057
- (NSMutableArray *)subSearchItem:(playlist_item_t *)p_item
{
1058
    playlist_t *p_playlist = pl_Yield( VLCIntf );
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1059
    playlist_item_t *p_selected_item;
1060 1061 1062 1063 1064 1065 1066 1067 1068
    int i_current, i_selected_row;

    i_selected_row = [o_outline_view selectedRow];
    if (i_selected_row < 0)
        i_selected_row = 0;

    p_selected_item = (playlist_item_t *)[[o_outline_view itemAtRow:
                                            i_selected_row] pointerValue];

Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1069
    for( i_current = 0; i_current < p_item->i_children ; i_current++ )
1070
    {
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1071 1072
        char *psz_temp;
        NSString *o_current_name, *o_current_author;
1073

1074
        PL_LOCK;
1075
        o_current_name = [NSString stringWithUTF8String:
1076
            p_item->pp_children[i_current]->p_input->psz_name];
1077
        psz_temp = input_ItemGetInfo( p_item->p_input ,
1078
                   _("Meta-information"),_("Artist") );
1079 1080
        o_current_author = [NSString stringWithUTF8String: psz_temp];
        free( psz_temp);
1081
        PL_UNLOCK;
1082

Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1083 1084
        if( p_selected_item == p_item->pp_children[i_current] &&
                    b_selected_item_met == NO )
1085 1086 1087
        {
            b_selected_item_met = YES;
        }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1088 1089
        else if( p_selected_item == p_item->pp_children[i_current] &&
                    b_selected_item_met == YES )
1090
        {
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1091
            vlc_object_release( p_playlist );
1092 1093
            return NULL;
        }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1094 1095
        else if( b_selected_item_met == YES &&
                    ( [o_current_name rangeOfString:[o_search_field
1096
                        stringValue] options:NSCaseInsensitiveSearch].length ||
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1097
                      [o_current_author rangeOfString:[o_search_field
1098
                        stringValue] options:NSCaseInsensitiveSearch].length ) )
1099
        {
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1100
            vlc_object_release( p_playlist );
1101 1102 1103 1104 1105
            /*Adds the parent items in the result array as well, so that we can
            expand the tree*/
            return [NSMutableArray arrayWithObject: [NSValue
                            valueWithPointer: p_item->pp_children[i_current]]];
        }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1106
        if( p_item->pp_children[i_current]->i_children > 0 )
1107 1108 1109
        {
            id o_result = [self subSearchItem:
                                            p_item->pp_children[i_current]];
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1110
            if( o_result != NULL )
1111
            {
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1112
                vlc_object_release( p_playlist );
1113 1114 1115 1116 1117 1118
                [o_result insertObject: [NSValue valueWithPointer:
                                p_item->pp_children[i_current]] atIndex:0];
                return o_result;
            }
        }
    }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1119
    vlc_object_release( p_playlist );
1120 1121 1122 1123 1124
    return NULL;
}

- (IBAction)searchItem:(id)sender
{
1125
    playlist_t * p_playlist = pl_Yield( VLCIntf );
1126 1127 1128 1129 1130 1131 1132 1133 1134
    id o_result;

    unsigned int i;
    int i_row = -1;

    b_selected_item_met = NO;

        /*First, only search after the selected item:*
         *(b_selected_item_met = NO)                 */
1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148
    o_result = [self subSearchItem:p_playlist->p_root_category];
    if( o_result == NULL )
    {
        /* If the first search failed, search again from the beginning */
        o_result = [self subSearchItem:p_playlist->p_root_category];
    }
    if( o_result != NULL )
    {
        int i_start;
        if( [[o_result objectAtIndex: 0] pointerValue] ==
                                                    p_playlist->p_local_category )
        i_start = 1;
        else
        i_start = 0;
1149

1150
        for( i = i_start ; i < [o_result count] - 1 ; i++ )
1151
        {
1152 1153 1154
            [o_outline_view expandItem: [o_outline_dict objectForKey:
                        [NSString stringWithFormat: @"%p",
                        [[o_result objectAtIndex: i] pointerValue]]]];
1155
        }
1156 1157 1158 1159 1160 1161 1162 1163 1164
        i_row = [o_outline_view rowForItem: [o_outline_dict objectForKey:
                        [NSString stringWithFormat: @"%p",
                        [[o_result objectAtIndex: [o_result count] - 1 ]
                        pointerValue]]]];
    }
    if( i_row > -1 )
    {
        [o_outline_view selectRow:i_row byExtendingSelection: NO];
        [o_outline_view scrollRowToVisible: i_row];
1165
    }
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
1166
    vlc_object_release( p_playlist );
1167 1168
}

1169 1170 1171 1172 1173 1174 1175 1176
- (IBAction)recursiveExpandNode:(id)sender
{
    id o_item = [o_outline_view itemAtRow: [o_outline_view selectedRow]];
    playlist_item_t *p_item = (playlist_item_t *)[o_item