source: releases/Pester/1.1a1/Source/NJRQTMediaPopUpButton.m@ 440

Last change on this file since 440 was 43, checked in by Nicholas Riley, 22 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.