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

Last change on this file since 543 was 541, checked in by Nicholas Riley, 15 years ago

Finish migrating NJRQTMediaPopUpButton to QTKit. The alert still needs to be migrated.

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