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

Last change on this file since 47 was 47, checked in by Nicholas Riley, 18 years ago

Pester 1.1a2 (again).

NJRQTMediaPopUpButton: Don't add in -_addRecentMediaAtPath if title or path is nil.

File size: 17.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 "SoundFileManager.h"
11#import "NSMovie-NJRExtensions.h"
12#import "NSImage-NJRExtensions.h"
13
14static const int NJRQTMediaPopUpButtonMaxRecentItems = 10;
15
16NSString * const NJRQTMediaPopUpButtonMovieChangedNotification = @"NJRQTMediaPopUpButtonMovieChangedNotification";
17
18@interface NJRQTMediaPopUpButton (Private)
19- (void)_setPath:(NSString *)path;
20- (NSMenuItem *)_itemForAlias:(BDAlias *)alias;
21- (BOOL)_validateWithPreview:(BOOL)doPreview;
22@end
23
24@implementation NJRQTMediaPopUpButton
25
26// XXX handle refreshing sound list on resume
27// XXX don't add icons on Puma, they look like ass
28// XXX launch preview on a separate thread (if movies take too long to load, they inhibit the interface responsiveness)
29
30// Recent media layout:
31// Most recent media are at TOP of menu (smaller item numbers, starting at [self indexOfItem: otherItem] + 1)
32// Most recent media are at END of array (larger indices)
33
34#pragma mark recently selected media tracking
35
36- (NSString *)_defaultKey;
37{
38    NSAssert([self tag] != 0, @"CanÕt track recently selected media for popup with tag 0: please set a tag");
39    return [NSString stringWithFormat: @"NJRQTMediaPopUpButtonMaxRecentItems tag %d", [self tag]];
40}
41
42- (void)_writeRecentMedia;
43{
44    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
45    [defaults setObject: recentMediaAliasData forKey: [self _defaultKey]];
46    [defaults synchronize];
47}
48
49- (NSMenuItem *)_addRecentMediaAtPath:(NSString *)path withAlias:(BDAlias *)alias;
50{
51    NSString *title = [[NSFileManager defaultManager] displayNameAtPath: path];
52    NSMenu *menu = [self menu];
53    NSMenuItem *item;
54    if (title == nil || path == nil) return nil;
55    item = [menu insertItemWithTitle: title action: @selector(_aliasSelected:) keyEquivalent: @"" atIndex: [menu indexOfItem: otherItem] + 1];
56    [item setTarget: self];
57    [item setRepresentedObject: alias];
58    [item setImage: [[[NSWorkspace sharedWorkspace] iconForFile: path] bestFitImageForSize: NSMakeSize(16, 16)]];
59    [recentMediaAliasData addObject: [alias aliasData]];
60    if ([recentMediaAliasData count] > NJRQTMediaPopUpButtonMaxRecentItems) {
61        [menu removeItemAtIndex: [menu numberOfItems] - 1];
62        [recentMediaAliasData removeObjectAtIndex: 0];
63    }
64    return item;
65}
66
67- (void)_addRecentMediaFromAliasesData:(NSArray *)aliasesData;
68{
69    NSEnumerator *e = [aliasesData objectEnumerator];
70    NSData *aliasData;
71    BDAlias *alias;
72    while ( (aliasData = [e nextObject]) != nil) {
73        if ( (alias = [[BDAlias alloc] initWithData: aliasData]) != nil) {
74            [self _addRecentMediaAtPath: [alias fullPath] withAlias: alias];
75            [alias release];
76        }
77    }
78}
79
80- (void)_validateRecentMedia;
81{
82    NSEnumerator *e = [recentMediaAliasData reverseObjectEnumerator];
83    NSData *aliasData;
84    NSMenuItem *item;
85    BDAlias *itemAlias;
86    int otherIndex = [self indexOfItem: otherItem];
87    int aliasDataCount = [recentMediaAliasData count];
88    int lastItemIndex = [self numberOfItems] - 1;
89    int recentItemCount = lastItemIndex - otherIndex;
90    int recentItemIndex = otherIndex;
91    NSAssert2(recentItemCount == aliasDataCount, @"Counted %d recent menu items, %d of alias data", recentItemCount, aliasDataCount);
92    while ( (aliasData = [e nextObject]) != nil) { // go BACKWARD through array while going DOWN menu
93        recentItemIndex++;
94        item = [self itemAtIndex: recentItemIndex];
95        itemAlias = [item representedObject];
96        if ([itemAlias aliasDataIsEqual: aliasData])
97            NSLog(@"item %d %@: %@", recentItemIndex, [item title], [itemAlias fullPath]);
98        else
99            NSLog(@"ITEM %d %@: %@ != aliasData %@", recentItemIndex, [item title], [itemAlias fullPath], [[BDAlias aliasWithData: aliasData] fullPath]);
100    }
101}
102
103#pragma mark initialize-release
104
105- (void)awakeFromNib;
106{
107    NSMenu *menu;
108    NSMenuItem *item;
109    SoundFileManager *sfm = [SoundFileManager sharedSoundFileManager];
110    int soundCount = [sfm count];
111
112    [self removeAllItems];
113    menu = [self menu];
114    item = [menu addItemWithTitle: @"Alert sound" action: @selector(_beepSelected:) keyEquivalent: @""];
115    [item setTarget: self];
116    [menu addItem: [NSMenuItem separatorItem]];
117    if (soundCount == 0) {
118        item = [menu addItemWithTitle: @"CanÕt locate alert sounds" action: nil keyEquivalent: @""];
119        [item setEnabled: NO];
120    } else {
121        SoundFile *sf;
122        int i;
123        [sfm sortByName];
124        for (i = 0 ; i < soundCount ; i++) {
125            sf = [sfm soundFileAtIndex: i];
126            item = [menu addItemWithTitle: [sf name] action: @selector(_soundFileSelected:) keyEquivalent: @""];
127            [item setTarget: self];
128            [item setRepresentedObject: sf];
129            [item setImage: [[[NSWorkspace sharedWorkspace] iconForFile: [sf path]] bestFitImageForSize: NSMakeSize(16, 16)]];
130        }
131    }
132    [menu addItem: [NSMenuItem separatorItem]];
133    item = [menu addItemWithTitle: @"OtherÉ" action: @selector(select:) keyEquivalent: @""];
134    [item setTarget: self];
135    otherItem = [item retain];
136
137    [self _validateWithPreview: NO];
138
139    recentMediaAliasData = [[NSMutableArray alloc] initWithCapacity: NJRQTMediaPopUpButtonMaxRecentItems + 1];
140    [self _addRecentMediaFromAliasesData: [[NSUserDefaults standardUserDefaults] arrayForKey: [self _defaultKey]]];
141    // [self _validateRecentMedia];
142
143    [self registerForDraggedTypes:
144        [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]];
145}
146
147- (void)dealloc;
148{
149    [recentMediaAliasData release]; recentMediaAliasData = nil;
150    [otherItem release];
151    [selectedAlias release]; [previousAlias release];
152    [super dealloc];
153}
154
155#pragma mark accessing
156
157- (BDAlias *)selectedAlias;
158{
159    return selectedAlias;
160}
161
162- (void)_setAlias:(BDAlias *)alias;
163{
164    BDAlias *oldAlias = [selectedAlias retain];
165    [previousAlias release];
166    previousAlias = oldAlias;
167    if (selectedAlias != alias) {
168        [selectedAlias release];
169        selectedAlias = [alias retain];
170    }
171}
172
173- (void)_setPath:(NSString *)path;
174{
175    [self _setAlias: [BDAlias aliasWithPath: path]];
176}
177
178- (NSMenuItem *)_itemForAlias:(BDAlias *)alias;
179{
180    NSString *path;
181    SoundFile *sf;
182    if (alias == nil) {
183        return [self itemAtIndex: 0];
184    }
185
186    // [self _validateRecentMedia];
187    path = [alias fullPath];
188    sf = [[SoundFileManager sharedSoundFileManager] soundFileFromPath: path];
189    // NSLog(@"_itemForAlias: %@", path);
190
191    // selected a system sound?
192    if (sf != nil) {
193        // NSLog(@"_itemForAlias: selected system sound");
194        return [self itemAtIndex: [self indexOfItemWithRepresentedObject: sf]];
195    } else {
196        NSEnumerator *e = [recentMediaAliasData reverseObjectEnumerator];
197        NSData *aliasData;
198        NSMenuItem *item;
199        int recentIndex = 1;
200
201        while ( (aliasData = [e nextObject]) != nil) {
202            // selected a recently selected, non-system sound?
203            if ([alias aliasDataIsEqual: aliasData]) {
204                int otherIndex = [self indexOfItem: otherItem];
205                int menuIndex = recentIndex + otherIndex;
206                if (menuIndex == otherIndex + 1) return [self itemAtIndex: menuIndex]; // already at top
207                // remove item, add (at top) later
208                // NSLog(@"_itemForAlias removing item: idx %d + otherItemIdx %d + 1 = %d [%@]", recentIndex, otherIndex, menuIndex, [self itemAtIndex: menuIndex]);
209                [self removeItemAtIndex: menuIndex];
210                [recentMediaAliasData removeObjectAtIndex: [recentMediaAliasData count] - recentIndex];
211                break;
212            }
213            recentIndex++;
214        }
215
216        // create the item
217        item = [self _addRecentMediaAtPath: path withAlias: alias];
218        [self _writeRecentMedia];
219        return item;
220    }
221}
222
223- (BOOL)canRepeat;
224{
225    return movieCanRepeat;
226}
227
228#pragma mark selected media validation
229
230- (void)_invalidateSelection;
231{
232    [self _setAlias: previousAlias];
233    [self selectItem: [self _itemForAlias: [self selectedAlias]]];
234    [[NSNotificationCenter defaultCenter] postNotificationName: NJRQTMediaPopUpButtonMovieChangedNotification object: self];
235}
236
237- (BOOL)_validateWithPreview:(BOOL)doPreview;
238{
239    [preview stop: self];
240    if (selectedAlias == nil) {
241        [preview setMovie: nil];
242        movieCanRepeat = YES;
243        if (doPreview) NSBeep();
244    } else {
245        NSMovie *movie = [[NSMovie alloc] initWithURL: [NSURL fileURLWithPath: [selectedAlias fullPath]] byReference: YES];
246        movieCanRepeat = ![movie isStatic];
247        if ([movie hasAudio])
248            [preview setMovie: movie];
249        else {
250            [preview setMovie: nil];
251            if (movie == nil) {
252                NSBeginAlertSheet(@"Format not recognized", @"OK", nil, nil, [self window], nil, nil, nil, nil, @"The item you selected isnÕt a sound or movie recognized by QuickTime.  Please select a different item.");
253                [self _invalidateSelection];
254                return NO;
255            }
256            if (![movie hasAudio] && ![movie hasVideo]) {
257                NSBeginAlertSheet(@"No video or audio", @"OK", nil, nil, [self window], nil, nil, nil, nil, @"Ò%@Ó contains neither audio nor video content playable by QuickTime.  Please select a different item.", [[NSFileManager defaultManager] displayNameAtPath: [selectedAlias fullPath]]);
258                [self _invalidateSelection];
259                [movie release];
260                return NO;
261            }
262        }
263        [movie release];
264        [preview start: self];
265    }
266    [[NSNotificationCenter defaultCenter] postNotificationName: NJRQTMediaPopUpButtonMovieChangedNotification object: self];
267    return YES;
268}
269
270#pragma mark actions
271
272- (IBAction)stopSoundPreview:(id)sender;
273{
274    [preview stop: self];
275}
276
277- (void)_beepSelected:(NSMenuItem *)sender;
278{
279    [self _setAlias: nil];
280    [self _validateWithPreview: YES];
281}
282
283- (void)_soundFileSelected:(NSMenuItem *)sender;
284{
285    [self _setPath: [(SoundFile *)[sender representedObject] path]];
286    if (![self _validateWithPreview: YES]) {
287        [[self menu] removeItem: sender];
288    }
289}
290
291- (void)_aliasSelected:(NSMenuItem *)sender;
292{
293    BDAlias *alias = [sender representedObject];
294    int index = [self indexOfItem: sender], otherIndex = [self indexOfItem: otherItem];
295    [self _setAlias: alias];
296    if (![self _validateWithPreview: YES]) {
297        [[self menu] removeItem: sender];
298    } else if (index > otherIndex + 1) { // move "other" item to top of list
299        int recentIndex = [recentMediaAliasData count] - index + otherIndex;
300        NSMenuItem *item = [[self itemAtIndex: index] retain];
301        NSData *data = [[recentMediaAliasData objectAtIndex: recentIndex] retain];
302        // [self _validateRecentMedia];
303        [self removeItemAtIndex: index];
304        [[self menu] insertItem: item atIndex: otherIndex + 1];
305        [self selectItem: item];
306        [item release];
307        NSAssert(recentIndex >= 0, @"Recent media index invalid");
308        // NSLog(@"_aliasSelected removing item %d - %d + %d = %d of recentMediaAliasData", [recentMediaAliasData count], index, otherIndex, recentIndex);
309        [recentMediaAliasData removeObjectAtIndex: recentIndex];
310        [recentMediaAliasData addObject: data];
311        [self _validateRecentMedia];
312        [data release];
313    } // else NSLog(@"_aliasSelected ...already at top");
314}
315
316- (IBAction)select:(id)sender;
317{
318    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
319    NSString *path = [selectedAlias fullPath];
320    [openPanel setAllowsMultipleSelection: NO];
321    [openPanel setCanChooseDirectories: NO];
322    [openPanel setCanChooseFiles: YES];
323    [openPanel beginSheetForDirectory: [path stringByDeletingLastPathComponent]
324                                 file: [path lastPathComponent]
325                                types: nil // XXX fix for QuickTime!
326                       modalForWindow: [self window]
327                        modalDelegate: self
328                       didEndSelector: @selector(openPanelDidEnd:returnCode:contextInfo:)
329                          contextInfo: nil];
330}
331
332- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
333{
334    [sheet close];
335
336    if (returnCode == NSOKButton) {
337        NSArray *files = [sheet filenames];
338        NSAssert1([files count] == 1, @"%d items returned, only one expected", [files count]);
339        [self _setPath: [files objectAtIndex: 0]];
340        if ([self _validateWithPreview: YES]) {
341            [self selectItem: [self _itemForAlias: selectedAlias]];
342        }
343    } else {
344        // "Other..." item is still selected, revert to previously selected item
345        // XXX issue with cancelling, top item in recent menu is sometimes duplicated!?
346        [self selectItem: [self _itemForAlias: selectedAlias]];
347    }
348    // [self _validateRecentMedia];
349}
350
351- (void)setEnabled:(BOOL)flag;
352{
353    [super setEnabled: flag];
354    if (flag) ; // XXX [self startSoundPreview: self]; // need to prohibit at startup
355    else [self stopSoundPreview: self];
356}
357
358#pragma mark drag feedback
359
360- (void)drawRect:(NSRect)rect;
361{
362    if (dragAccepted) {
363        NSWindow *window = [self window];
364        NSRect boundsRect = [self bounds];
365        BOOL isFirstResponder = ([window firstResponder] == self);
366        // focus ring and drag feedback interfere with one another
367        if (isFirstResponder) [window makeFirstResponder: window];
368        [super drawRect: rect];
369        [[NSColor selectedControlColor] set];
370        NSFrameRectWithWidthUsingOperation(NSInsetRect(boundsRect, 2, 2), 3, NSCompositeSourceIn);
371        if (isFirstResponder) [window makeFirstResponder: self];
372    } else {
373        [super drawRect: rect];
374    }
375}
376
377@end
378
379@implementation NJRQTMediaPopUpButton (NSDraggingDestination)
380
381- (BOOL)acceptsDragFrom:(id <NSDraggingInfo>)sender;
382{
383    NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
384    NSFileManager *fm = [NSFileManager defaultManager];
385    BOOL isDir;
386
387    if (url == nil || ![url isFileURL]) return NO;
388
389    if (![fm fileExistsAtPath: [url path] isDirectory: &isDir]) return NO;
390
391    if (isDir) return NO;
392   
393    return YES;
394}
395
396- (NSString *)_descriptionForDraggingInfo:(id <NSDraggingInfo>)sender;
397{
398    NSDragOperation mask = [sender draggingSourceOperationMask];
399    NSMutableString *s = [NSMutableString stringWithFormat: @"Drag seq %d source: %@",
400        [sender draggingSequenceNumber], [sender draggingSource]];
401    NSPasteboard *draggingPasteboard = [sender draggingPasteboard];
402    NSArray *types = [draggingPasteboard types];
403    NSEnumerator *e = [types objectEnumerator];
404    NSString *type;
405    [s appendString: @"\nDrag operations:"];
406    if (mask & NSDragOperationCopy) [s appendString: @" copy"];
407    if (mask & NSDragOperationLink) [s appendString: @" link"];
408    if (mask & NSDragOperationGeneric) [s appendString: @" generic"];
409    if (mask & NSDragOperationPrivate) [s appendString: @" private"];
410    if (mask & NSDragOperationMove) [s appendString: @" move"];
411    if (mask & NSDragOperationDelete) [s appendString: @" delete"];
412    if (mask & NSDragOperationEvery) [s appendString: @" every"];
413    if (mask & NSDragOperationNone) [s appendString: @" none"];
414    [s appendFormat: @"\nImage: %@ at %@", [sender draggedImage],
415        NSStringFromPoint([sender draggedImageLocation])];
416    [s appendFormat: @"\nDestination: %@ at %@", [sender draggingDestinationWindow],
417        NSStringFromPoint([sender draggingLocation])];
418    [s appendFormat: @"\nPasteboard: %@ types:", draggingPasteboard];
419    while ( (type = [e nextObject]) != nil) {
420        if ([type hasPrefix: @"CorePasteboardFlavorType 0x"]) {
421            const char *osTypeHex = [[type substringFromIndex: [type rangeOfString: @"0x" options: NSBackwardsSearch].location] lossyCString];
422            OSType osType;
423            sscanf(osTypeHex, "%lx", &osType);
424            [s appendFormat: @" '%4s'", &osType];
425        } else {
426            [s appendFormat: @" \"%@\"", type];
427        }
428    }
429    return s;
430}
431
432- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
433{
434    if ([self acceptsDragFrom: sender] && [sender draggingSourceOperationMask] &
435        (NSDragOperationCopy | NSDragOperationLink)) {
436        dragAccepted = YES;
437        [self setNeedsDisplay: YES];
438        // NSLog(@"draggingEntered accept:\n%@", [self _descriptionForDraggingInfo: sender]);
439        return NSDragOperationLink;
440    }
441    return NSDragOperationNone;
442}
443
444- (void)draggingExited:(id <NSDraggingInfo>)sender;
445{
446    dragAccepted = NO;
447    [self setNeedsDisplay: YES];
448}
449
450- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender;
451{
452    dragAccepted = NO;
453    [self setNeedsDisplay: YES];
454    return [self acceptsDragFrom: sender];
455}
456
457- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender;
458{
459    if ([sender draggingSource] != self) {
460        NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
461        if (url == nil) return NO;
462        [self _setPath: [url path]];
463        if ([self _validateWithPreview: YES]) {
464            [self selectItem: [self _itemForAlias: selectedAlias]];
465        }
466    }
467    return YES;
468}
469
470@end
Note: See TracBrowser for help on using the repository browser.