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

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

Updated for Pester 1.1a5 (very limited release).

Pester 1.1a4 was never released.

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