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

Last change on this file since 41 was 41, checked in by Nicholas Riley, 20 years ago

Popup triangle.tiff: Needed display component of NJRFSObjectSelector.
This one appears to use transparency unlike the one I prepared for
Process Exhibits, so it should be preferred.

NSMovie-NJRExtensions: Added -isStatic to identify whether the movie
contains dynamic components. If the movie is static, it most likely
contains only an image and shouldn't be 'played' as such, otherwise
the duration will be so short that the image won't be useful.

NJRFSObjectSelector: Fixed -drawRect: to draw the drag feedback
rectangle inside the bounds of the control, not inside whatever dirty
rectangle is passed to the method (often they are the same, but not
always). Only draw the popup arrow if the control is enabled.
Properly draw the popup arrow with transparency. Display the 'make
alias' cursor as additional drag feedback.

PSMovieAlertController: Only repeat movie and auto-close after movie
finished if it contains time-based media, otherwise just display the
movie (an image) until the window is closed or alarms are cancelled.

NSImage-NJRExtensions: Include code to actually scale icons from
F-Script Anywhere, otherwise the menu ends up with 32x32 (or
potentially larger) icons if smaller variations are not provided.
There's some more code in FSA that chooses which representation to
select; this code may still not be properly handling representations,
but it works better now. A good test case is the icon for Tex-Edit
Plus documents.

NJRQTMediaPopUpButton: Added notification for movie change (needed to
update interface). Changed -_validatePreview to
-_validateWithPreview: - preview is now optional. This will be needed
when I add archiving support for the selected item; we'll need to
validate it before updating the interface, but we don't want sounds to
play. Added some #pragma mark lines to separate methods by
functionality. Call -validateWithPreview: NO in awakeFromNib (again,
this logic will become more sophisticated later). Commented some
debugging logic since I'm pretty happy with the code. Added
-canRepeat accessor and setters, notification support in the
validation method. Added drag feedback.

PSAlarmSetController: Notification support, -setSoundRepetitionCount:
to avoid flashing with repetition text field control as NSStepper is
adjusted

English.lproj/MainMenu.nib: Switched action method for the NSStepper
stuff discussed above.

AM /Users/nicholas/Documents/Development/Cocoa/Pester/Source/Popup triangle.tiff
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/NSMovie-NJRExtensions.m
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/NJRFSObjectSelector.m
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/PSMovieAlertController.m
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/NSImage-NJRExtensions.m
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/NJRQTMediaPopUpButton.h
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/NJRQTMediaPopUpButton.m
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/.DS_Store
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/English.lproj/MainMenu.nib/objects.nib
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/English.lproj/MainMenu.nib/info.nib
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/English.lproj/MainMenu.nib/classes.nib
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/Pester.pbproj/nicholas.pbxuser
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/Pester.pbproj/project.pbxproj
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/PSAlarmSetController.h
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/PSAlarmSetController.m
M /Users/nicholas/Documents/Development/Cocoa/Pester/Source/NSMovie-NJRExtensions.h

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