Index: /trunk/Cocoa/Pester/Source/BDAlias.h
===================================================================
--- /trunk/Cocoa/Pester/Source/BDAlias.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/BDAlias.h	(revision 34)
@@ -0,0 +1,71 @@
+/*
+    Copyright (c) 2001, bDistributed.com, Inc.
+    All rights reserved.
+
+    Redistribution and use in source and binary forms, with or
+    without modification, are permitted provided that the following
+    conditions are met:
+
+    *   Redistributions of source code must retain the above
+        copyright notice, this list of conditions and the following
+        disclaimer.
+    
+    *   Redistributions in binary form must reproduce the above
+        copyright notice, this list of conditions and the following
+        disclaimer in the documentation and/or other materials
+        provided with the distribution.
+    
+    *   Neither the name of bDistributed.com, Inc. nor the names of
+        its contributors may be used to endorse or promote products
+        derived from this software without specific prior written
+        permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+    CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+    MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+    DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+    OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+    TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+    OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+    OF SUCH DAMAGE.
+*/
+
+#import <Foundation/Foundation.h>
+#import <CoreServices/CoreServices.h>
+
+@interface BDAlias : NSObject
+{
+	AliasHandle _alias;
+}
+
+- (id)initWithAliasHandle:(AliasHandle)alias; // designated initializer
+- (id)initWithData:(NSData *)data;
+- (id)initWithPath:(NSString *)fullPath;
+- (id)initWithPath:(NSString *)path relativeToPath:(NSString *)relPath;
+- (id)initWithFSRef:(FSRef *)ref;
+- (id)initWithFSRef:(FSRef *)ref relativeToFSRef:(FSRef *)relRef;
+
+- (void)dealloc;
+
+- (AliasHandle)alias;
+- (void)setAlias:(AliasHandle)newAlias;
+
+- (NSData *)aliasData;
+- (void)setAliasData:(NSData *)newAliasData;
+
+- (NSString *)fullPath;
+- (NSString *)fullPathRelativeToPath:(NSString *)relPath;
+
++ (BDAlias *)aliasWithAliasHandle:(AliasHandle)alias;
++ (BDAlias *)aliasWithData:(NSData *)data;
++ (BDAlias *)aliasWithPath:(NSString *)fullPath;
++ (BDAlias *)aliasWithPath:(NSString *)path relativeToPath:(NSString *)relPath;
++ (BDAlias *)aliasWithFSRef:(FSRef *)ref;
++ (BDAlias *)aliasWithFSRef:(FSRef *)ref relativeToFSRef:(FSRef *)relRef;
+
+@end
Index: /trunk/Cocoa/Pester/Source/BDAlias.m
===================================================================
--- /trunk/Cocoa/Pester/Source/BDAlias.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/BDAlias.m	(revision 34)
@@ -0,0 +1,313 @@
+/*
+    Copyright (c) 2001, bDistributed.com, Inc.
+    All rights reserved.
+
+    Redistribution and use in source and binary forms, with or
+    without modification, are permitted provided that the following
+    conditions are met:
+
+    *   Redistributions of source code must retain the above
+        copyright notice, this list of conditions and the following
+        disclaimer.
+    
+    *   Redistributions in binary form must reproduce the above
+        copyright notice, this list of conditions and the following
+        disclaimer in the documentation and/or other materials
+        provided with the distribution.
+    
+    *   Neither the name of bDistributed.com, Inc. nor the names of
+        its contributors may be used to endorse or promote products
+        derived from this software without specific prior written
+        permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+    CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+    MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+    DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
+    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+    OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+    TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+    OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+    OF SUCH DAMAGE.
+*/
+
+#include <assert.h>
+
+#import "BDAlias.h"
+
+
+static Handle DataToHandle(CFDataRef inData);
+static CFDataRef HandleToData(Handle inHandle);
+
+static OSStatus PathToFSRef(CFStringRef inPath, FSRef *outRef);
+static CFStringRef FSRefToPathCopy(const FSRef *inRef);
+
+
+static Handle DataToHandle(CFDataRef inData)
+{
+    CFIndex	len;
+    Handle	handle = NULL;
+    
+    if (inData == NULL) {
+        return NULL;
+    }
+    
+    len = CFDataGetLength(inData);
+    
+    handle = NewHandle(len);
+    
+    if ((handle != NULL) && (len > 0)) {
+        HLock(handle);
+        BlockMoveData(CFDataGetBytePtr(inData), *handle, len);
+        HUnlock(handle);
+    }
+    
+    return handle;
+}
+
+static CFDataRef HandleToData(Handle inHandle)
+{
+    CFDataRef	data = NULL;
+    CFIndex	len;
+    SInt8	handleState;
+    
+    if (inHandle == NULL) {
+        return NULL;
+    }
+    
+    len = GetHandleSize(inHandle);
+    
+    handleState = HGetState(inHandle);
+    
+    HLock(inHandle);
+    
+    data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *) *inHandle, len);
+    
+    HSetState(inHandle, handleState);
+    
+    return data;
+}
+
+static OSStatus PathToFSRef(CFStringRef inPath, FSRef *outRef)
+{
+    CFURLRef	tempURL = NULL;
+    Boolean	gotRef = false;
+    
+    tempURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, inPath,
+                                            kCFURLPOSIXPathStyle, false);
+    
+    if (tempURL == NULL) {
+        return fnfErr;
+    }
+    
+    gotRef = CFURLGetFSRef(tempURL, outRef);
+    
+    if (gotRef == false) {
+        return fnfErr;
+    }
+    
+    return noErr;
+}
+
+static CFStringRef FSRefToPathCopy(const FSRef *inRef)
+{
+    CFURLRef	tempURL = NULL;
+    CFStringRef	result = NULL;
+    
+    if (inRef != NULL) {
+        tempURL = CFURLCreateFromFSRef(kCFAllocatorDefault, inRef);
+        
+        if (tempURL == NULL) {
+            return NULL;
+        }
+        
+        result = CFURLCopyFileSystemPath(tempURL, kCFURLPOSIXPathStyle);
+        
+        CFRelease(tempURL);
+    }
+    
+    return result;
+}
+
+
+@implementation BDAlias
+
+- (id)initWithAliasHandle:(AliasHandle)alias
+{
+    id ret = [super init];
+    
+    if (ret != nil) {
+        _alias = alias;
+    }
+    
+    return ret;
+}
+
+- (id)initWithData:(NSData *)data
+{
+    return [self initWithAliasHandle:(AliasHandle)DataToHandle((CFDataRef) data)];
+}
+
+- (id)initWithPath:(NSString *)fullPath
+{
+    OSStatus	anErr = noErr;
+    FSRef		ref;
+    
+    anErr = PathToFSRef((CFStringRef) fullPath, &ref);
+    
+    if (anErr != noErr) {
+        return nil;
+    }
+    
+    return [self initWithFSRef:&ref];;
+}
+
+- (id)initWithPath:(NSString *)path relativeToPath:(NSString *)relPath
+{
+    OSStatus	anErr = noErr;
+    FSRef		ref, relRef;
+    
+    anErr = PathToFSRef((CFStringRef) [relPath stringByAppendingPathComponent:path],
+                        &ref);
+    
+    if (anErr != noErr) {
+        return nil;
+    }
+    
+    anErr = PathToFSRef((CFStringRef) relPath, &relRef);
+    
+    if (anErr != noErr) {
+        return nil;
+    }
+    
+    return [self initWithFSRef:&ref relativeToFSRef:&relRef];
+}
+
+- (id)initWithFSRef:(FSRef *)ref
+{
+    return [self initWithFSRef:ref relativeToFSRef:NULL];
+}
+
+- (id)initWithFSRef:(FSRef *)ref relativeToFSRef:(FSRef *)relRef
+{
+    OSStatus	anErr = noErr;
+    AliasHandle	alias = NULL;
+    
+    anErr = FSNewAlias(relRef, ref, &alias);
+    
+    if (anErr != noErr) {
+        return nil;
+    }
+    
+    return [self initWithAliasHandle:alias];
+}
+
+- (void)dealloc
+{
+    if (_alias != NULL) {
+        DisposeHandle((Handle) _alias);
+        _alias = NULL;
+    }
+    
+    [super dealloc];
+}
+
+- (AliasHandle)alias
+{
+    return _alias;
+}
+
+- (void)setAlias:(AliasHandle)newAlias
+{
+    if (_alias != NULL) {
+        DisposeHandle((Handle) _alias);
+    }
+    
+    _alias = newAlias;
+}
+
+- (NSData *)aliasData
+{
+    NSData *result;
+    
+    result = (NSData *)HandleToData((Handle) _alias);
+    
+    return [result autorelease];
+}
+
+- (void)setAliasData:(NSData *)newAliasData
+{
+    [self setAlias:(AliasHandle) DataToHandle((CFDataRef) newAliasData)];
+}
+
+- (NSString *)fullPath
+{
+    return [self fullPathRelativeToPath:nil];
+}
+
+- (NSString *)fullPathRelativeToPath:(NSString *)relPath
+{
+    OSStatus	anErr = noErr;
+    FSRef	relPathRef;
+    FSRef	tempRef;
+    NSString	*result = nil;
+    Boolean	wasChanged;
+    
+    if (_alias != NULL) {
+        if (relPath != nil) {
+            anErr = PathToFSRef((CFStringRef)relPath, &relPathRef);
+            
+            if (anErr != noErr) {
+                return NULL;
+            }
+            
+            anErr = FSResolveAlias(&relPathRef, _alias, &tempRef, &wasChanged);
+        } else {
+            anErr = FSResolveAlias(NULL, _alias, &tempRef, &wasChanged);
+        }
+        
+        if (anErr != noErr) {
+            return NULL;
+        }
+        
+        result = (NSString *)FSRefToPathCopy(&tempRef);
+    }
+    
+    return [result autorelease];
+}
+
++ (BDAlias *)aliasWithAliasHandle:(AliasHandle)alias
+{
+    return [[[BDAlias alloc] initWithAliasHandle:alias] autorelease];
+}
+
++ (BDAlias *)aliasWithData:(NSData *)data
+{
+    return [[[BDAlias alloc] initWithData:data] autorelease];
+}
+
++ (BDAlias *)aliasWithPath:(NSString *)fullPath
+{
+    return [[[BDAlias alloc] initWithPath:fullPath] autorelease];
+}
+
++ (BDAlias *)aliasWithPath:(NSString *)path relativeToPath:(NSString *)relPath
+{
+    return [[[BDAlias alloc] initWithPath:path relativeToPath:relPath] autorelease];
+}
+
++ (BDAlias *)aliasWithFSRef:(FSRef *)ref
+{
+    return [[[BDAlias alloc] initWithFSRef:ref] autorelease];
+}
+
++ (BDAlias *)aliasWithFSRef:(FSRef *)ref relativeToFSRef:(FSRef *)relRef
+{
+    return [[[BDAlias alloc] initWithFSRef:ref relativeToFSRef:relRef] autorelease];
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRFSObjectSelector.h
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRFSObjectSelector.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRFSObjectSelector.h	(revision 34)
@@ -0,0 +1,26 @@
+/* NJRFSObjectSelector */
+
+#import <Cocoa/Cocoa.h>
+#import "BDAlias.h"
+
+@interface NJRFSObjectSelector : NSButton
+{
+    BDAlias *selectedAlias;
+    NSArray *fileTypes;
+    BOOL canChooseDirectories;
+    BOOL canChooseFiles;
+    BOOL dragAccepted;
+}
+- (IBAction)select:(id)sender;
+- (void)setAlias:(BDAlias *)alias;
+- (BDAlias *)alias;
+- (BOOL)acceptsPath:(NSString *)path;
+- (void)setPath:(NSString *)path; // does not validate
+- (BOOL)canChooseDirectories;
+- (BOOL)canChooseFiles;
+- (void)setCanChooseDirectories:(BOOL)flag;
+- (void)setCanChooseFiles:(BOOL)flag;
+- (NSArray *)fileTypes;
+- (void)setFileTypes:(NSArray *)types;
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRFSObjectSelector.m
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRFSObjectSelector.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRFSObjectSelector.m	(revision 34)
@@ -0,0 +1,303 @@
+#import "NJRFSObjectSelector.h"
+#import "NSImage-NJRExtensions.h"
+#include <Carbon/Carbon.h>
+
+@implementation NJRFSObjectSelector
+
+- (void)_initSelector;
+{
+    canChooseFiles = YES; canChooseDirectories = NO;
+    [self setAlias: nil];
+    [[self cell] setHighlightsBy: NSChangeBackgroundCell];
+    [[self cell] setGradientType: NSGradientNone];
+    [self registerForDraggedTypes:
+        [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]];
+}
+
+- (void)dealloc;
+{
+    [selectedAlias release];
+    [fileTypes release];
+    [super dealloc];
+}
+
+- (id)initWithCoder:(NSCoder *)coder;
+{
+    if ( (self = [super initWithCoder: coder]) != nil) {
+        [self _initSelector];
+    }
+    return self;
+}
+
+- (id)initWithFrame:(NSRect)frame;
+{
+    if ( (self = [super initWithFrame: frame]) != nil) {
+        [self _initSelector];
+    }
+    return self;
+}
+
+- (void)drawRect:(NSRect)rect;
+{
+    [super drawRect: rect];
+    if (dragAccepted) {
+        [[NSColor selectedControlColor] set];
+        [NSBezierPath setDefaultLineWidth: 2];
+        [NSBezierPath strokeRect: NSInsetRect(rect, 2, 2)];
+    } else {
+        static NSImage *popupTriangle = nil;
+        static NSSize imageSize;
+        if (popupTriangle == nil) {
+            popupTriangle = [[NSImage imageNamed: @"Popup triangle"] retain];
+            imageSize = [popupTriangle size];
+        }
+        // equivalent to popup triangle location for large bezel in Carbon
+        [popupTriangle compositeToPoint: NSMakePoint(NSMaxX(rect) - imageSize.width - 5, NSMaxY(rect) - 5) operation: NSCompositeCopy];
+    }
+}
+
+- (BOOL)acceptsPath:(NSString *)path;
+{
+    NSFileManager *fm = [NSFileManager defaultManager];
+    BOOL isDir;
+
+    if (![fm fileExistsAtPath: path isDirectory: &isDir]) return NO;
+
+    if (isDir) return canChooseDirectories;
+    if (canChooseFiles) {
+        NSEnumerator *e = [fileTypes objectEnumerator];
+        NSString *extension = [path pathExtension];
+        NSString *hfsType = NSHFSTypeOfFile(path);
+        NSString *fileType;
+        
+        while ( (fileType = [e nextObject]) != nil) {
+            if ([fileType isEqualToString: extension] || [fileType isEqualToString: hfsType])
+                return YES;
+        }
+    }
+    return NO;
+}
+
+- (void)setImage:(NSImage *)image;
+{
+    [super setImage: [image bestFitImageForSize: [[self cell] cellSize]]];
+}
+
+- (IBAction)select:(id)sender;
+{
+    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
+    NSString *path = [selectedAlias fullPath];
+    [openPanel setAllowsMultipleSelection: NO];
+    [openPanel setCanChooseDirectories: canChooseDirectories];
+    [openPanel setCanChooseFiles: canChooseFiles];
+    [openPanel beginSheetForDirectory: [path stringByDeletingLastPathComponent]
+                                 file: [path lastPathComponent]
+                                types: fileTypes
+                       modalForWindow: [self window]
+                        modalDelegate: self
+                       didEndSelector: @selector(openPanelDidEnd:returnCode:contextInfo:)
+                          contextInfo: nil];
+}
+
+- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
+{
+    [sheet close];
+
+    if (returnCode == NSOKButton) {
+        NSArray *files = [sheet filenames];
+        NSAssert1([files count] == 1, @"%d items returned, only one expected", [files count]);
+        [self setPath: [files objectAtIndex: 0]];
+        [[self target] tryToPerform: [self action] with: self];
+    }
+}
+
+- (void)revealInFinder:(id<NSMenuItem>)sender;
+{
+    NSString *path = [sender representedObject];
+    if (path == nil) return;
+    [[NSWorkspace sharedWorkspace] selectFile: path inFileViewerRootedAtPath: @""];
+}
+
+- (void)setAlias:(BDAlias *)alias;
+{
+    if (selectedAlias != alias) {
+        [selectedAlias release];
+        selectedAlias = [alias retain];
+    }
+
+    if (alias != nil) { // alias is set
+        NSString *path = [alias fullPath];
+        NSString *revealPath = nil;
+        NSString *targetName = [path lastPathComponent];
+        NSMenu *menu = [[NSMenu alloc] initWithTitle: @""];
+        NSFileManager *fmgr = [NSFileManager defaultManager];
+        NSMenuItem *item;
+        if (path != nil) { // can resolve alias
+            [self setImage: [[NSWorkspace sharedWorkspace] iconForFile: path]];
+            [self setTitle: targetName];
+            do {
+                item = [menu addItemWithTitle: [fmgr displayNameAtPath: path]
+                                       action: @selector(revealInFinder:)
+                                keyEquivalent: @""];
+                [item setTarget: self];
+                [item setRepresentedObject: revealPath];
+                [item setImage:
+                    [[[NSWorkspace sharedWorkspace] iconForFile: path] bestFitImageForSize: NSMakeSize(16, 16)]];
+                revealPath = path;
+                path = [path stringByDeletingLastPathComponent];
+                NSAssert1(![path isEqualToString: revealPath], @"Stuck on path |%@|", [alias fullPath]);
+            } while (![revealPath isEqualToString: @"/"] && ![path isEqualToString: @"/Volumes"]);
+            [[self cell] setMenu: menu];
+        } else {
+            [self setImage: nil];
+            [self setTitle: @"(not available)"];
+            [[self cell] setMenu: nil];
+        }
+    } else {
+        [self setImage: nil];
+        [self setTitle: @"(none selected)"];
+        [[self cell] setMenu: nil];
+    }
+    [self setEnabled: YES];
+}
+
+- (void)setEnabled:(BOOL)enabled;
+{
+    [super setEnabled: enabled ? selectedAlias != nil : NO];
+}
+
+- (void)rightMouseDown:(NSEvent *)theEvent;
+{
+    [self mouseDown: theEvent];
+}
+
+- (void)otherMouseDown:(NSEvent *)theEvent;
+{
+    [self mouseDown: theEvent];
+}
+
+extern MenuRef _NSGetCarbonMenu(NSMenu *menu);
+
+- (void)mouseDown:(NSEvent *)theEvent;
+{
+    NSMenu *menu = [[self cell] menu];
+    MenuRef mRef = _NSGetCarbonMenu(menu);
+
+    if (mRef == NULL) {
+        NSMenu *appMenu = [[[NSApp mainMenu] itemWithTitle: @""] submenu];
+        if (appMenu != nil) {
+            NSMenuItem *item = [appMenu addItemWithTitle: @"" action: NULL keyEquivalent: @""];
+            [appMenu setSubmenu: menu forItem: item];
+            [appMenu removeItem: item];
+        }
+        mRef = _NSGetCarbonMenu(menu);
+    }
+
+    ChangeMenuAttributes(mRef, kMenuAttrExcludesMarkColumn, 0);
+    theEvent = [NSEvent mouseEventWithType: [theEvent type]
+                                  location: [self convertPoint: NSMakePoint(-1, 1) toView: nil]
+                             modifierFlags: [theEvent modifierFlags]
+                                 timestamp: [theEvent timestamp]
+                              windowNumber: [theEvent windowNumber]
+                                   context: [theEvent context]
+                               eventNumber: [theEvent eventNumber]
+                                clickCount: [theEvent clickCount]
+                                  pressure: [theEvent pressure]];
+
+    // this undocumented API does not work any better; contextual menu items are still added:
+    // [menu _popUpMenuWithEvent: theEvent forView: self];
+    [NSMenu popUpContextMenu: menu withEvent: theEvent forView: self];
+}
+
+- (BDAlias *)alias;
+{
+    return selectedAlias;
+}
+
+- (void)setPath:(NSString *)path;
+{
+    [self setAlias: [BDAlias aliasWithPath: path]];
+}
+
+- (BOOL)canChooseDirectories;
+{
+    return canChooseDirectories;
+}
+
+- (BOOL)canChooseFiles;
+{
+    return canChooseFiles;
+}
+
+- (void)setCanChooseDirectories:(BOOL)flag;
+{
+    canChooseDirectories = flag;
+}
+
+- (void)setCanChooseFiles:(BOOL)flag;
+{
+    canChooseFiles = flag;
+}
+
+- (NSArray *)fileTypes;
+{
+    return fileTypes;
+}
+
+- (void)setFileTypes:(NSArray *)types;
+{
+    if (fileTypes == types) return;
+    
+    [fileTypes release];
+    fileTypes = [types retain];
+}
+
+@end
+
+
+@implementation NJRFSObjectSelector (NSDraggingDestination)
+
+- (BOOL)acceptsDragFrom:(id <NSDraggingInfo>)sender;
+{
+    NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
+
+    if (url == nil || ![url isFileURL]) return NO;
+    return [self acceptsPath: [url path]];
+}
+
+- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
+{
+    if ([self acceptsDragFrom: sender] && [sender draggingSourceOperationMask] &
+        NSDragOperationCopy) {
+        dragAccepted = YES;
+        [self setNeedsDisplay: YES];
+        return NSDragOperationGeneric;
+    }
+    return NSDragOperationNone;
+}
+
+- (void)draggingExited:(id <NSDraggingInfo>)sender;
+{
+    dragAccepted = NO;
+    [self setNeedsDisplay: YES];
+}
+
+- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender;
+{
+    dragAccepted = NO;
+    [self setNeedsDisplay: YES];
+    return [self acceptsDragFrom: sender];
+}
+
+- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender;
+{
+    if ([sender draggingSource] != self) {
+        NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
+        if (url == nil) return NO;
+        [self setPath: [url path]];
+        [[self target] tryToPerform: [self action] with: self];
+    }
+    return YES;
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRSoundPopUpButton.h
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRSoundPopUpButton.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRSoundPopUpButton.h	(revision 34)
@@ -0,0 +1,20 @@
+//
+//  NJRSoundPopUpButton.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+#import "BDAlias.h"
+
+@interface NJRSoundPopUpButton : NSPopUpButton {
+    IBOutlet NSMovieView *preview;
+    BDAlias *selectedAlias;
+}
+
+- (BDAlias *)selectedAlias;
+- (IBAction)stopSoundPreview:(id)sender;
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRSoundPopUpButton.m
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRSoundPopUpButton.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRSoundPopUpButton.m	(revision 34)
@@ -0,0 +1,176 @@
+//
+//  NJRQTMediaPopUpButton.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "NJRQTMediaPopUpButton.h"
+#import "SoundFileManager.h"
+#import "NSMovie-NJRExtensions.h"
+#import "NSImage-NJRExtensions.h"
+
+@interface NJRQTMediaPopUpButton (Private)
+- (BOOL)_previewSound;
+@end
+
+@implementation NJRQTMediaPopUpButton
+
+// XXX handle refreshing sound list on resume
+// XXX don't add icons on Puma
+// XXX add saving of recently selected media
+// XXX launch preview on a separate thread (if movies take too long to load, they inhibit the interface)
+
+- (void)awakeFromNib;
+{
+    NSMenu *menu;
+    NSMenuItem *item;
+    SoundFileManager *sfm = [SoundFileManager sharedSoundFileManager];
+    int soundCount = [sfm count];
+    
+    [self removeAllItems];
+    menu = [self menu];
+    item = [menu addItemWithTitle: @"Alert sound" action: @selector(_beepSelected:) keyEquivalent: @""];
+    [item setTarget: self];
+    [menu addItem: [NSMenuItem separatorItem]];
+    if (soundCount == 0) {
+        item = [menu addItemWithTitle: @"CanÕt locate alert sounds" action: nil keyEquivalent: @""];
+        [item setEnabled: NO];
+    } else {
+        SoundFile *sf;
+        int i;
+        [sfm sortByName];
+        for (i = 0 ; i < soundCount ; i++) {
+            sf = [sfm soundFileAtIndex: i];
+            item = [menu addItemWithTitle: [sf name] action: @selector(_soundFileSelected:) keyEquivalent: @""];
+            [item setTarget: self];
+            [item setRepresentedObject: sf];
+            [item setImage: [[[NSWorkspace sharedWorkspace] iconForFile: [sf path]] bestFitImageForSize: NSMakeSize(16, 16)]];
+        }
+    }
+    [menu addItem: [NSMenuItem separatorItem]];
+    item = [menu addItemWithTitle: @"OtherÉ" action: @selector(select:) keyEquivalent: @""];
+    [item setTarget: self];
+}
+
+- (IBAction)select:(id)sender;
+{
+    NSOpenPanel *openPanel = [NSOpenPanel openPanel];
+    NSString *path = [selectedAlias fullPath];
+    [openPanel setAllowsMultipleSelection: NO];
+    [openPanel setCanChooseDirectories: NO];
+    [openPanel setCanChooseFiles: YES];
+    [openPanel beginSheetForDirectory: [path stringByDeletingLastPathComponent]
+                                 file: [path lastPathComponent]
+                                types: nil // XXX fix for QuickTime!
+                       modalForWindow: [self window]
+                        modalDelegate: self
+                       didEndSelector: @selector(openPanelDidEnd:returnCode:contextInfo:)
+                          contextInfo: nil];
+}
+
+- (void)setAlias:(BDAlias *)alias;
+{
+    if (selectedAlias != alias) {
+        [selectedAlias release];
+        selectedAlias = [alias retain];
+    }
+}
+
+- (void)setPath:(NSString *)path;
+{
+    [self setAlias: [BDAlias aliasWithPath: path]];
+}
+
+- (BDAlias *)selectedAlias;
+{
+    return selectedAlias;
+}
+
+
+- (void)openPanelDidEnd:(NSOpenPanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
+{
+    [sheet close];
+
+    if (returnCode == NSOKButton) {
+        NSArray *files = [sheet filenames];
+        NSAssert1([files count] == 1, @"%d items returned, only one expected", [files count]);
+        [self setPath: [files objectAtIndex: 0]];
+        if ([self _previewSound]) {
+            NSString *path = [selectedAlias fullPath];
+            SoundFile *sf = [[SoundFileManager sharedSoundFileManager] soundFileFromPath: path];
+            if (sf != nil) {
+                [self selectItemAtIndex: [self indexOfItemWithRepresentedObject: sf]];
+            } else {
+                NSString *title = [[NSFileManager defaultManager] displayNameAtPath: path];
+                NSMenuItem *item = [[self menu] addItemWithTitle: title action: @selector(_aliasSelected:) keyEquivalent: @""];
+                [item setTarget: self];
+                [item setRepresentedObject: selectedAlias];
+                [item setImage: [[[NSWorkspace sharedWorkspace] iconForFile: path] bestFitImageForSize: NSMakeSize(16, 16)]];
+                [self selectItem: item];
+            }
+        }
+    }
+}
+
+- (void)_beepSelected:(NSMenuItem *)sender;
+{
+    [self setAlias: nil];
+    [self _previewSound];
+}
+
+- (void)_soundFileSelected:(NSMenuItem *)sender;
+{
+    [self setPath: [(SoundFile *)[sender representedObject] path]];
+    [self _previewSound];
+}
+
+- (void)_aliasSelected:(NSMenuItem *)sender;
+{
+    [self setAlias: [sender representedObject]];
+    [self _previewSound];
+}
+
+- (void)_invalidateSoundSelection;
+{
+    [self setAlias: nil];
+    [self selectItemAtIndex: 0];
+}
+
+- (IBAction)stopSoundPreview:(id)sender;
+{
+    [preview stop: self];
+}
+
+- (BOOL)_previewSound;
+{
+    [preview stop: self];
+    if (selectedAlias == nil) {
+        [preview setMovie: nil];
+        NSBeep();
+    } else {
+        NSMovie *movie = [[NSMovie alloc] initWithURL: [NSURL fileURLWithPath: [selectedAlias fullPath]] byReference: YES];
+        if ([movie hasAudio])
+            [preview setMovie: movie];
+        else {
+            [preview setMovie: nil];
+            if (movie == nil) {
+                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.");
+                [self _invalidateSoundSelection];
+                return NO;
+            }
+            if (![movie hasAudio] && ![movie hasVideo]) {
+                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]]);
+                [self _invalidateSoundSelection];
+                [movie release];
+                return NO;
+            }
+        }
+        [movie release];
+        [preview start: self];
+    }
+    return YES;
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRTableDelegate.h
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRTableDelegate.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRTableDelegate.h	(revision 34)
@@ -0,0 +1,21 @@
+//
+//  NJRTableDelegate.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sun Oct 27 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+
+
+@interface NJRTableDelegate : NSObject {
+    IBOutlet NSTableView *tableView;
+    NSTableColumn *sortingColumn;
+    NSString *sortingKey;
+    BOOL sortDescending;
+
+    id oData; // XXX ???
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRTableDelegate.m
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRTableDelegate.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRTableDelegate.m	(revision 34)
@@ -0,0 +1,176 @@
+//
+//  NJRTableDelegate.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sun Oct 27 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import "NJRTableDelegate.h"
+#import "NSTableView-NJRExtensions.h"
+
+#pragma mark sorting support
+
+typedef struct { NSString *key; BOOL descending; } SortContext;
+
+// Sort array of itemNums, by looking up the itemNum in the dictionary of dictionaries.
+// based on code of Ondra Cada <ocs@ocs.cz> on cocoa-dev list
+
+int ORDER_BY_CONTEXT(id left, id right, void *ctxt) {
+    SortContext *context = (SortContext *)ctxt;
+    int order = 0;
+    id key = context->key;
+    if (0 != key) {
+        id first, second;	// the actual objects to compare
+
+        if (context->descending) {
+            first  = [right objectForKey: key];
+            second = [left  objectForKey: key];
+        } else {
+            first  = [left  objectForKey: key];
+            second = [right objectForKey: key];
+        }
+
+        if ([first respondsToSelector: @selector(caseInsensitiveCompare:)]) {
+            order = [first caseInsensitiveCompare:second];
+        } else { // sort numbers or dates
+            order = [(NSNumber *)first compare:second];
+        }
+    }
+    return order;
+}
+
+@implementation NJRTableDelegate
+
+#pragma mark initialize-release
+
+- (void)dealloc
+{
+    [sortingColumn release];
+    [sortingKey release];
+    [super dealloc];
+}
+
+#pragma mark accessing
+
+- (void)setSortingColumn:(NSTableColumn *)inNewValue;
+{
+    [inNewValue retain];
+    [sortingColumn release];
+    sortingColumn = inNewValue;
+}
+
+- (void)setSortingKey:(NSString *)inNewValue;
+{
+    [inNewValue retain];
+    [sortingKey release];
+    sortingKey = inNewValue;
+}
+
+#pragma mark saving/restoring selection
+
+- (NSSet *)selectedItems;
+{
+    NSMutableSet *result = [NSMutableSet set];
+    NSEnumerator *e = [tableView selectedRowEnumerator];
+    NSNumber *rowNum;
+
+    while ( (rowNum = [e nextObject]) != nil) {
+        id item = [oData objectAtIndex: [rowNum intValue]];
+        [result addObject: item];
+    }
+    return result;
+}
+
+- (void)selectItems:(NSSet *)inSelectedItems;
+{
+    NSEnumerator *e = [inSelectedItems objectEnumerator];
+    id item;
+    int savedLastRow = 0;
+
+    [tableView deselectAll: nil];
+
+    while ( (item = [e nextObject]) != nil ) {
+        int row = [oData indexOfObjectIdenticalTo: item];
+        if (row != NSNotFound) {
+            [tableView selectRow: row byExtendingSelection: YES];
+            savedLastRow = row;
+        }
+    }
+    [tableView scrollRowToVisible: savedLastRow];
+}
+
+// ----------------------------------------------------------------------------------------
+// Sorting
+// ----------------------------------------------------------------------------------------
+
+- (void)sortData
+{
+    SortContext ctxt = { sortingKey, sortDescending };
+    NSSet *oldSelection = [self selectedItems];
+
+    // sort the NSMutableArray
+    [oData sortUsingFunction: ORDER_BY_CONTEXT context: &ctxt];
+
+    [tableView reloadData];
+    [self selectItems: oldSelection];
+}
+
+- (void)sortByColumn:(NSTableColumn *)inTableColumn;
+{
+    if (sortingColumn == inTableColumn) {
+        // User clicked same column, change sort order
+        sortDescending = !sortDescending;
+        // Possible optimization: Don't actually re-sort if you just change the sorting direction;
+        // instead, just display either the nth item or the (count-1-n)th item depending on ascending/descending.)
+    } else {
+        // User clicked new column, change old/new column headers,
+        // save new sorting column, and re-sort the array.
+        sortDescending = NO;
+        if (nil != sortingColumn) {
+            [tableView setIndicatorImage: nil inTableColumn: sortingColumn];
+        }
+        [self setSortingKey: [inTableColumn identifier]];
+        [self setSortingColumn: inTableColumn];
+        [tableView setHighlightedTableColumn: inTableColumn];
+    }
+    [tableView setIndicatorImage: (sortDescending ? [NSTableView descendingSortIndicator] : [NSTableView ascendingSortIndicator]) inTableColumn: inTableColumn];
+    // Actually sort the data
+    [self sortData];
+}
+
+//	Sort by whatever column was clicked upon
+- (void)tableView:(NSTableView*)aTableView didClickTableColumn:(NSTableColumn *)inTableColumn
+{
+    [[tableView window] makeFirstResponder: aTableView]; // help make this tableView be first responder
+    [self sortByColumn:inTableColumn];
+}
+
+// ----------------------------------------------------------------------------------------
+// Alphabetic Type Ahead
+// ----------------------------------------------------------------------------------------
+
+- (void) typeAheadString:(NSString *)inString;
+{
+    // This general sample looks for a highlighted column, presuming that is that column we are sorted by, and uses that as the lookup key.
+    NSTableColumn *col = [tableView highlightedTableColumn];
+    if (nil != col) {
+        NSString *key = [col identifier];
+        int i;
+        for ( i = 0 ; i < [oData count] ; i++ ) {
+            NSDictionary *rowDict = [oData objectAtIndex:i];
+            NSString *compareTo = [rowDict objectForKey:key];
+            NSComparisonResult order = [inString caseInsensitiveCompare:compareTo];
+            if (order != NSOrderedDescending) break;
+        }
+        // Make sure we're not overflowing the row count.
+        if (i >= [oData count]) {
+            i = [oData count] - 1;
+        }
+        // Now select row i -- either the one we found, or the last row if not found.
+        [tableView selectRow:i byExtendingSelection:NO];
+        [tableView scrollRowToVisible:i];
+    }
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRVoicePopUpButton.h
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRVoicePopUpButton.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRVoicePopUpButton.h	(revision 34)
@@ -0,0 +1,28 @@
+//
+//  NJRVoicePopUpButton.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+#import "SUSpeaker.h"
+
+@interface NJRVoicePopUpButton : NSPopUpButton {
+    id _delegate;
+    SUSpeaker *_speaker;
+}
+
+- (void)setDelegate:(id)delegate;
+- (id)delegate;
+
+- (IBAction)stopVoicePreview:(id)sender;
+
+@end
+
+@interface NSObject (NJRVoicePopUpButtonDelegate)
+
+- (NSString *)voicePopUpButton:(NJRVoicePopUpButton *)sender previewStringForVoice:(NSString *)voice;
+
+@end
Index: /trunk/Cocoa/Pester/Source/NJRVoicePopUpButton.m
===================================================================
--- /trunk/Cocoa/Pester/Source/NJRVoicePopUpButton.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NJRVoicePopUpButton.m	(revision 34)
@@ -0,0 +1,125 @@
+//
+//  NJRVoicePopUpButton.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import "NJRVoicePopUpButton.h"
+
+
+@implementation NJRVoicePopUpButton
+
+- (void)_refreshVoiceList;
+{
+    NSMenu *menu;
+    NSMenuItem *item;
+    NSArray *voiceNames = [SUSpeaker voiceNames];
+
+    [self removeAllItems];
+    menu = [self menu];
+    [menu setAutoenablesItems: NO];
+    // XXX would be more elegant with surrogate support like my font popup menu
+    item = [menu addItemWithTitle: @"ÇunknownÈ" action: nil keyEquivalent: @""];
+    [item setEnabled: NO];
+    [menu addItem: [NSMenuItem separatorItem]];
+    if (voiceNames == nil || [voiceNames count] == 0) {
+        item = [menu addItemWithTitle: @"CanÕt locate voices" action: nil keyEquivalent: @""];
+        [item setEnabled: NO];
+    } else {
+        NSEnumerator *e = [voiceNames objectEnumerator];
+        NSString *voiceName;
+        while ( (voiceName = [e nextObject]) != nil) {
+            item = [menu addItemWithTitle: voiceName action: @selector(_previewVoice) keyEquivalent: @""];
+            [item setTarget: self];
+        }
+    }
+    if (_speaker == nil) [self selectItemWithTitle: [SUSpeaker defaultVoice]];
+}
+
+- (id)initWithFrame:(NSRect)frame;
+{
+    if ( (self = [super initWithFrame: frame]) != nil) {
+        [self _refreshVoiceList];
+    }
+    return self;
+}
+
+- (id)initWithCoder:(NSCoder *)coder;
+{
+    if ( (self = [super initWithCoder: coder]) != nil) {
+        [self _refreshVoiceList];
+    }
+    return self;
+}
+
+- (SUSpeaker *)_speaker;
+{
+    if (_speaker == nil) _speaker = [[SUSpeaker alloc] init];
+    return _speaker;
+}
+
+- (void)_invalidateVoiceSelection;
+{
+    [self _refreshVoiceList];
+    [self selectItemAtIndex: 0];
+}
+
+- (void)_previewVoice;
+{
+    NSString *voiceName = [self titleOfSelectedItem];
+    NSString *previewString = nil;
+    VoiceSpec voice;
+    OSStatus err = noErr;
+    VoiceDescription info;
+    short voiceIndex = [[SUSpeaker voiceNames] indexOfObject: voiceName] + 1;
+
+    [_speaker stopSpeaking];
+
+    if ( (err = GetIndVoice(voiceIndex, &voice)) != noErr) {
+        NSBeginAlertSheet(@"Voice not available", @"OK", nil, nil, [self window], nil, nil, nil, nil, @"The voice Ò%@Ó you selected could not be used.  An error of type %ld occurred while attempting to retrieve voice information.", voiceName, err);
+        [self _invalidateVoiceSelection];
+        return;
+    }
+
+    if (_delegate != nil && [_delegate respondsToSelector: @selector(voicePopUpButton:previewStringForVoice:)]) {
+        previewString = [_delegate voicePopUpButton: self previewStringForVoice: voiceName];
+    }
+
+    if (previewString == nil) {
+        err = GetVoiceDescription(&voice, &info, sizeof(info));
+        if (err != noErr || info.comment[0] == 0)
+            previewString = voiceName;
+        else {
+            previewString = (NSString *)CFStringCreateWithPascalString(NULL, info.comment, kCFStringEncodingMacRoman);
+            [previewString autorelease];
+        }
+    }
+
+    [[self _speaker] setVoice: voiceIndex];
+    [_speaker speakText: previewString];
+}
+
+- (void)dealloc;
+{
+    [_speaker release];
+    [super dealloc];
+}
+
+- (IBAction)stopVoicePreview:(id)sender;
+{
+    [_speaker stopSpeaking];
+}
+
+- (void)setDelegate:(id)delegate;
+{
+    _delegate = delegate;
+}
+
+- (id)delegate;
+{
+    return _delegate;
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NSImage-NJRExtensions.h
===================================================================
--- /trunk/Cocoa/Pester/Source/NSImage-NJRExtensions.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NSImage-NJRExtensions.h	(revision 34)
@@ -0,0 +1,16 @@
+//
+//  NSImage-NJRExtensions.h
+//  Pester
+//
+//  Created by Nicholas Riley on Mon Oct 28 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+
+
+@interface NSImage (NJRExtensions)
+
+- (NSImage *)bestFitImageForSize:(NSSize)imageSize;
+
+@end
Index: /trunk/Cocoa/Pester/Source/NSImage-NJRExtensions.m
===================================================================
--- /trunk/Cocoa/Pester/Source/NSImage-NJRExtensions.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NSImage-NJRExtensions.m	(revision 34)
@@ -0,0 +1,54 @@
+//
+//  NSImage-NJRExtensions.m
+//  Pester
+//
+//  Created by Nicholas Riley on Mon Oct 28 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import "NSImage-NJRExtensions.h"
+
+
+@implementation NSImage (NJRExtensions)
+
+- (NSImage *)bestFitImageForSize:(NSSize)imageSize;
+{
+    NSSize repSize;
+    NSArray *imageReps;
+    int i, repCount;
+    NSImageRep *preferredRep, *rep;
+    imageReps = [self representations];
+    repCount = [imageReps count];
+    preferredRep = [imageReps objectAtIndex: 0];
+    for (i = 1 ; i < repCount ; i++) {
+        rep = [imageReps objectAtIndex: i];
+        repSize = [rep size];
+        if (repSize.width == imageSize.width && repSize.height == imageSize.height) {
+            preferredRep = rep;
+            break;
+        }
+        if (repSize.width >= imageSize.width || repSize.height >= imageSize.height) {
+            // pick the smallest of the larger representations
+            if (repSize.width <= [preferredRep size].width ||
+                repSize.height <= [preferredRep size].height) preferredRep = rep;
+        } else {
+            // or the largest of the smaller representations
+            if (([preferredRep size].width > imageSize.width ||
+                    [preferredRep size].height > imageSize.height) ||
+                // (assuming that the previous preferred rep was smaller, too)
+                // XXX fix this in HostLauncher too
+                (repSize.width >= [preferredRep size].width ||
+                    repSize.height >= [preferredRep size].height)) preferredRep = rep;
+        }
+    }
+    // Begin workaround code for bug in OS X 10.1 (removeRepresentation: has no effect)
+    if (repCount > 1) {
+        NSImage *image = [[[NSImage alloc] initWithSize: [preferredRep size]] autorelease];
+        [image addRepresentation: preferredRep];
+        return image;
+    } else {
+        return self;
+    }
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NSMovie-NJRExtensions.h
===================================================================
--- /trunk/Cocoa/Pester/Source/NSMovie-NJRExtensions.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NSMovie-NJRExtensions.h	(revision 34)
@@ -0,0 +1,17 @@
+//
+//  NSMovie-NJRExtensions.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+
+
+@interface NSMovie (NJRExtensions)
+
+- (BOOL)hasAudio;
+- (BOOL)hasVideo;
+
+@end
Index: /trunk/Cocoa/Pester/Source/NSMovie-NJRExtensions.m
===================================================================
--- /trunk/Cocoa/Pester/Source/NSMovie-NJRExtensions.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/NSMovie-NJRExtensions.m	(revision 34)
@@ -0,0 +1,24 @@
+//
+//  NSMovie-NJRExtensions.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import "NSMovie-NJRExtensions.h"
+#import <QuickTime/Movies.h>
+
+@implementation NSMovie (NJRExtensions)
+
+- (BOOL)hasAudio;
+{
+    return (NULL != GetMovieIndTrackType((Movie)[self QTMovie], 1, AudioMediaCharacteristic, movieTrackCharacteristic | movieTrackEnabledOnly));
+}
+
+- (BOOL)hasVideo;
+{
+    return (NULL != GetMovieIndTrackType((Movie)[self QTMovie], 1, VisualMediaCharacteristic, movieTrackCharacteristic | movieTrackEnabledOnly));
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/NSTableView-NJRExtensions.h
===================================================================
--- /trunk/Cocoa/Pester/Source/NSTableView-NJRExtensions.h	(revision 33)
+++ /trunk/Cocoa/Pester/Source/NSTableView-NJRExtensions.h	(revision 34)
@@ -1,16 +1,21 @@
 //
 //  NSTableView-NJRExtensions.h
-//  HostLauncher
+//  Pester
 //
-//  Created by Nicholas Riley on Mon Apr 22 2002.
+//  Created by Nicholas Riley on Sun Oct 27 2002.
 //  Copyright (c) 2002 Nicholas Riley. All rights reserved.
 //
 
-#import <Cocoa/Cocoa.h>
+#import <AppKit/AppKit.h>
 
 
 @interface NSTableView (NJRExtensions)
 
++ (NSImage *)ascendingSortIndicator;
++ (NSImage *)descendingSortIndicator;
+
 - (float)cellHeight;
 
+- (NSArray *)selectedRowIndices;
+
 @end
Index: /trunk/Cocoa/Pester/Source/NSTableView-NJRExtensions.m
===================================================================
--- /trunk/Cocoa/Pester/Source/NSTableView-NJRExtensions.m	(revision 33)
+++ /trunk/Cocoa/Pester/Source/NSTableView-NJRExtensions.m	(revision 34)
@@ -1,7 +1,7 @@
 //
 //  NSTableView-NJRExtensions.m
-//  HostLauncher
+//  Pester
 //
-//  Created by Nicholas Riley on Mon Apr 22 2002.
+//  Created by Nicholas Riley on Sun Oct 27 2002.
 //  Copyright (c) 2002 Nicholas Riley. All rights reserved.
 //
@@ -10,5 +10,39 @@
 
 
+@interface NSTableView (PumaPrivate)
+// Declarations of 10.1 private methods, just to make the compiler happy.
++ (id) _defaultTableHeaderReverseSortImage;
++ (id) _defaultTableHeaderSortImage;
+@end
+
 @implementation NSTableView (NJRExtensions)
+
++ (NSImage *)ascendingSortIndicator;
+{
+    NSImage *result = [NSImage imageNamed: @"NSAscendingSortIndicator"];
+    if (result == nil && [[NSTableView class] respondsToSelector: @selector(_defaultTableHeaderSortImage)])
+        result = [NSTableView _defaultTableHeaderSortImage];
+    return result;
+}
+
++ (NSImage *)descendingSortIndicator;
+{
+    NSImage *result = [NSImage imageNamed:@"NSDescendingSortIndicator"];
+    if (result == nil && [[NSTableView class] respondsToSelector:@selector(_defaultTableHeaderReverseSortImage)])
+        result = [NSTableView _defaultTableHeaderReverseSortImage];
+    return result;
+}
+
+- (NSArray *)selectedRowIndices;
+{
+    NSEnumerator *theEnum = [self selectedRowEnumerator];
+    NSNumber *rowNumber;
+    NSMutableArray *rowNumberArray = [NSMutableArray arrayWithCapacity: [self numberOfSelectedRows]];
+
+    while (nil != (rowNumber = [theEnum nextObject]) )
+        [rowNumberArray addObject: rowNumber];
+
+    return rowNumberArray;
+}
 
 - (float)cellHeight;
Index: /trunk/Cocoa/Pester/Source/PSAlarm.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarm.h	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarm.h	(revision 34)
@@ -16,4 +16,8 @@
 extern NSString * const PSAlarmTimerExpiredNotification;
 
+// XXX figure out how to support reading old alarms
+
+@class PSAlert;
+
 @interface PSAlarm : NSObject <NSCoding> {
     PSAlarmType alarmType;
@@ -23,4 +27,5 @@
     NSString *invalidMessage;
     NSTimer *timer;
+    NSMutableArray *alerts;
 }
 
@@ -29,8 +34,11 @@
 - (void)setForDate:(NSDate *)date atTime:(NSDate *)time;
 - (void)setMessage:(NSString *)aMessage;
+- (void)addAlert:(PSAlert *)alert;
+- (void)removeAlerts;
 
 - (NSCalendarDate *)date;
 - (NSTimeInterval)interval;
 - (NSString *)message;
+- (NSArray *)alerts;
 
 - (NSString *)shortDateString;
Index: /trunk/Cocoa/Pester/Source/PSAlarm.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarm.m	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarm.m	(revision 34)
@@ -8,7 +8,10 @@
 
 #import "PSAlarm.h"
+#import "PSAlert.h"
 
 NSString * const PSAlarmTimerSetNotification = @"PSAlarmTimerSetNotification";
 NSString * const PSAlarmTimerExpiredNotification = @"PSAlarmTimerExpiredNotification";
+
+// XXX need to reset pending alarms after sleep, they "freeze" and never expire.
 
 @implementation PSAlarm
@@ -22,4 +25,5 @@
     [invalidMessage release]; invalidMessage = nil;
     [timer invalidate]; [timer release]; timer = nil;
+    [alerts release]; alerts = nil;
     [super dealloc];
 }
@@ -224,4 +228,20 @@
 {
     return [[self date] compare: [otherAlarm date]];
+}
+
+- (void)addAlert:(PSAlert *)alert;
+{
+    if (alerts == nil) alerts = [[NSMutableArray alloc] initWithCapacity: 4];
+    [alerts addObject: alert];
+}
+
+- (void)removeAlerts;
+{
+    [alerts removeAllObjects];
+}
+
+- (NSArray *)alerts;
+{
+    return [[alerts copy] autorelease];
 }
 
Index: /trunk/Cocoa/Pester/Source/PSAlarmAlertController.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarmAlertController.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSAlarmAlertController.h	(revision 34)
@@ -0,0 +1,24 @@
+//
+//  PSAlarmAlertController.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+#import "PSAlarm.h"
+
+extern NSString * const PSAlarmAlertStopNotification;
+
+@interface PSAlarmAlertController : NSObject {
+
+}
+
++ (PSAlarmAlertController *)controllerWithTimerExpiredNotification:(NSNotification *)notification;
+
++ (IBAction)stopAlerts:(id)sender;
+
+- (id)initWithAlarm:(PSAlarm *)alarm;
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSAlarmAlertController.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarmAlertController.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSAlarmAlertController.m	(revision 34)
@@ -0,0 +1,35 @@
+//
+//  PSAlarmAlertController.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSAlarmAlertController.h"
+
+NSString * const PSAlarmAlertStopNotification = @"PSAlarmAlertStopNotification";
+
+@implementation PSAlarmAlertController
+
++ (PSAlarmAlertController *)controllerWithTimerExpiredNotification:(NSNotification *)notification;
+{
+    return [[[self alloc] initWithAlarm: [notification object]] autorelease];
+}
+
++ (IBAction)stopAlerts:(id)sender;
+{
+    [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmAlertStopNotification object: nil];
+}
+
+- (id)initWithAlarm:(PSAlarm *)alarm;
+{
+    if ( (self = [super init]) != nil) {
+        [NSApp activateIgnoringOtherApps: YES];
+        [[alarm alerts] makeObjectsPerformSelector: @selector(triggerForAlarm:)
+                                        withObject: alarm];
+    }
+    return self;
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSAlarmNotifierController.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarmNotifierController.h	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarmNotifierController.h	(revision 34)
@@ -16,6 +16,4 @@
 }
 
-+ (PSAlarmNotifierController *)controllerWithTimerExpiredNotification:(NSNotification *)notification;
-
 - (id)initWithAlarm:(PSAlarm *)alarm;
 
Index: /trunk/Cocoa/Pester/Source/PSAlarmNotifierController.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarmNotifierController.m	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarmNotifierController.m	(revision 34)
@@ -8,4 +8,5 @@
 
 #import "PSAlarmNotifierController.h"
+#import "PSAlarmAlertController.h"
 #import "PSAlarm.h"
 
@@ -13,9 +14,4 @@
 
 // XXX should use NSNonactivatingPanelMask on 10.2
-
-+ (PSAlarmNotifierController *)controllerWithTimerExpiredNotification:(NSNotification *)notification;
-{
-    return [[self alloc] initWithAlarm: [notification object]];
-}
 
 - (id)initWithAlarm:(PSAlarm *)alarm;
@@ -25,10 +21,6 @@
         [messageField setStringValue: [alarm message]];
         [dateField setObjectValue: [alarm date]];
-        [NSApp requestUserAttention: NSInformationalRequest];
-        [NSApp activateIgnoringOtherApps: YES];
         [[self window] makeKeyAndOrderFront: nil];
         [[self window] orderFrontRegardless];
-        [NSApp cancelUserAttentionRequest: NSInformationalRequest];
-        NSBeep();
     }
     return self;
@@ -37,4 +29,5 @@
 - (IBAction)close:(id)sender;
 {
+    [PSAlarmAlertController stopAlerts: sender];
     [self close];
 }
Index: /trunk/Cocoa/Pester/Source/PSAlarmSetController.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarmSetController.h	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarmSetController.h	(revision 34)
@@ -10,4 +10,8 @@
 #import "PSAlarm.h"
 
+@class NJRFSObjectSelector;
+@class NJRSoundPopUpButton;
+@class NJRVoicePopUpButton;
+
 @interface PSAlarmSetController : NSWindowController {
     IBOutlet NSTextField *messageField;
@@ -15,7 +19,20 @@
     IBOutlet NSTextField *timeInterval;
     IBOutlet NSPopUpButton *timeIntervalUnits;
+    IBOutlet NSButton *timeIntervalRepeats;
     IBOutlet NSTextField *timeOfDay;
     IBOutlet NSTextField *timeDate;
     IBOutlet NSPopUpButton *timeDateCompletions; // XXX should go away when bug preventing both formatters and popup menus from existing is fixed
+    IBOutlet NSButtonCell *displayMessage;
+    IBOutlet NSButton *bounceDockIcon;
+    IBOutlet NSButtonCell *playSound;
+    IBOutlet NJRSoundPopUpButton *sound;
+    IBOutlet NSTextField *soundRepetitions;
+    IBOutlet NSStepper *soundRepetitionStepper;
+    IBOutlet NSTextField *soundRepetitionsLabel;
+    IBOutlet NSButtonCell *doScript;
+    IBOutlet NJRFSObjectSelector *script;
+    IBOutlet NSButtonCell *doSpeak;
+    IBOutlet NJRVoicePopUpButton *voice;
+    IBOutlet NSButton *scriptSelectButton;
     IBOutlet NSTextField *timeSummary;
     IBOutlet NSButton *setButton;
@@ -29,5 +46,10 @@
 - (IBAction)dateCompleted:(NSPopUpButton *)sender;
 - (IBAction)inAtChanged:(id)sender;
+- (IBAction)playSoundChanged:(id)sender;
+- (IBAction)doScriptChanged:(id)sender;
+- (IBAction)doSpeakChanged:(id)sender;
 - (IBAction)setAlarm:(NSButton *)sender;
 
+- (IBAction)silence:(id)sender;
+
 @end
Index: /trunk/Cocoa/Pester/Source/PSAlarmSetController.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarmSetController.m	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarmSetController.m	(revision 34)
@@ -8,6 +8,17 @@
 
 #import "PSAlarmSetController.h"
-#import "PSAlarmNotifierController.h"
+#import "PSAlarmAlertController.h"
 #import "NJRDateFormatter.h"
+#import "NJRFSObjectSelector.h"
+#import "NJRSoundPopUpButton.h"
+#import "NJRVoicePopUpButton.h"
+#import <Carbon/Carbon.h>
+
+#import "PSDockBounceAlert.h"
+#import "PSScriptAlert.h"
+#import "PSNotifierAlert.h"
+#import "PSBeepAlert.h"
+#import "PSMovieAlert.h"
+#import "PSSpeechAlert.h"
 
 /* Bugs to file:
@@ -48,4 +59,10 @@
     [[self window] center];
     [self inAtChanged: nil];
+    [self playSoundChanged: nil];
+    [self doScriptChanged: nil];
+    [self doSpeakChanged: nil];
+    [script setFileTypes: [NSArray arrayWithObjects: @"applescript", @"script", NSFileTypeForHFSTypeCode(kOSAFileType), NSFileTypeForHFSTypeCode('TEXT'), nil]];
+    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(silence:) name: PSAlarmAlertStopNotification object: nil];
+    [voice setDelegate: self];
     [[self window] makeKeyAndOrderFront: nil];
 }
@@ -131,4 +148,5 @@
     [timeInterval setEnabled: isInterval];
     [timeIntervalUnits setEnabled: isInterval];
+    [timeIntervalRepeats setEnabled: isInterval];
     [timeOfDay setEnabled: !isInterval];
     [timeDate setEnabled: !isInterval];
@@ -140,4 +158,34 @@
 }
 
+- (IBAction)playSoundChanged:(id)sender;
+{
+    BOOL playSoundSelected = [playSound intValue];
+    [sound setEnabled: playSoundSelected];
+    [soundRepetitions setEnabled: playSoundSelected];
+    [soundRepetitionStepper setEnabled: playSoundSelected];
+    [soundRepetitionsLabel setTextColor: playSoundSelected ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]];
+    if (playSoundSelected && sender != nil)
+        [[self window] makeFirstResponder: sound];
+}
+
+// XXX should check the 'Do script:' button when someone drops a script on the button
+
+- (IBAction)doScriptChanged:(id)sender;
+{
+    BOOL doScriptSelected = [doScript intValue];
+    [script setEnabled: doScriptSelected];
+    [scriptSelectButton setEnabled: doScriptSelected];
+    if (doScriptSelected && sender != nil)
+        [[self window] makeFirstResponder: scriptSelectButton];
+}
+
+- (IBAction)doSpeakChanged:(id)sender;
+{
+    BOOL doSpeakSelected = [doSpeak intValue];
+    [voice setEnabled: doSpeakSelected];
+    if (doSpeakSelected && sender != nil)
+        [[self window] makeFirstResponder: voice];
+}
+
 - (IBAction)dateCompleted:(NSPopUpButton *)sender;
 {
@@ -159,9 +207,5 @@
 - (IBAction)setAlarm:(NSButton *)sender;
 {
-    PSAlarmNotifierController *notifier = [PSAlarmNotifierController alloc];
-    if (notifier == nil) {
-        [self setStatus: @"Unable to set alarm."];
-        return;
-    }
+    // set alarm
     [self setAlarmDateAndInterval: sender];
     [alarm setMessage: [messageField stringValue]];
@@ -170,8 +214,44 @@
         return;
     }
+
+    [alarm removeAlerts];
+    // dock bounce alert
+    if ([bounceDockIcon state] == NSOnState)
+        [alarm addAlert: [PSDockBounceAlert alert]];
+    // script alert
+    if ([doScript intValue]) {
+        BDAlias *scriptFileAlias = [script alias];
+        if (scriptFileAlias == nil) {
+            [self setStatus: @"Unable to set script alert (no script specified?)"];
+            return;
+        }
+        [alarm addAlert: [PSScriptAlert alertWithScriptFileAlias: scriptFileAlias]];
+    }
+    // notifier alert
+    if ([displayMessage intValue])
+        [alarm addAlert: [PSNotifierAlert alert]];
+    // sound alerts
+    if ([playSound intValue]) {
+        BDAlias *soundAlias = [sound selectedAlias];
+        unsigned short numReps = [soundRepetitions intValue];
+        if (soundAlias == nil) // beep alert
+            [alarm addAlert: [PSBeepAlert alertWithRepetitions: numReps]];
+        else // movie alert
+            [alarm addAlert: [PSMovieAlert alertWithMovieFileAlias: soundAlias repetitions: numReps]];
+    }
+    // speech alert
+    if ([doSpeak intValue])
+        [alarm addAlert: [PSSpeechAlert alertWithVoice: [voice titleOfSelectedItem]]];
+    
     [self setStatus: [[alarm date] descriptionWithCalendarFormat: @"Alarm set for %x at %X" timeZone: nil locale: nil]];
     [[self window] close];
     [alarm release];
     alarm = [[PSAlarm alloc] init];
+}
+
+- (IBAction)silence:(id)sender;
+{
+    [sound stopSoundPreview: self];
+    [voice stopVoicePreview: self];
 }
 
@@ -206,4 +286,5 @@
 {
     // NSLog(@"stopping update timer");
+    [self silence: nil];
     [self _stopUpdateTimer];
 }
@@ -222,2 +303,11 @@
 
 @end
+
+@implementation PSAlarmSetController (NJRVoicePopUpButtonDelegate)
+
+- (NSString *)voicePopUpButton:(NJRVoicePopUpButton *)sender previewStringForVoice:(NSString *)voice;
+{
+    return [messageField stringValue];
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSAlarms.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarms.h	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarms.h	(revision 34)
@@ -4,5 +4,5 @@
 //
 //  Created by Nicholas Riley on Fri Oct 11 2002.
-//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
 //
 
Index: /trunk/Cocoa/Pester/Source/PSAlarmsController.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlarmsController.m	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSAlarmsController.m	(revision 34)
@@ -122,3 +122,2 @@
 
 @end
-
Index: /trunk/Cocoa/Pester/Source/PSAlert.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlert.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSAlert.h	(revision 34)
@@ -0,0 +1,22 @@
+//
+//  PSAlert.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "PSAlarm.h"
+
+@interface PSAlert : NSObject {
+
+}
+
+// XXX need archiving support
+
+// subclasses should implement these methods
++ (PSAlert *)alert;
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSAlert.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSAlert.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSAlert.m	(revision 34)
@@ -0,0 +1,25 @@
+//
+//  PSAlert.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSAlert.h"
+
+
+@implementation PSAlert
+
++ (PSAlert *)alert;
+{
+    NSAssert(NO, @"Class is abstract");
+    return nil;
+}
+
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+{
+    NSAssert(NO, @"Class is abstract");
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSApplication.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSApplication.h	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSApplication.h	(revision 34)
@@ -22,4 +22,5 @@
 - (IBAction)orderFrontSetAlarmPanel:(id)sender;
 - (IBAction)orderFrontAlarmsPanel:(id)sender;
+- (IBAction)stopAlerts:(id)sender;
 
 @end
Index: /trunk/Cocoa/Pester/Source/PSApplication.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSApplication.m	(revision 33)
+++ /trunk/Cocoa/Pester/Source/PSApplication.m	(revision 34)
@@ -9,5 +9,5 @@
 #import "PSApplication.h"
 #import "PSAlarmSetController.h"
-#import "PSAlarmNotifierController.h"
+#import "PSAlarmAlertController.h"
 #import "PSAlarmsController.h"
 #import "PSAlarm.h"
@@ -19,5 +19,5 @@
 {
     appIconImage = [[NSImage imageNamed: @"NSApplicationIcon"] retain];
-    [[NSNotificationCenter defaultCenter] addObserver: [PSAlarmNotifierController class] selector: @selector(controllerWithTimerExpiredNotification:) name: PSAlarmTimerExpiredNotification object: nil];
+    [[NSNotificationCenter defaultCenter] addObserver: [PSAlarmAlertController class] selector: @selector(controllerWithTimerExpiredNotification:) name: PSAlarmTimerExpiredNotification object: nil];
     [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(nextAlarmDidChange:) name: PSAlarmsNextAlarmDidChangeNotification object: nil];
     [PSAlarms setUp];
@@ -30,4 +30,9 @@
     [[NSWorkspace sharedWorkspace] openFile: [[NSBundle mainBundle] pathForResource: @"Read Me" ofType: @"rtfd"]];
 }
+
+- (IBAction)stopAlerts:(id)sender;
+{
+    [PSAlarmAlertController stopAlerts: sender];
+}	
 
 - (IBAction)orderFrontSetAlarmPanel:(id)sender;
Index: /trunk/Cocoa/Pester/Source/PSBeepAlert.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSBeepAlert.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSBeepAlert.h	(revision 34)
@@ -0,0 +1,20 @@
+//
+//  PSBeepAlert.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSAlert.h"
+
+
+@interface PSBeepAlert : PSAlert {
+    unsigned short repetitions;
+    unsigned short repetitionsRemaining;    
+}
+
++ (PSBeepAlert *)alertWithRepetitions:(unsigned short)numReps;
+- (id)initWithRepetitions:(unsigned short)numReps;
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSBeepAlert.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSBeepAlert.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSBeepAlert.m	(revision 34)
@@ -0,0 +1,58 @@
+//
+//  PSBeepAlert.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+#import "PSBeepAlert.h"
+#import "PSAlarmAlertController.h"
+
+@implementation PSBeepAlert
+
++ (PSBeepAlert *)alertWithRepetitions:(unsigned short)numReps;
+{
+    return [[self alloc] initWithRepetitions: numReps];
+}
+
+- (id)initWithRepetitions:(unsigned short)numReps;
+{
+    if ( (self = [super init]) != nil) {
+        repetitions = numReps;
+    }
+    return self;
+}
+
+- (void)dealloc;
+{
+    [[NSNotificationCenter defaultCenter] removeObserver: self];
+    [super dealloc];
+}
+
+- (void)beep;
+{
+    NSBeep();
+    repetitionsRemaining--;
+    if (repetitionsRemaining == 0) {
+        [self release];
+        return;
+    }
+    [self performSelector: @selector(beep) withObject: nil afterDelay: 0.15 inModes: [NSArray arrayWithObject: NSDefaultRunLoopMode]];
+}
+
+- (void)_stopBeeping:(NSNotification *)notification;
+{
+    repetitionsRemaining = 1;
+}
+
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+{
+    repetitionsRemaining = repetitions;
+    [self retain];
+    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_stopBeeping:) name: PSAlarmAlertStopNotification object: nil];
+    [self beep];
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSDockBounceAlert.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSDockBounceAlert.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSDockBounceAlert.h	(revision 34)
@@ -0,0 +1,16 @@
+//
+//  PSDockBounceAlert.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSAlert.h"
+
+
+@interface PSDockBounceAlert : PSAlert {
+
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSDockBounceAlert.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSDockBounceAlert.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSDockBounceAlert.m	(revision 34)
@@ -0,0 +1,30 @@
+//
+//  PSDockBounceAlert.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+#import "PSDockBounceAlert.h"
+
+static PSDockBounceAlert *PSDockBounceAlertShared;
+
+@implementation PSDockBounceAlert
+
++ (PSAlert *)alert;
+{
+    if (PSDockBounceAlertShared == nil)
+        PSDockBounceAlertShared = [[PSDockBounceAlert alloc] init];
+    return PSDockBounceAlertShared;
+}
+
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+{
+    [NSApp requestUserAttention: NSInformationalRequest];
+    [NSApp activateIgnoringOtherApps: YES];
+    [NSApp cancelUserAttentionRequest: NSInformationalRequest];
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSMovieAlert.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSMovieAlert.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSMovieAlert.h	(revision 34)
@@ -0,0 +1,30 @@
+//
+//  PSMovieAlert.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+#import "PSAlert.h"
+#import "BDAlias.h"
+
+
+@interface PSMovieAlert : PSAlert {
+    BDAlias *alias;
+    NSMovie *movie;
+    unsigned short repetitions;
+    BOOL hasAudio;
+    BOOL hasVideo;
+}
+
++ (PSMovieAlert *)alertWithMovieFileAlias:(BDAlias *)anAlias repetitions:(unsigned short)numReps;
+
+- (id)initWithAlias:(BDAlias *)anAlias repetitions:(unsigned int) numReps;
+
+- (BOOL)hasVideo;
+- (NSMovie *)movie;
+- (unsigned short)repetitions;
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSMovieAlert.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSMovieAlert.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSMovieAlert.m	(revision 34)
@@ -0,0 +1,76 @@
+//
+//  PSMovieAlert.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <QuickTime/Movies.h>
+#import "PSMovieAlert.h"
+#import "PSMovieAlertController.h"
+#import "NSMovie-NJRExtensions.h"
+
+@implementation PSMovieAlert
+
++ (PSMovieAlert *)alertWithMovieFileAlias:(BDAlias *)anAlias repetitions:(unsigned short)numReps;
+{
+    return [[[self alloc] initWithAlias: anAlias repetitions: numReps] autorelease];
+}
+
+- (id)initWithAlias:(BDAlias *)anAlias repetitions:(unsigned int) numReps;
+{
+    if ( (self = [super init]) != nil) {
+        alias = [anAlias retain];
+        repetitions = numReps;
+        movie = [[NSMovie alloc] initWithURL: [NSURL fileURLWithPath: [anAlias fullPath]] byReference: YES];
+        if (movie == nil) {
+            [self release];
+            self = nil;
+        } else {
+            hasAudio = [movie hasAudio];
+            hasVideo = [movie hasVideo];
+            
+            if (!hasAudio && !hasVideo) {
+                [self release]; self = nil;
+            }
+        }
+    }
+    
+    return self;
+}
+
+- (BOOL)hasVideo;
+{
+    return hasVideo;
+}
+
+- (NSMovie *)movie;
+{
+    return movie;
+}
+
+- (unsigned short)repetitions;
+{
+    return repetitions;
+}
+
+- (void)dealloc;
+{
+    [alias release];
+    [movie release];
+    [super dealloc];
+}
+
+- (NSString *)description;
+{
+    return [NSString stringWithFormat: @"PSMovieAlert (%@%@): %@, repeats %hu times", hasAudio ? @"A" : @"", hasVideo ? @"V" : @"", [alias fullPath], repetitions];
+}
+
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+{
+    NSLog(@"%@", self);
+    [PSMovieAlertController controllerWithAlarm: alarm movieAlert: self];
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSMovieAlertController.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSMovieAlertController.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSMovieAlertController.h	(revision 34)
@@ -0,0 +1,25 @@
+//
+//  PSMovieAlertController.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import <AppKit/AppKit.h>
+
+@class PSAlarm;
+@class PSMovieAlert;
+
+@interface PSMovieAlertController : NSWindowController {
+    IBOutlet NSMovieView *movieView;
+    void *theMovie; /* Movie */
+    unsigned short repetitions;
+    unsigned short repetitionsRemaining;    
+}
+
++ (PSMovieAlertController *)controllerWithAlarm:(PSAlarm *)alarm movieAlert:(PSMovieAlert *)alert;
+
+- (id)initWithAlarm:(PSAlarm *)alarm movieAlert:(PSMovieAlert *)alert;
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSMovieAlertController.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSMovieAlertController.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSMovieAlertController.m	(revision 34)
@@ -0,0 +1,93 @@
+//
+//  PSMovieAlertController.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSAlarmAlertController.h"
+#import "PSMovieAlertController.h"
+#import "PSMovieAlert.h"
+#import <QuickTime/Movies.h>
+
+@implementation PSMovieAlertController
+
++ (PSMovieAlertController *)controllerWithAlarm:(PSAlarm *)alarm movieAlert:(PSMovieAlert *)alert;
+{
+    return [[self alloc] initWithAlarm: alarm movieAlert: alert];
+}
+
+- (void)play;
+{
+    NSTimeInterval delay;
+    if (repetitions == 0) return;
+    if (IsMovieDone((Movie)theMovie) || repetitionsRemaining == repetitions) {
+        if (repetitionsRemaining == 0) {
+            [self close];
+            return;
+        }
+        repetitionsRemaining--;
+        [movieView gotoBeginning: self];
+        [movieView start: self];
+    }
+    delay = (GetMovieDuration((Movie)theMovie) - GetMovieTime((Movie)theMovie, NULL)) / (double)GetMovieTimeScale((Movie)theMovie);
+    [self performSelector: @selector(play) withObject: nil afterDelay: delay inModes: [NSArray arrayWithObject: NSDefaultRunLoopMode]];
+}
+
+- (id)initWithAlarm:(PSAlarm *)alarm movieAlert:(PSMovieAlert *)alert;
+{
+    if ([self initWithWindowNibName: @"Movie alert"]) {
+        NSMovie *movie = [alert movie];
+        NSWindow *window = [self window]; // connect outlets
+        [movieView setMovie: movie];
+        theMovie = [movie QTMovie];
+        if ([alert hasVideo]) {
+            NSRect screenRect = [[window screen] visibleFrame];
+            float magnification = 1.0;
+            NSSize movieSize;
+            NSRect frame;
+            screenRect.size.height -= [window frame].size.height - [[window contentView] frame].size.height; // account for height of window frame
+            while (1) {
+                movieSize = [movieView sizeForMagnification: magnification];
+                if (movieSize.width > screenRect.size.width || movieSize.height > screenRect.size.height)
+                    magnification /= 2;
+                else
+                    break;
+            }
+            [window setContentSize: movieSize];
+            [window center];
+            frame = [window frame];
+            frame.origin.y -= 250; // appear below notifier window
+            if (frame.origin.y < screenRect.origin.y) frame.origin.y = screenRect.origin.y;
+            [window setFrame: frame display: NO];
+            [window setTitle: [alarm message]];
+            [[self window] orderFrontRegardless];
+        }
+        [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(close) name: PSAlarmAlertStopNotification object: nil];
+        repetitions = [alert repetitions];
+        repetitionsRemaining = repetitions;
+        [self play];
+    }
+    return self;
+}
+
+- (void)dealloc;
+{
+    [[NSNotificationCenter defaultCenter] removeObserver: self];
+    [super dealloc];
+}
+
+@end
+
+@implementation PSMovieAlertController (NSWindowNotifications)
+
+- (void)windowWillClose:(NSNotification *)notification;
+{
+    repetitions = 0;
+    [movieView stop: self];
+    [self release];
+    // note: there may still be a retained copy of this object until the runloop timer has let go of us at the end of the current movie playback cycle; donÕt worry about it.
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSNotifierAlert.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSNotifierAlert.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSNotifierAlert.h	(revision 34)
@@ -0,0 +1,16 @@
+//
+//  PSNotifierAlert.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSAlert.h"
+
+
+@interface PSNotifierAlert : PSAlert {
+
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSNotifierAlert.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSNotifierAlert.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSNotifierAlert.m	(revision 34)
@@ -0,0 +1,28 @@
+//
+//  PSNotifierAlert.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSNotifierAlert.h"
+#import "PSAlarmNotifierController.h"
+
+static PSNotifierAlert *PSNotifierAlertShared;
+
+@implementation PSNotifierAlert
+
++ (PSAlert *)alert;
+{
+    if (PSNotifierAlertShared == nil)
+        PSNotifierAlertShared = [[PSNotifierAlert alloc] init];
+    return PSNotifierAlertShared;
+}
+
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+{
+    [[PSAlarmNotifierController alloc] initWithAlarm: alarm];
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSScriptAlert.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSScriptAlert.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSScriptAlert.h	(revision 34)
@@ -0,0 +1,20 @@
+//
+//  PSScriptAlert.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSAlert.h"
+
+@class BDAlias;
+
+@interface PSScriptAlert : PSAlert {
+    BDAlias *alias;
+}
+
++ (PSScriptAlert *)alertWithScriptFileAlias:(BDAlias *)anAlias;
+- (id)initWithScriptFileAlias:(BDAlias *)anAlias;
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSScriptAlert.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSScriptAlert.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSScriptAlert.m	(revision 34)
@@ -0,0 +1,36 @@
+//
+//  PSScriptAlert.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
+//
+
+#import "PSScriptAlert.h"
+#import "BDAlias.h"
+#import "NDAppleScriptObject.h"
+
+@implementation PSScriptAlert
+
++ (PSScriptAlert *)alertWithScriptFileAlias:(BDAlias *)anAlias;
+{
+    return [[[self alloc] initWithScriptFileAlias: anAlias] autorelease];
+}
+
+- (id)initWithScriptFileAlias:(BDAlias *)anAlias;
+{
+    if ( (self = [super init]) != nil) {
+        alias = [anAlias retain];
+    }
+    return self;
+}
+
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+{
+    NDAppleScriptObject *as = [NDAppleScriptObject appleScriptObjectWithContentsOfFile: [alias fullPath]];
+
+    if (as != nil) {
+        [as execute];
+    }
+}
+@end
Index: /trunk/Cocoa/Pester/Source/PSSpeechAlert.h
===================================================================
--- /trunk/Cocoa/Pester/Source/PSSpeechAlert.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSSpeechAlert.h	(revision 34)
@@ -0,0 +1,21 @@
+//
+//  PSSpeechAlert.h
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import "PSAlert.h"
+#import "SUSpeaker.h"
+
+@interface PSSpeechAlert : PSAlert {
+    SUSpeaker *speaker;
+    NSString *voice;
+}
+
++ (PSSpeechAlert *)alertWithVoice:(NSString *)aVoice;
+
+- (id)initWithVoice:(NSString *)aVoice;
+
+@end
Index: /trunk/Cocoa/Pester/Source/PSSpeechAlert.m
===================================================================
--- /trunk/Cocoa/Pester/Source/PSSpeechAlert.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/PSSpeechAlert.m	(revision 34)
@@ -0,0 +1,62 @@
+//
+//  PSSpeechAlert.m
+//  Pester
+//
+//  Created by Nicholas Riley on Sat Oct 26 2002.
+//  Copyright (c) 2002 __MyCompanyName__. All rights reserved.
+//
+
+#import "PSSpeechAlert.h"
+#import "PSAlarmAlertController.h"
+
+@implementation PSSpeechAlert
+
++ (PSSpeechAlert *)alertWithVoice:(NSString *)aVoice;
+{
+    return [[[self alloc] initWithVoice: aVoice] autorelease];
+}
+
+- (id)initWithVoice:(NSString *)aVoice;
+{
+    if ( (self = [super init]) != nil) {
+        voice = aVoice;
+    }
+    return self;
+}
+
+- (void)dealloc;
+{
+    [[NSNotificationCenter defaultCenter] removeObserver: self];
+    [speaker release]; speaker = nil;
+    [voice release]; voice = nil;
+    [super dealloc];
+}
+
+- (void)_stopSpeaking:(NSNotification *)notification;
+{
+    [speaker stopSpeaking];
+    // don't release here, we'll still get the didFinishSpeaking: message as a delegate
+}
+
+- (void)triggerForAlarm:(PSAlarm *)alarm;
+{
+    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_stopSpeaking:) name: PSAlarmAlertStopNotification object: nil];
+    
+    if ( (speaker = [[SUSpeaker alloc] init]) == nil) return;
+    [speaker setDelegate: self];
+    [speaker setVoice: [[SUSpeaker voiceNames] indexOfObject: voice] + 1];
+    [speaker speakText: [alarm message]];
+    
+    [self retain];
+}
+
+@end
+
+@implementation PSSpeechAlert (SUSpeakerDelegate)
+
+- (void)didFinishSpeaking:(SUSpeaker*)speaker;
+{
+    [self release];
+}
+
+@end
Index: /trunk/Cocoa/Pester/Source/SUSpeaker.h
===================================================================
--- /trunk/Cocoa/Pester/Source/SUSpeaker.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/SUSpeaker.h	(revision 34)
@@ -0,0 +1,40 @@
+//
+//  SUSpeaker.h
+//  SpeechTest
+//
+//  Created by raf on Sun Jan 28 2001.
+//  Copyright (c) 2000 Raphael Sebbe. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <Carbon/Carbon.h>
+
+@interface SUSpeaker : NSObject 
+{
+    SpeechChannel _speechChannel;
+    id _delegate;
+    NSPort *_port;
+    
+    BOOL _usePort;
+    unsigned int _reserved1;
+    unsigned int _reserved2;
+}
+
++(NSArray*) voiceNames;
++ (NSString *)defaultVoice;
+
+-(void) setPitch:(float)pitch;
+-(void) setVoice:(int)index;
+-(void) speakText:(NSString*)text;
+-(void) stopSpeaking;
+
+-(void) setDelegate:(id)delegate;
+-(id) delegate;
+
+@end
+
+
+@interface NSObject (SUSpeakerDelegate)
+-(void) didFinishSpeaking:(SUSpeaker*)speaker;
+-(void) willSpeakWord:(SUSpeaker*)speaker at:(int)where length:(int)length;
+@end
Index: /trunk/Cocoa/Pester/Source/SUSpeaker.m
===================================================================
--- /trunk/Cocoa/Pester/Source/SUSpeaker.m	(revision 34)
+++ /trunk/Cocoa/Pester/Source/SUSpeaker.m	(revision 34)
@@ -0,0 +1,296 @@
+//
+//  SUSpeaker.m
+//  SpeechTest
+//
+//  Created by raf on Sun Jan 28 2001.
+//  Copyright (c) 2000 Raphael Sebbe. All rights reserved.
+//
+
+#import "SUSpeaker.h"
+#include <unistd.h>
+#include <pthread.h>
+
+void MySpeechDoneCallback(SpeechChannel chan,SInt32 refCon);
+void MySpeechWordCallback (SpeechChannel chan, SInt32 refCon, UInt32 wordPos, 
+    UInt16 wordLen);
+
+@interface SUSpeaker (Private)
+-(void) setCallbacks;
+-(NSPort*) port;
+-(void) setReserved1:(unsigned int)r;
+-(void) setReserved2:(unsigned int)r;
+-(BOOL) usesPort;
+-(void) handleMessage:(unsigned)msgid;
+@end
+
+
+@implementation SUSpeaker
+
+/*"Returns the voice names in the same order as expected by setVoice:."*/
++(NSArray*) voiceNames
+{
+    NSMutableArray *voices = [NSMutableArray arrayWithCapacity:0];
+    short voiceCount;
+    OSErr error = noErr;
+    int voiceIndex;
+    
+    
+    
+    error = CountVoices(&voiceCount);
+    
+    if(error != noErr) return voices;
+    //NSLog(@"hello : %d", voiceCount);
+    for(voiceIndex=0; voiceIndex<voiceCount; voiceIndex++)
+    {
+        VoiceSpec	voiceSpec;
+        VoiceDescription voiceDescription;
+        
+        error = GetIndVoice(voiceIndex+1, &voiceSpec);
+        if(error != noErr) return voices;
+        error = GetVoiceDescription( &voiceSpec, &voiceDescription, sizeof(voiceDescription));
+        if(error == noErr)
+        {
+            NSString *voiceName = [[[NSString alloc] initWithCString:
+                &(voiceDescription.name[1]) length:voiceDescription.name[0]] autorelease];
+            //NSLog(voiceName);
+            
+            [voices addObject:voiceName];
+        }
+        else return voices;
+    }
+    return voices;
+}
+
++ (NSString *)defaultVoice;
+{
+    OSStatus err = noErr;
+    VoiceDescription voiceDescription;
+
+    err = GetVoiceDescription(NULL, &voiceDescription, sizeof(voiceDescription));
+
+    return [[[NSString alloc] initWithCString: &(voiceDescription.name[1]) length:voiceDescription.name[0]] autorelease];
+}
+
+-init
+{
+    NSRunLoop *loop = [NSRunLoop currentRunLoop];
+    [super init];
+
+    // we have 2 options here : we use a port or we don't.
+    // using a port means delegate message are invoked from the main 
+    // thread (runloop in which this object is created), otherwise, those message 
+    // are asynchronous.
+    if(loop != nil) 
+    {
+        _port = [[NSPort port] retain];
+        // we use a port so that the speech manager callbacks can talk to the main thread.
+        // That way, we can safely access interface elements from the delegate methods
+        
+        [_port setDelegate:self];
+        [loop addPort:_port forMode:NSDefaultRunLoopMode];
+        _usePort = YES;
+    }
+    else _usePort = NO;
+    
+    NewSpeechChannel(NULL, &_speechChannel); // NULL voice is default voice
+    [self setCallbacks];
+    return self;
+}
+
+-(void) dealloc
+{
+
+    [_port release];
+    if(_speechChannel != NULL)
+    {
+        DisposeSpeechChannel(_speechChannel);
+    }
+    
+    [super dealloc];
+}
+
+
+/*"Sets the pitch. Pitch is given in Hertz and should be comprised between 80 and 500, depending on the voice. Note that extreme value can make you app crash..."*/ 
+-(void) setPitch:(float)pitch
+{
+    int fixedPitch;
+    
+    
+    pitch = (pitch-90.0)/(300.0-90.0)*(65.0 - 30.0) + 30.0;
+    /* I don't know what Apple means with pitch between 30 and 65, so I convert that range to [90, 300]. I did not test frequencies correspond, though.*/
+    
+    fixedPitch = (int)pitch;
+    
+    fixedPitch = fixedPitch << 16; // fixed point
+    
+    if(_speechChannel != NULL)
+    {
+        SetSpeechPitch (_speechChannel, fixedPitch);
+    }
+}
+-(void) setVoice:(int)index
+{
+    VoiceSpec voice;
+    OSErr error = noErr;
+    
+    if(_speechChannel != NULL)
+    {
+        DisposeSpeechChannel(_speechChannel);
+        _speechChannel = NULL;
+    }
+    
+    error = GetIndVoice(index, &voice);
+    if(error == noErr)
+    {
+        NewSpeechChannel(&voice, &_speechChannel);
+        [self setCallbacks];
+    }
+}
+-(void) speakText:(NSString*)text
+{
+    //pid_t pid = getpid();
+    //pthread_t t = pthread_self();
+    //NSLog(@"pid : %d", t);
+
+    if(_speechChannel != NULL && text != nil)
+    {
+        //finished = NO;
+        SpeakText(_speechChannel, [text cString], [text length]);
+        //while(!finished) ;
+        //sleep(2);
+    }
+}
+-(void) stopSpeaking
+{
+    if(_speechChannel != NULL)
+    {
+        StopSpeech(_speechChannel);
+        if([_delegate respondsToSelector:@selector(didFinishSpeaking:)])
+        {
+            [_delegate didFinishSpeaking:self];
+        }
+    }
+}
+
+-(void) setDelegate:(id)delegate
+{
+    _delegate = delegate;
+}
+-(id) delegate
+{
+    return _delegate;
+}
+
+//--- Private ---
+
+-(void) setCallbacks
+{
+    if(_speechChannel != NULL)
+    {
+        SetSpeechInfo(_speechChannel, soSpeechDoneCallBack, &MySpeechDoneCallback);
+        SetSpeechInfo(_speechChannel, soWordCallBack, &MySpeechWordCallback);
+        SetSpeechInfo(_speechChannel, soRefCon, (const void*)self);
+    }
+}
+-(void) setReserved1:(unsigned int)r
+{
+    _reserved1 = r;
+}
+-(void) setReserved2:(unsigned int)r
+{
+    _reserved2 = r;
+}
+-(NSPort*) port
+{
+    return _port;
+}
+-(BOOL) usesPort
+{
+    return _usePort;
+}
+-(void) handleMessage:(unsigned)msgid
+{
+    if(msgid == 5)
+    {
+        if([_delegate respondsToSelector:@selector(willSpeakWord:at:length:)])
+        {
+            if(_reserved1 >= 0 && _reserved2 >= 0)
+                [_delegate willSpeakWord:self at:_reserved1 length:_reserved2];
+            else
+                [_delegate willSpeakWord:self at:0 length:0];
+        }
+    }
+    else if(msgid == 8)
+    {
+        if([_delegate respondsToSelector:@selector(didFinishSpeaking:)])
+        {
+            [_delegate didFinishSpeaking:self];
+        }
+    }
+}
+//--- NSPort delegate ---
+- (void)handlePortMessage:(NSPortMessage *)portMessage
+{
+    int msg = [portMessage msgid];
+    
+    [self handleMessage:msg];
+}
+
+@end
+
+void MySpeechDoneCallback(SpeechChannel chan,SInt32 refCon)
+{
+    SUSpeaker *speaker = (SUSpeaker*)refCon;
+    unsigned msg = 8;
+    //NSLog(@"Speech Done");
+    
+    if([speaker isKindOfClass:[SUSpeaker class]])
+    {
+        if([speaker usesPort])
+        {
+            NSPortMessage *message = [[NSPortMessage alloc] initWithSendPort:[speaker port]
+                receivePort:[speaker port] components:nil];
+        
+            [message setMsgid:msg];
+            [message sendBeforeDate:nil];
+            [message release];
+        }
+        else
+        {
+            // short-circuit port
+            [speaker handleMessage:msg];
+        }
+    } 
+}
+void MySpeechWordCallback(SpeechChannel chan, SInt32 refCon, UInt32 wordPos,UInt16 wordLen)
+{
+    SUSpeaker *speaker = (SUSpeaker*)refCon;
+    unsigned msg = 5;
+    //pid_t pid = getpid();
+    //pthread_t t = pthread_self();
+    //NSLog(@"pid : %d", t);
+    
+    
+    //NSLog(@"Word Done");
+    //while(1);
+    if([speaker isKindOfClass:[SUSpeaker class]])
+    {
+        [speaker setReserved1:wordPos];
+        [speaker setReserved2:wordLen];
+        
+        if([speaker usesPort])
+        {
+            NSPortMessage *message = [[NSPortMessage alloc] initWithSendPort:[speaker port]
+                receivePort:[speaker port] components:nil];
+        
+            [message setMsgid:msg];
+            [message sendBeforeDate:nil];
+            [message release];
+        }
+        else 
+        {
+            // short-circuit port
+            [speaker handleMessage:msg];
+        }
+    } 
+}
Index: /trunk/Cocoa/Pester/Source/SoundFileManager.h
===================================================================
--- /trunk/Cocoa/Pester/Source/SoundFileManager.h	(revision 34)
+++ /trunk/Cocoa/Pester/Source/SoundFileManager.h	(revision 34)
@@ -0,0 +1,56 @@
+/*
+ *     Generated by class-dump (version 2.1.5).
+ *
+ *     class-dump is Copyright (C) 1997, 1999, 2000, 2001 by Steve Nygard.
+ */
+
+/*
+ * File: /Volumes/Mac OS X/System/Library/PrivateFrameworks/MachineSettings.framework/Versions/A/MachineSettings
+ */
+
+// Note: dumped from Mac OS X 10.1.5; Mac OS X 10.2.1's SoundFileManager is identical.
+
+@class SoundFile;
+
+@interface SoundFileManager
+{
+    NSMutableArray *_soundFiles;
+    NSFileManager *_fileManager;
+}
+
++ (void)initialize;
++ (SoundFileManager *)sharedSoundFileManager;
+- (id)init;
+- (void)dealloc;
+- (void)findSoundFiles;
+- (void)addSoundFilesFromDirectory:(NSString *)directory asType:(int)type;
+- (void)sortByType;
+- (void)sortByName;
+- (SoundFile *)soundFileAtIndex:(int)index;
+- (int)count;
+- (int)indexOfSoundFile:index;
+- (SoundFile *)soundFileFromPath:(NSString *)path;
+- (SoundFile *)soundFileWithName:(NSString *)name type:(int)type;
+
+@end
+
+@interface SoundFile
+{
+    int _type;
+    NSString *_name;
+}
+
+- (id)initWithName:(NSString *)name type:(int)type;
+- (void)dealloc;
+- (int)type;
+- (NSString *)name;
+- (NSString *)path;
+- (NSString *)pathWithoutExtension;
+- (void)setType:(int)type;
+- (void)setName:(NSString *)name;
+- (int)compareName:(SoundFile *)other;
+- (int)compareType:(SoundFile *)other;
+- (int)compareNameThenType:(SoundFile *)other;
+- (int)compareTypeThenName:(SoundFile *)other;
+
+@end
