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

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