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

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

Pester 1.1a1.

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

English.lproj/MainMenu.nib: Placeholder for day names in popup menu, fixed up by code (this means you can still edit it from IB though). Added command-shift-T to both in/at cells (required, code removes one or the other as appropriate). Fixed up sizes of fields. Default to today (this will need fixing when we localized the word "today", but it's fine for now...).

English.lproj/Notifier.nib: Remove date formatter because we set a string directly now instead (could set formatter from code, but we don't).

NJRDateFormatter: many workarounds for Cocoa bugs: missing AM/PM, incorrect results with space before AM/PM, etc. Added class methods to do format manipulation and return localized formats which work for output (though not always for input; this class has an internal workaround for the AM/PM problem).

NJRFSObjectSelector: properly handle enabled attribute, store internally and report externally as appropriate. Previously, the button would become enabled if you dropped something on it even if it was supposed to be disabled.

NJRQTMediaPopUpButton: stop sound preview when button disabled.

NJRVoicePopUpButton: stop voice preview when button disabled.

PSAlarm: new method -dateString returns long date string. Maintain local copy of long date, short date and time formats, and locale, using NJRDateFormatter.

PSAlarmNotifierController: update to use -[PSAlarm dateString], -[PSAlarm timeString] for localization instead of using broken formatter.

PSAlarmSetController: update documentation for some more Cocoa bugs I need to file. Set time of day and date formatters with localized date formats from NJRDateFormatter (retain/release issue here?) Localize weekday popup for predefined dates. Localize static date display with NJRDateFormatter. Note a solution (thanks to Douglas Davidson) for figuring out which control is editing. Added command-shift-T key equivalent to toggle in/at. Properly work around bugs witih soundRepetitionCount flashing, except where it's impossible to do anything else.

Read Me.rtfd: Updated for 1.1a1.

VERSION: Updated for 1.1a1.

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// 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- (void)setEnabled:(BOOL)flag;
351{
352    [super setEnabled: flag];
353    if (flag) ; // XXX [self startSoundPreview: self]; // need to prohibit at startup
354    else [self stopSoundPreview: self];
355}
356
357#pragma mark drag feedback
358
359- (void)drawRect:(NSRect)rect;
360{
361    if (dragAccepted) {
362        NSWindow *window = [self window];
363        NSRect boundsRect = [self bounds];
364        BOOL isFirstResponder = ([window firstResponder] == self);
365        // focus ring and drag feedback interfere with one another
366        if (isFirstResponder) [window makeFirstResponder: window];
367        [super drawRect: rect];
368        [[NSColor selectedControlColor] set];
369        NSFrameRectWithWidthUsingOperation(NSInsetRect(boundsRect, 2, 2), 3, NSCompositeSourceIn);
370        if (isFirstResponder) [window makeFirstResponder: self];
371    } else {
372        [super drawRect: rect];
373    }
374}
375
376@end
377
378@implementation NJRQTMediaPopUpButton (NSDraggingDestination)
379
380- (BOOL)acceptsDragFrom:(id <NSDraggingInfo>)sender;
381{
382    NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
383    NSFileManager *fm = [NSFileManager defaultManager];
384    BOOL isDir;
385
386    if (url == nil || ![url isFileURL]) return NO;
387
388    if (![fm fileExistsAtPath: [url path] isDirectory: &isDir]) return NO;
389
390    if (isDir) return NO;
391   
392    return YES;
393}
394
395- (NSString *)_descriptionForDraggingInfo:(id <NSDraggingInfo>)sender;
396{
397    NSDragOperation mask = [sender draggingSourceOperationMask];
398    NSMutableString *s = [NSMutableString stringWithFormat: @"Drag seq %d source: %@",
399        [sender draggingSequenceNumber], [sender draggingSource]];
400    NSPasteboard *draggingPasteboard = [sender draggingPasteboard];
401    NSArray *types = [draggingPasteboard types];
402    NSEnumerator *e = [types objectEnumerator];
403    NSString *type;
404    [s appendString: @"\nDrag operations:"];
405    if (mask & NSDragOperationCopy) [s appendString: @" copy"];
406    if (mask & NSDragOperationLink) [s appendString: @" link"];
407    if (mask & NSDragOperationGeneric) [s appendString: @" generic"];
408    if (mask & NSDragOperationPrivate) [s appendString: @" private"];
409    if (mask & NSDragOperationMove) [s appendString: @" move"];
410    if (mask & NSDragOperationDelete) [s appendString: @" delete"];
411    if (mask & NSDragOperationEvery) [s appendString: @" every"];
412    if (mask & NSDragOperationNone) [s appendString: @" none"];
413    [s appendFormat: @"\nImage: %@ at %@", [sender draggedImage],
414        NSStringFromPoint([sender draggedImageLocation])];
415    [s appendFormat: @"\nDestination: %@ at %@", [sender draggingDestinationWindow],
416        NSStringFromPoint([sender draggingLocation])];
417    [s appendFormat: @"\nPasteboard: %@ types:", draggingPasteboard];
418    while ( (type = [e nextObject]) != nil) {
419        if ([type hasPrefix: @"CorePasteboardFlavorType 0x"]) {
420            const char *osTypeHex = [[type substringFromIndex: [type rangeOfString: @"0x" options: NSBackwardsSearch].location] lossyCString];
421            OSType osType;
422            sscanf(osTypeHex, "%lx", &osType);
423            [s appendFormat: @" '%4s'", &osType];
424        } else {
425            [s appendFormat: @" \"%@\"", type];
426        }
427    }
428    return s;
429}
430
431- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
432{
433    if ([self acceptsDragFrom: sender] && [sender draggingSourceOperationMask] &
434        (NSDragOperationCopy | NSDragOperationLink)) {
435        dragAccepted = YES;
436        [self setNeedsDisplay: YES];
437        // NSLog(@"draggingEntered accept:\n%@", [self _descriptionForDraggingInfo: sender]);
438        return NSDragOperationLink;
439    }
440    return NSDragOperationNone;
441}
442
443- (void)draggingExited:(id <NSDraggingInfo>)sender;
444{
445    dragAccepted = NO;
446    [self setNeedsDisplay: YES];
447}
448
449- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender;
450{
451    dragAccepted = NO;
452    [self setNeedsDisplay: YES];
453    return [self acceptsDragFrom: sender];
454}
455
456- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender;
457{
458    if ([sender draggingSource] != self) {
459        NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
460        if (url == nil) return NO;
461        [self _setPath: [url path]];
462        if ([self _validateWithPreview: YES]) {
463            [self selectItem: [self _itemForAlias: selectedAlias]];
464        }
465    }
466    return YES;
467}
468
469@end
Note: See TracBrowser for help on using the repository browser.