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

Last change on this file since 103 was 103, checked in by Nicholas Riley, 19 years ago

Localization, bug fixes

File size: 19.7 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, 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"));
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: NSLocalizedString(@"Can't locate alert sounds", "QuickTime media popup menu item surrogate for alert sound list if no sounds are found") 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: NSLocalizedString(@"Other...", "Media popup item to select another sound/movie/image") 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, 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"));
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, 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]]);
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.