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

Last change on this file since 560 was 560, checked in by Nicholas Riley, 10 years ago

NJRQTMediaPopUpButton.m: Remove no-longer-needed logging code for troubleshooting alias uniqueing.

File size: 20.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 "NJRSoundManager.h"
11#import "QTMovie-NJRExtensions.h"
12#import "NSMenuItem-NJRExtensions.h"
13
14#include <limits.h>
15
16static const int NJRQTMediaPopUpButtonMaxRecentItems = 10;
17
18NSString * const NJRQTMediaPopUpButtonMovieChangedNotification = @"NJRQTMediaPopUpButtonMovieChangedNotification";
19
20@interface NJRQTMediaPopUpButton (Private)
21- (void)_setPath:(NSString *)path;
22- (NSMenuItem *)_itemForAlias:(BDAlias *)alias;
23- (BOOL)_validateWithPreview:(BOOL)doPreview;
24- (void)_startSoundPreview;
25- (void)_resetPreview;
26- (void)_resetOutputVolume;
27@end
28
29@implementation NJRQTMediaPopUpButton
30
31// XXX handle refreshing sound list on resume
32// XXX don't add icons on Puma, they look like ass
33// XXX launch preview on a separate thread (if movies take too long to load, they inhibit the interface responsiveness)
34
35// Recent media layout:
36// Most recent media are at TOP of menu (smaller item numbers, starting at [self indexOfItem: otherItem] + 1)
37// Most recent media are at END of array (larger indices)
38
39#pragma mark recently selected media tracking
40
41- (NSString *)_defaultKey;
42{
43    NSAssert([self tag] != 0, NSLocalizedString(@"Can't track recently selected media for popup with tag 0: please set a tag", "Assertion for QuickTime media popup button if tag is 0"));
44    return [NSString stringWithFormat: @"NJRQTMediaPopUpButtonMaxRecentItems tag %d", [self tag]];
45}
46
47- (void)_writeRecentMedia;
48{
49    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
50    [defaults setObject: recentMediaAliasData forKey: [self _defaultKey]];
51    [defaults synchronize];
52}
53
54- (NSMenuItem *)_addRecentMediaAtPath:(NSString *)path withAlias:(BDAlias *)alias;
55{
56    NSString *title = [[NSFileManager defaultManager] displayNameAtPath: path];
57    NSMenu *menu = [self menu];
58    NSMenuItem *item;
59    if (title == nil || path == nil) return nil;
60    item = [menu insertItemWithTitle: title action: @selector(_aliasSelected:) keyEquivalent: @"" atIndex: [menu indexOfItem: otherItem] + 1];
61    [item setTarget: self];
62    [item setRepresentedObject: alias];
63    [item setImageFromPath: path];
64    [recentMediaAliasData addObject: [alias aliasData]];
65    if ([recentMediaAliasData count] > NJRQTMediaPopUpButtonMaxRecentItems) {
66        [menu removeItemAtIndex: [menu numberOfItems] - 1];
67        [recentMediaAliasData removeObjectAtIndex: 0];
68    }
69    return item;
70}
71
72- (void)_addRecentMediaFromAliasesData:(NSArray *)aliasesData;
73{
74    NSEnumerator *e = [aliasesData objectEnumerator];
75    NSData *aliasData;
76    BDAlias *alias;
77    while ( (aliasData = [e nextObject]) != nil) {
78        if ( (alias = [[BDAlias alloc] initWithData: aliasData]) != nil) {
79            [self _addRecentMediaAtPath: [alias fullPath] withAlias: alias];
80            [alias release];
81        }
82    }
83}
84
85- (void)_validateRecentMedia;
86{
87    NSEnumerator *e = [recentMediaAliasData reverseObjectEnumerator];
88    NSData *aliasData;
89    NSMenuItem *item;
90    BDAlias *itemAlias;
91    int otherIndex = [self indexOfItem: otherItem];
92    int aliasDataCount = [recentMediaAliasData count];
93    int lastItemIndex = [self numberOfItems] - 1;
94    int recentItemCount = lastItemIndex - otherIndex;
95    int recentItemIndex = otherIndex;
96    NSAssert2(recentItemCount == aliasDataCount, @"Counted %d recent menu items, %d of alias data", recentItemCount, aliasDataCount);
97    while ( (aliasData = [e nextObject]) != nil) { // go BACKWARD through array while going DOWN menu
98        recentItemIndex++;
99        item = [self itemAtIndex: recentItemIndex];
100        itemAlias = [item representedObject];
101    }
102}
103
104#pragma mark initialize-release
105
106- (void)_setUp;
107{
108    NSMenu *menu = [self menu];
109    [self removeAllItems];
110    [menu setAutoenablesItems: NO];
111
112    NSMenuItem *item = [menu addItemWithTitle: @"Alert sound" action: @selector(_beepSelected:) keyEquivalent: @""];
113    [item setTarget: self];
114    [menu addItem: [NSMenuItem separatorItem]];
115
116    NSMutableArray *soundFolderPaths = [[NSMutableArray alloc] initWithCapacity: kLastDomainConstant - kSystemDomain + 1];
117    for (FSVolumeRefNum domain = kSystemDomain ; domain <= kLastDomainConstant ; domain++) {
118        OSStatus err;
119        FSRef fsr;
120        err = FSFindFolder(domain, kSystemSoundsFolderType, false, &fsr);
121        if (err != noErr) continue;
122
123        UInt8 path[PATH_MAX];
124        err = FSRefMakePath(&fsr, path, PATH_MAX);
125        if (err != noErr) continue;
126
127        CFStringRef pathString = CFStringCreateWithFileSystemRepresentation(NULL, (const char *)path);
128        if (pathString == NULL) continue;
129
130        [soundFolderPaths addObject: (NSString *)pathString];
131        CFRelease(pathString);
132    }
133    NSFileManager *fm = [NSFileManager defaultManager];
134    NSEnumerator *e = [soundFolderPaths objectEnumerator];
135    NSString *folderPath;
136    while ( (folderPath = [e nextObject]) != nil) {
137        if (![fm changeCurrentDirectoryPath: folderPath]) continue;
138
139        NSDirectoryEnumerator *de = [fm enumeratorAtPath: folderPath];
140        NSString *path;
141        while ( (path = [de nextObject]) != nil) {
142            BOOL isDir;
143            if (![fm fileExistsAtPath: path isDirectory: &isDir] || isDir) {
144                [de skipDescendents];
145                continue;
146            }
147
148            if (![QTMovie canInitWithFile: path]) continue;
149           
150            item = [menu addItemWithTitle: [fm displayNameAtPath: path]
151                                   action: @selector(_systemSoundSelected:)
152                            keyEquivalent: @""];
153            [item setTarget: self];
154            [item setImageFromPath: path];
155            path = [folderPath stringByAppendingPathComponent: path];
156            [item setRepresentedObject: path];
157            [item setToolTip: path];
158        }
159    }
160    [soundFolderPaths release];
161   
162    if ([menu numberOfItems] == 2) {
163        item = [menu addItemWithTitle: NSLocalizedString(@"Can't locate alert sounds", "QuickTime media popup menu item surrogate for alert sound list if no sounds are found") action: nil keyEquivalent: @""];
164        [item setEnabled: NO];
165    }
166         
167    [menu addItem: [NSMenuItem separatorItem]];
168    item = [menu addItemWithTitle: NSLocalizedString(@"Other...", "Media popup item to select another sound/movie/image") action: @selector(select:) keyEquivalent: @""];
169    [item setTarget: self];
170    otherItem = [item retain];
171
172    [self _validateWithPreview: NO];
173
174    recentMediaAliasData = [[NSMutableArray alloc] initWithCapacity: NJRQTMediaPopUpButtonMaxRecentItems + 1];
175    [self _addRecentMediaFromAliasesData: [[NSUserDefaults standardUserDefaults] arrayForKey: [self _defaultKey]]];
176    // [self _validateRecentMedia];
177
178    [self registerForDraggedTypes:
179        [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]];
180}
181
182- (id)initWithFrame:(NSRect)frame;
183{
184    if ( (self = [super initWithFrame: frame]) != nil) {
185        [self _setUp];
186    }
187    return self;
188}
189
190- (id)initWithCoder:(NSCoder *)coder;
191{
192    if ( (self = [super initWithCoder: coder]) != nil) {
193        [self _setUp];
194    }
195    return self;
196}
197
198- (void)dealloc;
199{
200    [recentMediaAliasData release]; recentMediaAliasData = nil;
201    [otherItem release];
202    [selectedAlias release]; [previousAlias release];
203    [super dealloc];
204}
205
206#pragma mark accessing
207
208- (BDAlias *)selectedAlias;
209{
210    return selectedAlias;
211}
212
213- (void)_setAlias:(BDAlias *)alias;
214{
215    BDAlias *oldAlias = [selectedAlias retain];
216    [previousAlias release];
217    previousAlias = oldAlias;
218    if (selectedAlias != alias) {
219        [selectedAlias release];
220        selectedAlias = [alias retain];
221    }
222}
223
224- (void)setAlias:(BDAlias *)alias;
225{
226    [self _setAlias: alias];
227    if ([self _validateWithPreview: NO]) {
228        [self selectItem: [self _itemForAlias: selectedAlias]];
229    }
230}
231
232- (void)_setPath:(NSString *)path;
233{
234    [self _setAlias: [BDAlias aliasWithPath: path]];
235}
236
237- (NSMenuItem *)_itemForAlias:(BDAlias *)alias;
238{
239    if (alias == nil) return [self itemAtIndex: 0];
240
241    // [self _validateRecentMedia];
242    NSString *path = [alias fullPath];
243
244    // selected a system sound?
245    int itemIndex = [[self menu] indexOfItemWithRepresentedObject: path];
246    if (itemIndex != -1) {
247        // NSLog(@"_itemForAlias: selected system sound");
248        return [self itemAtIndex: itemIndex];
249    } else {
250        NSEnumerator *e = [recentMediaAliasData reverseObjectEnumerator];
251        NSData *aliasData;
252        NSMenuItem *item;
253        int recentIndex = 1;
254
255        while ( (aliasData = [e nextObject]) != nil) {
256            // selected a recently selected, non-system sound?
257            if ([alias aliasDataIsEqual: aliasData]) {
258                int otherIndex = [self indexOfItem: otherItem];
259                int menuIndex = recentIndex + otherIndex;
260                if (menuIndex == otherIndex + 1) return [self itemAtIndex: menuIndex]; // already at top
261                // remove item, add (at top) later
262                // NSLog(@"_itemForAlias removing item: idx %d + otherItemIdx %d + 1 = %d [%@]", recentIndex, otherIndex, menuIndex, [self itemAtIndex: menuIndex]);
263                [self removeItemAtIndex: menuIndex];
264                [recentMediaAliasData removeObjectAtIndex: [recentMediaAliasData count] - recentIndex];
265                break;
266            }
267            recentIndex++;
268        }
269
270        // create the item
271        item = [self _addRecentMediaAtPath: path withAlias: alias];
272        [self _writeRecentMedia];
273        return item;
274    }
275}
276
277- (BOOL)canRepeat;
278{
279    return movieCanRepeat;
280}
281
282- (BOOL)hasAudio;
283{
284    return movieHasAudio;
285}
286
287- (float)outputVolume;
288{
289    return outputVolume;
290}
291
292- (void)setOutputVolume:(float)volume withPreview:(BOOL)doPreview;
293{
294    if (![NJRSoundManager volumeIsNotMutedOrInvalid: volume]) return;
295    outputVolume = volume;
296    if (!doPreview) return;
297    // NSLog(@"setting volume to %f, preview movie %@", volume, [preview movie]);
298    if ([preview movie] == nil) {
299        [self _validateWithPreview: YES];
300    } else {
301        [self _startSoundPreview];
302    }
303}
304
305#pragma mark selected media validation
306
307- (void)_invalidateSelection;
308{
309    [self _setAlias: previousAlias];
310    [self selectItem: [self _itemForAlias: [self selectedAlias]]];
311    [[NSNotificationCenter defaultCenter] postNotificationName: NJRQTMediaPopUpButtonMovieChangedNotification object: self];
312}
313
314- (void)_startSoundPreview;
315{
316    if ([preview movie] == nil || outputVolume == kNoVolume)
317        return;
318
319    if (savedVolume || [NJRSoundManager saveDefaultOutputVolume]) {
320        savedVolume = YES;
321        [NJRSoundManager setDefaultOutputVolume: outputVolume];
322    }
323
324    if ([[preview movie] rate] != 0)
325        return; // don't restart preview if already playing
326   
327    [[NSNotificationCenter defaultCenter] addObserver: self
328                                             selector: @selector(_soundPreviewDidEnd:)
329                                                 name: QTMovieDidEndNotification
330                                               object: [preview movie]];
331    [preview play: self];
332}
333
334- (void)_soundPreviewDidEnd:(NSNotification *)notification;
335{
336    [self _resetPreview];
337}
338
339- (void)_resetPreview;
340{
341    [preview setMovie: nil];
342    [self _resetOutputVolume];
343}
344
345- (void)_resetOutputVolume;
346{
347    [NJRSoundManager restoreSavedDefaultOutputVolumeIfCurrently: outputVolume];
348    savedVolume = NO;
349}
350
351- (BOOL)_validateWithPreview:(BOOL)doPreview;
352{
353    // prevent _resetPreview from triggering afterward (crashes)
354    [[NSNotificationCenter defaultCenter] removeObserver: self
355                                                    name: QTMovieDidEndNotification
356                                                  object: [preview movie]];
357    [preview pause: self];
358    if (selectedAlias == nil) {
359        [preview setMovie: nil];
360        movieCanRepeat = YES;
361        movieHasAudio = NO; // XXX should be YES - this is broken, NSBeep() is asynchronous
362        if (doPreview) {
363            // XXX [self _updateOutputVolume];
364            NSBeep();
365            // XXX [self _resetOutputVolume];
366        }
367    } else {
368        NSError *error;
369        QTMovie *movie = [[QTMovie alloc] initWithFile: [selectedAlias fullPath] error: &error];
370        movieCanRepeat = ![movie NJR_isStatic];
371        if (movieHasAudio = [movie NJR_hasAudio]) {
372            [preview setMovie: doPreview ? movie : nil];
373        } else {
374            [self _resetPreview];
375            doPreview = NO;
376            if (movie == nil) {
377                NSBeginAlertSheet(@"Format not recognized", nil, nil, nil, [self window], nil, nil, nil, nil, [NSString stringWithFormat: NSLocalizedString(@"The item you selected isn't an image, sound or movie recognized by QuickTime. (%@)\n\nPlease select a different item.", "Message displayed in alert sheet when media document is not recognized by QuickTime"), [error localizedDescription]]);
378                [self _invalidateSelection];
379                return NO;
380            }
381            if (![movie NJR_hasAudio] && ![movie NJR_hasVideo]) {
382                NSBeginAlertSheet(@"No video or audio", nil, nil, nil, [self window], nil, nil, nil, nil, NSLocalizedString(@"'%@' contains neither audio nor video content playable by QuickTime.\n\nPlease select a different item.", "Message displayed in alert sheet when media document is readable, but has neither audio nor video tracks"), [[NSFileManager defaultManager] displayNameAtPath: [selectedAlias fullPath]]);
383                [self _invalidateSelection];
384                [movie release];
385                return NO;
386            }
387        }
388        if (doPreview) {
389            [self _startSoundPreview];
390        }
391        [movie release];
392    }
393    [[NSNotificationCenter defaultCenter] postNotificationName: NJRQTMediaPopUpButtonMovieChangedNotification object: self];
394    return YES;
395}
396
397#pragma mark actions
398
399- (IBAction)stopSoundPreview:(id)sender;
400{
401    [preview pause: self];
402    [self _resetPreview];
403}
404
405- (void)_beepSelected:(NSMenuItem *)sender;
406{
407    [self _setAlias: nil];
408    [self _validateWithPreview: YES];
409}
410
411- (void)_systemSoundSelected:(NSMenuItem *)sender;
412{
413    [self _setPath: [sender representedObject]];
414    if (![self _validateWithPreview: YES]) {
415        [[self menu] removeItem: sender];
416    }
417}
418
419- (void)_aliasSelected:(NSMenuItem *)sender;
420{
421    BDAlias *alias = [sender representedObject];
422    int index = [self indexOfItem: sender], otherIndex = [self indexOfItem: otherItem];
423    [self _setAlias: alias];
424    if (![self _validateWithPreview: YES]) {
425        [[self menu] removeItem: sender];
426    } else if (index > otherIndex + 1) { // move "other" item to top of list
427        int recentIndex = [recentMediaAliasData count] - index + otherIndex;
428        NSMenuItem *item = [[self itemAtIndex: index] retain];
429        NSData *data = [[recentMediaAliasData objectAtIndex: recentIndex] retain];
430        // [self _validateRecentMedia];
431        [self removeItemAtIndex: index];
432        [[self menu] insertItem: item atIndex: otherIndex + 1];
433        [self selectItem: item];
434        [item release];
435        NSAssert(recentIndex >= 0, @"Recent media index invalid");
436        // NSLog(@"_aliasSelected removing item %d - %d + %d = %d of recentMediaAliasData", [recentMediaAliasData count], index, otherIndex, recentIndex);
437        [recentMediaAliasData removeObjectAtIndex: recentIndex];
438        [recentMediaAliasData addObject: data];
439        [self _validateRecentMedia];
440        [data release];
441    } // else NSLog(@"_aliasSelected ...already at top");
442}
443
444- (IBAction)select:(id)sender;
445{
446    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
447    NSString *path = [selectedAlias fullPath];
448    [openPanel setAllowsMultipleSelection: NO];
449    [openPanel setCanChooseDirectories: NO];
450    [openPanel setCanChooseFiles: YES];
451    [openPanel setDelegate: self];
452    [openPanel beginSheetForDirectory: [path stringByDeletingLastPathComponent]
453                                 file: [path lastPathComponent]
454                                types: nil
455                       modalForWindow: [self window]
456                        modalDelegate: self
457                       didEndSelector: @selector(openPanelDidEnd:returnCode:contextInfo:)
458                          contextInfo: nil];
459}
460
461- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
462{
463    [sheet close];
464
465    if (returnCode == NSOKButton) {
466        NSArray *files = [sheet filenames];
467        NSAssert1([files count] == 1, @"%d items returned, only one expected", [files count]);
468        [self _setPath: [files objectAtIndex: 0]];
469        if ([self _validateWithPreview: YES]) {
470            [self selectItem: [self _itemForAlias: selectedAlias]];
471        }
472    } else {
473        // "Other..." item is still selected, revert to previously selected item
474        // XXX issue with cancelling, top item in recent menu is sometimes duplicated!?
475        [self selectItem: [self _itemForAlias: selectedAlias]];
476    }
477    // [self _validateRecentMedia];
478}
479
480- (void)setEnabled:(BOOL)flag;
481{
482    [super setEnabled: flag];
483    if (flag) ; // XXX [self startSoundPreview: self]; // need to prohibit at startup
484    else [self stopSoundPreview: self];
485}
486
487#pragma mark drag feedback
488
489- (void)drawRect:(NSRect)rect;
490{
491    if (dragAccepted) {
492        NSWindow *window = [self window];
493        NSRect boundsRect = [self bounds];
494        BOOL isFirstResponder = ([window firstResponder] == self);
495        // focus ring and drag feedback interfere with one another
496        if (isFirstResponder) [window makeFirstResponder: window];
497        [super drawRect: rect];
498        [[NSColor selectedControlColor] set];
499        NSFrameRectWithWidthUsingOperation(NSInsetRect(boundsRect, 2, 2), 3, NSCompositeSourceIn);
500        if (isFirstResponder) [window makeFirstResponder: self];
501    } else {
502        [super drawRect: rect];
503    }
504}
505
506@end
507
508@implementation NJRQTMediaPopUpButton (NSSavePanelDelegate)
509
510- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
511{
512    BOOL isDir = NO;
513    [[NSFileManager defaultManager] fileExistsAtPath: filename isDirectory: &isDir];
514
515    if (isDir)
516        return YES;
517
518    return [QTMovie canInitWithFile: filename];
519}
520
521@end
522
523@implementation NJRQTMediaPopUpButton (NSDraggingDestination)
524
525- (BOOL)acceptsDragFrom:(id <NSDraggingInfo>)sender;
526{
527    NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
528    NSFileManager *fm = [NSFileManager defaultManager];
529    BOOL isDir;
530
531    if (url == nil || ![url isFileURL]) return NO;
532
533    if (![fm fileExistsAtPath: [url path] isDirectory: &isDir]) return NO;
534
535    if (isDir) return NO;
536   
537    return YES;
538}
539
540- (NSString *)_descriptionForDraggingInfo:(id <NSDraggingInfo>)sender;
541{
542    NSDragOperation mask = [sender draggingSourceOperationMask];
543    NSMutableString *s = [NSMutableString stringWithFormat: @"Drag seq %d source: %@",
544        [sender draggingSequenceNumber], [sender draggingSource]];
545    NSPasteboard *draggingPasteboard = [sender draggingPasteboard];
546    NSArray *types = [draggingPasteboard types];
547    NSEnumerator *e = [types objectEnumerator];
548    NSString *type;
549    [s appendString: @"\nDrag operations:"];
550    if (mask & NSDragOperationCopy) [s appendString: @" copy"];
551    if (mask & NSDragOperationLink) [s appendString: @" link"];
552    if (mask & NSDragOperationGeneric) [s appendString: @" generic"];
553    if (mask & NSDragOperationPrivate) [s appendString: @" private"];
554    if (mask & NSDragOperationMove) [s appendString: @" move"];
555    if (mask & NSDragOperationDelete) [s appendString: @" delete"];
556    if (mask & NSDragOperationEvery) [s appendString: @" every"];
557    if (mask & NSDragOperationNone) [s appendString: @" none"];
558    [s appendFormat: @"\nImage: %@ at %@", [sender draggedImage],
559        NSStringFromPoint([sender draggedImageLocation])];
560    [s appendFormat: @"\nDestination: %@ at %@", [sender draggingDestinationWindow],
561        NSStringFromPoint([sender draggingLocation])];
562    [s appendFormat: @"\nPasteboard: %@ types:", draggingPasteboard];
563    while ( (type = [e nextObject]) != nil) {
564        if ([type hasPrefix: @"CorePasteboardFlavorType 0x"]) {
565            const char *osTypeHex = [[type substringFromIndex: [type rangeOfString: @"0x" options: NSBackwardsSearch].location] lossyCString];
566            OSType osType;
567            sscanf(osTypeHex, "%lx", &osType);
568            [s appendFormat: @" '%4s'", &osType];
569        } else {
570            [s appendFormat: @" '%@'", type];
571        }
572    }
573    return s;
574}
575
576- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
577{
578    if ([self acceptsDragFrom: sender] && [sender draggingSourceOperationMask] &
579        (NSDragOperationCopy | NSDragOperationLink)) {
580        dragAccepted = YES;
581        [self setNeedsDisplay: YES];
582        // NSLog(@"draggingEntered accept:\n%@", [self _descriptionForDraggingInfo: sender]);
583        return NSDragOperationLink;
584    }
585    return NSDragOperationNone;
586}
587
588- (void)draggingExited:(id <NSDraggingInfo>)sender;
589{
590    dragAccepted = NO;
591    [self setNeedsDisplay: YES];
592}
593
594- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender;
595{
596    dragAccepted = NO;
597    [self setNeedsDisplay: YES];
598    return [self acceptsDragFrom: sender];
599}
600
601- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender;
602{
603    if ([sender draggingSource] != self) {
604        NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
605        if (url == nil) return NO;
606        [self _setPath: [url path]];
607        if ([self _validateWithPreview: YES]) {
608            [self selectItem: [self _itemForAlias: selectedAlias]];
609        }
610    }
611    return YES;
612}
613
614@end
Note: See TracBrowser for help on using the repository browser.