source: releases/Pester/1.1a2/Source/NJRQTMediaPopUpButton.m@ 269

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

Pester 1.1a2.

English.lproj/Credits.html: Fixed some HTML formatting issues, added Ben Hines to credits (thanks for helping out with 1.1a1 testing!)

English.lproj/InfoPlist.strings: Updated for 1.1a2.

English.lproj/MainMenu.nib: Reconnected initialFirstResponder outlet on the window; somehow it became disconnected. Fixed keyboard navigation loop. Removed formatters from date/time fields which were causing crashes on launch on 10.2 (they're instantiated from code in any case). Removed text from date field because it didn't work without the formatter.

NJRDateFormatter: Workaround for 10.2 NSScanner bug [Ben Hines].

NJRQTMediaPopUpButton: Remove corrupt JPEG note, can no longer reproduce. Removed -validateRecentMedia invocation, debug code shouldn't have been left in.

PSAlarmSetController: Set alerts before setting alarm, otherwise alarm in bogus state remains. Set date to today in awakeFromNib, moved from the nib. Disconnect initial first responder to work around 10.1 bug so keyboard focus is set properly when the window opens.

Pester.pbproj: Added VERSION.

Read Me.rtfd: Updated for 1.1a2.

VERSION: Updated for 1.1a2.

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