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