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

Last change on this file since 133 was 133, checked in by Nicholas Riley, 17 years ago

VERSION: Updated for 1.1b4.

PSMovieAlertController.[hm]: Set output volume from alert; save and
restore output volume (bug 27).

PSBeepAlert.[hm]: Save and set volume, disabled pending a
synchronous equivalent to NSBeep().

NJRQTMediaPopUpButton.[hm]: Save movieHasAudio (does not include
beeps, pending the above fix, since they don't permit volume
adjustment). savedVolume indicates whether the volume has been saved
but not restored yet; outputVolume stores the set volume (eventually
incorporated into a PSMediaAlert).

PSMediaAlert.[hm]: Stores repetitions and volume information for audio
alerts. New superclass of PSBeepAlert and PSMovieAlert.

PSPreferencesController.m: Fixed bug where hot keys still appeared
even after they couldn't be set - last of bug 29. Add Command-comma
to the list of disallowed equivalents, as it's reserved for
Preferences now (still a bug - it'll show the entire set key
equivalent at the left side of the window when you press
command-comma; ah well.)

Volume [0123].png: Volume-indicating small icons from QuickTime? 6.

NJRSoundManager.[hm]: Interface to volume saving, restoring, setting -
necessary because the QuickTime? call to SetDefaultOutputVolume? sets
the right channel volume only (bug 27).

PSVolumeController.h: Controller for volume popup window (bug 27).
Not your average NSWindowController, but it works. Keyboard control
of volume is still necessary; filed as bug 31.

PSAlarmSetController.[hm]: Added references to sound volume button and
showVolume: action. Added volume setting support (bug 27); mostly
similar interface to calendar, though we need direct calls to
NJRSoundManager to restore sound volume at times. Only enable sound
volume button if selected media file has audio component (and isn't the
system alert sound, which I discussed above). Take advantage of
PSMediaAlert to factor some code in _readAlerts:. Save and restore
volume as part of alerts.

Read Me.rtfd: Updated release notes; fixed some bizarre text
formatting problems; search/replace "* " bullet-space with "*\t"
bullet-tab to improve alignment in release notes.

PSMovieAlert.[hm]: Factored code into PSMediaAlert. Describe output
volume as percentage of maximum.

NJRHotKey.m: Fixed some odd spacing left over from Ecky's code.

PSApplication.m: Restore saved output volume on quit.

English.lproj/MainMenu.nib: Added volume button.

English.lproj/Volume.nib: Volume nib (bug 27).

PSCalendarController.m: Removed casts from a copy/paste error. Fixed
variable names in some code inherited from my TextExtras? incremental
search modifications - it's not always a text field now.

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