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

Last change on this file since 40 was 40, checked in by Nicholas Riley, 18 years ago

PSDockBounceAlert: Make it work, assuming the app doesn't get brought
forward immediately upon the alarm activation.

NJRQTMediaPopUpButton: Fixed drag and drop. Added _descriptionForDraggingInfo:
to

PSMovieAlertController: Fixed movie vertical size, looks like a bug
got enshrined in the documentation.

PSAlarmAlertController: Don't force app to front until alerts are ready.

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