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

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