source: trunk/Cocoa/Pester/Source/NJRQTMediaPopUpButton.m @ 576

Last change on this file since 576 was 576, checked in by Nicholas Riley, 10 years ago

NJRQTMediaPopUpButton.m: Hide extension on system sounds.

File size: 21.0 KB
Line 
1//
2//  NJRQTMediaPopUpButton.m
3//  Pester
4//
5//  Created by Nicholas Riley on Sat Oct 26 2002.
6//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9#import "NJRQTMediaPopUpButton.h"
10#import "NJRSoundManager.h"
11#import "QTMovie-NJRExtensions.h"
12#import "NSMenuItem-NJRExtensions.h"
13
14#include <limits.h>
15
16static const int NJRQTMediaPopUpButtonMaxRecentItems = 10;
17
18NSString * const NJRQTMediaPopUpButtonMovieChangedNotification = @"NJRQTMediaPopUpButtonMovieChangedNotification";
19
20@interface NJRQTMediaPopUpButton (Private)
21- (void)_setPath:(NSString *)path;
22- (NSMenuItem *)_itemForAlias:(BDAlias *)alias;
23- (BOOL)_validateWithPreview:(BOOL)doPreview;
24- (void)_startSoundPreview;
25- (void)_resetPreview;
26- (void)_resetOutputVolume;
27@end
28
29@implementation NJRQTMediaPopUpButton
30
31// XXX handle refreshing sound list on resume
32// XXX don't add icons on Puma, they look like ass
33// XXX launch preview on a separate thread (if movies take too long to load, they inhibit the interface responsiveness)
34
35// Recent media layout:
36// Most recent media are at TOP of menu (smaller item numbers, starting at [self indexOfItem: otherItem] + 1)
37// Most recent media are at END of array (larger indices)
38
39#pragma mark recently selected media tracking
40
41- (NSString *)_defaultKey;
42{
43    NSAssert([self tag] != 0, NSLocalizedString(@"Can't track recently selected media for popup with tag 0: please set a tag", "Assertion for QuickTime media popup button if tag is 0"));
44    return [NSString stringWithFormat: @"NJRQTMediaPopUpButtonMaxRecentItems tag %d", [self tag]];
45}
46
47- (void)_writeRecentMedia;
48{
49    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
50    [defaults setObject: recentMediaAliasData forKey: [self _defaultKey]];
51    [defaults synchronize];
52}
53
54- (NSMenuItem *)_addRecentMediaAtPath:(NSString *)path withAlias:(BDAlias *)alias;
55{
56    NSString *title = [[NSFileManager defaultManager] displayNameAtPath: path];
57    NSMenu *menu = [self menu];
58    NSMenuItem *item;
59    if (title == nil || path == nil) return nil;
60    item = [menu insertItemWithTitle: title action: @selector(_aliasSelected:) keyEquivalent: @"" atIndex: [menu indexOfItem: otherItem] + 1];
61    [item setTarget: self];
62    [item setRepresentedObject: alias];
63    [item setImageFromPath: path];
64    [recentMediaAliasData addObject: [alias aliasData]];
65    if ([recentMediaAliasData count] > NJRQTMediaPopUpButtonMaxRecentItems) {
66        [menu removeItemAtIndex: [menu numberOfItems] - 1];
67        [recentMediaAliasData removeObjectAtIndex: 0];
68    }
69    return item;
70}
71
72- (void)_addRecentMediaFromAliasesData:(NSArray *)aliasesData;
73{
74    NSEnumerator *e = [aliasesData objectEnumerator];
75    NSData *aliasData;
76    BDAlias *alias;
77    while ( (aliasData = [e nextObject]) != nil) {
78        if ( (alias = [[BDAlias alloc] initWithData: aliasData]) != nil) {
79            [self _addRecentMediaAtPath: [alias fullPath] withAlias: alias];
80            [alias release];
81        }
82    }
83}
84
85- (void)_validateRecentMedia;
86{
87    NSEnumerator *e = [recentMediaAliasData reverseObjectEnumerator];
88    NSData *aliasData;
89    NSMenuItem *item;
90    BDAlias *itemAlias;
91    int otherIndex = [self indexOfItem: otherItem];
92    int aliasDataCount = [recentMediaAliasData count];
93    int lastItemIndex = [self numberOfItems] - 1;
94    int recentItemCount = lastItemIndex - otherIndex;
95    int recentItemIndex = otherIndex;
96    NSAssert2(recentItemCount == aliasDataCount, @"Counted %d recent menu items, %d of alias data", recentItemCount, aliasDataCount);
97    while ( (aliasData = [e nextObject]) != nil) { // go BACKWARD through array while going DOWN menu
98        recentItemIndex++;
99        item = [self itemAtIndex: recentItemIndex];
100        itemAlias = [item representedObject];
101    }
102}
103
104#pragma mark initialize-release
105
106- (void)_setUp;
107{
108    NSMenu *menu = [self menu];
109    [self removeAllItems];
110    [menu setAutoenablesItems: NO];
111
112    NSMenuItem *item = [menu addItemWithTitle: @"Alert sound" action: @selector(_beepSelected:) keyEquivalent: @""];
113    [item setTarget: self];
114    [menu addItem: [NSMenuItem separatorItem]];
115
116    NSMutableArray *soundFolderPaths = [[NSMutableArray alloc] initWithCapacity: kLastDomainConstant - kSystemDomain + 1];
117    for (FSVolumeRefNum domain = kSystemDomain ; domain <= kLastDomainConstant ; domain++) {
118        OSStatus err;
119        FSRef fsr;
120        err = FSFindFolder(domain, kSystemSoundsFolderType, false, &fsr);
121        if (err != noErr) continue;
122
123        UInt8 path[PATH_MAX];
124        err = FSRefMakePath(&fsr, path, PATH_MAX);
125        if (err != noErr) continue;
126
127        CFStringRef pathString = CFStringCreateWithFileSystemRepresentation(NULL, (const char *)path);
128        if (pathString == NULL) continue;
129
130        [soundFolderPaths addObject: (NSString *)pathString];
131        CFRelease(pathString);
132    }
133    NSFileManager *fm = [NSFileManager defaultManager];
134    NSEnumerator *e = [soundFolderPaths objectEnumerator];
135    NSString *folderPath;
136    while ( (folderPath = [e nextObject]) != nil) {
137        if (![fm changeCurrentDirectoryPath: folderPath]) continue;
138
139        NSDirectoryEnumerator *de = [fm enumeratorAtPath: folderPath];
140        NSString *path;
141        NSString *displayName;
142        while ( (path = [de nextObject]) != nil) {
143            BOOL isDir;
144            if (![fm fileExistsAtPath: path isDirectory: &isDir] || isDir) {
145                [de skipDescendents];
146                continue;
147            }
148
149            if (![QTMovie canInitWithFile: path]) continue;
150           
151            displayName = [fm displayNameAtPath: path];
152            if ([[NSNumber numberWithBool: NO] isEqualTo: [[de fileAttributes] objectForKey: NSFileExtensionHidden]])
153                displayName = [displayName stringByDeletingPathExtension];
154
155            item = [menu addItemWithTitle: displayName
156                                   action: @selector(_systemSoundSelected:)
157                            keyEquivalent: @""];
158            [item setTarget: self];
159            [item setImageFromPath: path];
160            path = [folderPath stringByAppendingPathComponent: path];
161            [item setRepresentedObject: path];
162            [item setToolTip: path];
163        }
164    }
165    [soundFolderPaths release];
166   
167    if ([menu numberOfItems] == 2) {
168        item = [menu addItemWithTitle: NSLocalizedString(@"Can't locate alert sounds", "QuickTime media popup menu item surrogate for alert sound list if no sounds are found") action: nil keyEquivalent: @""];
169        [item setEnabled: NO];
170    }
171         
172    [menu addItem: [NSMenuItem separatorItem]];
173    item = [menu addItemWithTitle: NSLocalizedString(@"Other...", "Media popup item to select another sound/movie/image") action: @selector(select:) keyEquivalent: @""];
174    [item setTarget: self];
175    otherItem = [item retain];
176
177    [self _validateWithPreview: NO];
178
179    recentMediaAliasData = [[NSMutableArray alloc] initWithCapacity: NJRQTMediaPopUpButtonMaxRecentItems + 1];
180    [self _addRecentMediaFromAliasesData: [[NSUserDefaults standardUserDefaults] arrayForKey: [self _defaultKey]]];
181    // [self _validateRecentMedia];
182
183    [self registerForDraggedTypes:
184        [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]];
185}
186
187- (id)initWithFrame:(NSRect)frame;
188{
189    if ( (self = [super initWithFrame: frame]) != nil) {
190        [self _setUp];
191    }
192    return self;
193}
194
195- (id)initWithCoder:(NSCoder *)coder;
196{
197    if ( (self = [super initWithCoder: coder]) != nil) {
198        [self _setUp];
199    }
200    return self;
201}
202
203- (void)dealloc;
204{
205    [recentMediaAliasData release]; recentMediaAliasData = nil;
206    [otherItem release];
207    [selectedAlias release]; [previousAlias release];
208    [super dealloc];
209}
210
211#pragma mark accessing
212
213- (BDAlias *)selectedAlias;
214{
215    return selectedAlias;
216}
217
218- (void)_setAlias:(BDAlias *)alias;
219{
220    BDAlias *oldAlias = [selectedAlias retain];
221    [previousAlias release];
222    previousAlias = oldAlias;
223    if (selectedAlias != alias) {
224        [selectedAlias release];
225        selectedAlias = [alias retain];
226    }
227}
228
229- (void)setAlias:(BDAlias *)alias;
230{
231    [self _setAlias: alias];
232    if ([self _validateWithPreview: NO]) {
233        [self selectItem: [self _itemForAlias: selectedAlias]];
234    }
235}
236
237- (void)_setPath:(NSString *)path;
238{
239    [self _setAlias: [BDAlias aliasWithPath: path]];
240}
241
242- (NSMenuItem *)_itemForAlias:(BDAlias *)alias;
243{
244    if (alias == nil) return [self itemAtIndex: 0];
245
246    // [self _validateRecentMedia];
247    NSString *path = [alias fullPath];
248
249    // selected a system sound?
250    int itemIndex = [[self menu] indexOfItemWithRepresentedObject: path];
251    if (itemIndex != -1) {
252        // NSLog(@"_itemForAlias: selected system sound");
253        return [self itemAtIndex: itemIndex];
254    } else {
255        NSEnumerator *e = [recentMediaAliasData reverseObjectEnumerator];
256        NSData *aliasData;
257        NSMenuItem *item;
258        int recentIndex = 1;
259
260        while ( (aliasData = [e nextObject]) != nil) {
261            // selected a recently selected, non-system sound?
262            if ([alias aliasDataIsEqual: aliasData]) {
263                int otherIndex = [self indexOfItem: otherItem];
264                int menuIndex = recentIndex + otherIndex;
265                if (menuIndex == otherIndex + 1) return [self itemAtIndex: menuIndex]; // already at top
266                // remove item, add (at top) later
267                // NSLog(@"_itemForAlias removing item: idx %d + otherItemIdx %d + 1 = %d [%@]", recentIndex, otherIndex, menuIndex, [self itemAtIndex: menuIndex]);
268                [self removeItemAtIndex: menuIndex];
269                [recentMediaAliasData removeObjectAtIndex: [recentMediaAliasData count] - recentIndex];
270                break;
271            }
272            recentIndex++;
273        }
274
275        // create the item
276        item = [self _addRecentMediaAtPath: path withAlias: alias];
277        [self _writeRecentMedia];
278        return item;
279    }
280}
281
282- (BOOL)canRepeat;
283{
284    return movieCanRepeat;
285}
286
287- (BOOL)hasAudio;
288{
289    return movieHasAudio;
290}
291
292- (float)outputVolume;
293{
294    return outputVolume;
295}
296
297- (void)setOutputVolume:(float)volume withPreview:(BOOL)doPreview;
298{
299    if (![NJRSoundManager volumeIsNotMutedOrInvalid: volume]) return;
300    outputVolume = volume;
301    if (!doPreview) return;
302    // NSLog(@"setting volume to %f, preview movie %@", volume, [preview movie]);
303    if ([preview movie] == nil) {
304        [self _validateWithPreview: YES];
305    } else {
306        [self _startSoundPreview];
307    }
308}
309
310#pragma mark selected media validation
311
312- (void)_invalidateSelection;
313{
314    [self _setAlias: previousAlias];
315    [self selectItem: [self _itemForAlias: [self selectedAlias]]];
316    [[NSNotificationCenter defaultCenter] postNotificationName: NJRQTMediaPopUpButtonMovieChangedNotification object: self];
317}
318
319- (void)_startSoundPreview;
320{
321    if ([preview movie] == nil || outputVolume == kNoVolume)
322        return;
323
324    if (savedVolume || [NJRSoundManager saveDefaultOutputVolume]) {
325        savedVolume = YES;
326        [NJRSoundManager setDefaultOutputVolume: outputVolume];
327    }
328
329    if ([[preview movie] rate] != 0)
330        return; // don't restart preview if already playing
331   
332    [[NSNotificationCenter defaultCenter] addObserver: self
333                                             selector: @selector(_soundPreviewDidEnd:)
334                                                 name: QTMovieDidEndNotification
335                                               object: [preview movie]];
336    [preview play: self];
337}
338
339- (void)_soundPreviewDidEnd:(NSNotification *)notification;
340{
341    [self _resetPreview];
342}
343
344- (void)_resetPreview;
345{
346    [preview setMovie: nil];
347    [self _resetOutputVolume];
348}
349
350- (void)_resetOutputVolume;
351{
352    [NJRSoundManager restoreSavedDefaultOutputVolumeIfCurrently: outputVolume];
353    savedVolume = NO;
354}
355
356- (BOOL)_validateWithPreview:(BOOL)doPreview;
357{
358    // prevent _resetPreview from triggering afterward (crashes)
359    [[NSNotificationCenter defaultCenter] removeObserver: self
360                                                    name: QTMovieDidEndNotification
361                                                  object: [preview movie]];
362    [preview pause: self];
363    if (selectedAlias == nil) {
364        [preview setMovie: nil];
365        movieCanRepeat = YES;
366        movieHasAudio = NO; // XXX should be YES - this is broken, NSBeep() is asynchronous
367        if (doPreview) {
368            // XXX [self _updateOutputVolume];
369            NSBeep();
370            // XXX [self _resetOutputVolume];
371        }
372    } else {
373        NSError *error;
374        QTMovie *movie = [[QTMovie alloc] initWithFile: [selectedAlias fullPath] error: &error];
375        movieCanRepeat = ![movie NJR_isStatic];
376        if (movieHasAudio = [movie NJR_hasAudio]) {
377            [preview setMovie: doPreview ? movie : nil];
378        } else {
379            [self _resetPreview];
380            doPreview = NO;
381            if (movie == nil) {
382                NSBeginAlertSheet(@"Format not recognized", nil, nil, nil, [self window], nil, nil, nil, nil, [NSString stringWithFormat: NSLocalizedString(@"The item you selected isn't an image, sound or movie recognized by QuickTime. (%@)\n\nPlease select a different item.", "Message displayed in alert sheet when media document is not recognized by QuickTime"), [error localizedDescription]]);
383                [self _invalidateSelection];
384                return NO;
385            }
386            if (![movie NJR_hasAudio] && ![movie NJR_hasVideo]) {
387                NSBeginAlertSheet(@"No video or audio", nil, nil, nil, [self window], nil, nil, nil, nil, NSLocalizedString(@"'%@' contains neither audio nor video content playable by QuickTime.\n\nPlease select a different item.", "Message displayed in alert sheet when media document is readable, but has neither audio nor video tracks"), [[NSFileManager defaultManager] displayNameAtPath: [selectedAlias fullPath]]);
388                [self _invalidateSelection];
389                [movie release];
390                return NO;
391            }
392        }
393        if (doPreview) {
394            [self _startSoundPreview];
395        }
396        [movie release];
397    }
398    [[NSNotificationCenter defaultCenter] postNotificationName: NJRQTMediaPopUpButtonMovieChangedNotification object: self];
399    return YES;
400}
401
402#pragma mark actions
403
404- (IBAction)stopSoundPreview:(id)sender;
405{
406    [preview pause: self];
407    [self _resetPreview];
408}
409
410- (void)_beepSelected:(NSMenuItem *)sender;
411{
412    [self _setAlias: nil];
413    [self _validateWithPreview: YES];
414}
415
416- (void)_systemSoundSelected:(NSMenuItem *)sender;
417{
418    [self _setPath: [sender representedObject]];
419    if (![self _validateWithPreview: YES]) {
420        [[self menu] removeItem: sender];
421    }
422}
423
424- (void)_aliasSelected:(NSMenuItem *)sender;
425{
426    BDAlias *alias = [sender representedObject];
427    int index = [self indexOfItem: sender], otherIndex = [self indexOfItem: otherItem];
428    [self _setAlias: alias];
429    if (![self _validateWithPreview: YES]) {
430        [[self menu] removeItem: sender];
431    } else if (index > otherIndex + 1) { // move "other" item to top of list
432        int recentIndex = [recentMediaAliasData count] - index + otherIndex;
433        NSMenuItem *item = [[self itemAtIndex: index] retain];
434        NSData *data = [[recentMediaAliasData objectAtIndex: recentIndex] retain];
435        // [self _validateRecentMedia];
436        [self removeItemAtIndex: index];
437        [[self menu] insertItem: item atIndex: otherIndex + 1];
438        [self selectItem: item];
439        [item release];
440        NSAssert(recentIndex >= 0, @"Recent media index invalid");
441        // NSLog(@"_aliasSelected removing item %d - %d + %d = %d of recentMediaAliasData", [recentMediaAliasData count], index, otherIndex, recentIndex);
442        [recentMediaAliasData removeObjectAtIndex: recentIndex];
443        [recentMediaAliasData addObject: data];
444        [self _validateRecentMedia];
445        [data release];
446    } // else NSLog(@"_aliasSelected ...already at top");
447}
448
449- (IBAction)select:(id)sender;
450{
451    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
452    NSString *path = [selectedAlias fullPath];
453    [openPanel setAllowsMultipleSelection: NO];
454    [openPanel setCanChooseDirectories: NO];
455    [openPanel setCanChooseFiles: YES];
456    [openPanel setDelegate: self];
457    [openPanel beginSheetForDirectory: [path stringByDeletingLastPathComponent]
458                                 file: [path lastPathComponent]
459                                types: nil
460                       modalForWindow: [self window]
461                        modalDelegate: self
462                       didEndSelector: @selector(openPanelDidEnd:returnCode:contextInfo:)
463                          contextInfo: nil];
464}
465
466- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
467{
468    [sheet close];
469
470    if (returnCode == NSOKButton) {
471        NSArray *files = [sheet filenames];
472        NSAssert1([files count] == 1, @"%d items returned, only one expected", [files count]);
473        [self _setPath: [files objectAtIndex: 0]];
474        if ([self _validateWithPreview: YES]) {
475            [self selectItem: [self _itemForAlias: selectedAlias]];
476        }
477    } else {
478        // "Other..." item is still selected, revert to previously selected item
479        // XXX issue with cancelling, top item in recent menu is sometimes duplicated!?
480        [self selectItem: [self _itemForAlias: selectedAlias]];
481    }
482    // [self _validateRecentMedia];
483}
484
485- (void)setEnabled:(BOOL)flag;
486{
487    [super setEnabled: flag];
488    if (flag) ; // XXX [self startSoundPreview: self]; // need to prohibit at startup
489    else [self stopSoundPreview: self];
490}
491
492#pragma mark drag feedback
493
494- (void)drawRect:(NSRect)rect;
495{
496    if (dragAccepted) {
497        NSWindow *window = [self window];
498        NSRect boundsRect = [self bounds];
499        BOOL isFirstResponder = ([window firstResponder] == self);
500        // focus ring and drag feedback interfere with one another
501        if (isFirstResponder) [window makeFirstResponder: window];
502        [super drawRect: rect];
503        [[NSColor selectedControlColor] set];
504        NSFrameRectWithWidthUsingOperation(NSInsetRect(boundsRect, 2, 2), 3, NSCompositeSourceIn);
505        if (isFirstResponder) [window makeFirstResponder: self];
506    } else {
507        [super drawRect: rect];
508    }
509}
510
511@end
512
513@implementation NJRQTMediaPopUpButton (NSSavePanelDelegate)
514
515- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
516{
517    BOOL isDir = NO;
518    [[NSFileManager defaultManager] fileExistsAtPath: filename isDirectory: &isDir];
519
520    if (isDir)
521        return YES;
522
523    return [QTMovie canInitWithFile: filename];
524}
525
526@end
527
528@implementation NJRQTMediaPopUpButton (NSDraggingDestination)
529
530- (BOOL)acceptsDragFrom:(id <NSDraggingInfo>)sender;
531{
532    NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
533    NSFileManager *fm = [NSFileManager defaultManager];
534    BOOL isDir;
535
536    if (url == nil || ![url isFileURL]) return NO;
537
538    if (![fm fileExistsAtPath: [url path] isDirectory: &isDir]) return NO;
539
540    if (isDir) return NO;
541   
542    return YES;
543}
544
545- (NSString *)_descriptionForDraggingInfo:(id <NSDraggingInfo>)sender;
546{
547    NSDragOperation mask = [sender draggingSourceOperationMask];
548    NSMutableString *s = [NSMutableString stringWithFormat: @"Drag seq %d source: %@",
549        [sender draggingSequenceNumber], [sender draggingSource]];
550    NSPasteboard *draggingPasteboard = [sender draggingPasteboard];
551    NSArray *types = [draggingPasteboard types];
552    NSEnumerator *e = [types objectEnumerator];
553    NSString *type;
554    [s appendString: @"\nDrag operations:"];
555    if (mask & NSDragOperationCopy) [s appendString: @" copy"];
556    if (mask & NSDragOperationLink) [s appendString: @" link"];
557    if (mask & NSDragOperationGeneric) [s appendString: @" generic"];
558    if (mask & NSDragOperationPrivate) [s appendString: @" private"];
559    if (mask & NSDragOperationMove) [s appendString: @" move"];
560    if (mask & NSDragOperationDelete) [s appendString: @" delete"];
561    if (mask & NSDragOperationEvery) [s appendString: @" every"];
562    if (mask & NSDragOperationNone) [s appendString: @" none"];
563    [s appendFormat: @"\nImage: %@ at %@", [sender draggedImage],
564        NSStringFromPoint([sender draggedImageLocation])];
565    [s appendFormat: @"\nDestination: %@ at %@", [sender draggingDestinationWindow],
566        NSStringFromPoint([sender draggingLocation])];
567    [s appendFormat: @"\nPasteboard: %@ types:", draggingPasteboard];
568    while ( (type = [e nextObject]) != nil) {
569        if ([type hasPrefix: @"CorePasteboardFlavorType 0x"]) {
570            const char *osTypeHex = [[type substringFromIndex: [type rangeOfString: @"0x" options: NSBackwardsSearch].location] lossyCString];
571            OSType osType;
572            sscanf(osTypeHex, "%lx", &osType);
573            [s appendFormat: @" '%4s'", &osType];
574        } else {
575            [s appendFormat: @" '%@'", type];
576        }
577    }
578    return s;
579}
580
581- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
582{
583    if ([self acceptsDragFrom: sender] && [sender draggingSourceOperationMask] &
584        (NSDragOperationCopy | NSDragOperationLink)) {
585        dragAccepted = YES;
586        [self setNeedsDisplay: YES];
587        // NSLog(@"draggingEntered accept:\n%@", [self _descriptionForDraggingInfo: sender]);
588        return NSDragOperationLink;
589    }
590    return NSDragOperationNone;
591}
592
593- (void)draggingExited:(id <NSDraggingInfo>)sender;
594{
595    dragAccepted = NO;
596    [self setNeedsDisplay: YES];
597}
598
599- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender;
600{
601    dragAccepted = NO;
602    [self setNeedsDisplay: YES];
603    return [self acceptsDragFrom: sender];
604}
605
606- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender;
607{
608    if ([sender draggingSource] != self) {
609        NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
610        if (url == nil) return NO;
611        [self _setPath: [url path]];
612        if ([self _validateWithPreview: YES]) {
613            [self selectItem: [self _itemForAlias: selectedAlias]];
614        }
615    }
616    return YES;
617}
618
619@end
Note: See TracBrowser for help on using the repository browser.