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.