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

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

PSScriptAlert.m: Removed reference to NDAppleScriptObject.

PSMovieAlertController.[hm]: Added minimum sizing.

NJRCenteringMovieView.[hm]: Support for centering movie inside its
frame if the frame is too large for the movie; sizes movie
proportionally if necessary (not used in Pester).

NSImage-NJRExtensions.[hm]: Fixed copyright.

Read Me.rtfd: Updated with bug fixes.

PSMovieAlert.m: Note bug in NSMovieView, currently doesn't affect us.

NJRQTMediaPopUpButton.m: Work around background processor use bug when
NSMovieView has movie set but not playing.

NJRVoicePopUpButton.m: Removed obsolete comment.

PSApplication.m: Only show alarm set window on rapp if alerts aren't
expiring.

PSAlarms.[hm]: Added -alarmsExpiring for support of conditional
rapp window open feature in PSApplication.

PSAlarmSetController.m: Stop window update timer and movie playback on
hide, restart timer on activate - fixes background processor usage.

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