source: releases/Pester/1.1a5/Source/NJRQTMediaPopUpButton.m

Last change on this file was 53, checked in by Nicholas Riley, 21 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.