source: releases/Pester/1.1a3/Source/NJRQTMediaPopUpButton.m@ 241

Last change on this file since 241 was 47, checked in by Nicholas Riley, 22 years ago

Pester 1.1a2 (again).

NJRQTMediaPopUpButton: Don't add in -_addRecentMediaAtPath if title or path is nil.

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