Index: trunk/Cocoa/Pester/Source/NJRQTMediaPopUpButton.m
===================================================================
--- trunk/Cocoa/Pester/Source/NJRSoundPopUpButton.m	(revision 34)
+++ trunk/Cocoa/Pester/Source/NJRQTMediaPopUpButton.m	(revision 39)
@@ -12,6 +12,9 @@
 #import "NSImage-NJRExtensions.h"
 
+static const int NJRQTMediaPopUpButtonMaxRecentItems = 10;
+
 @interface NJRQTMediaPopUpButton (Private)
-- (BOOL)_previewSound;
+- (void)_setPath:(NSString *)path;
+- (BOOL)_validatePreview;
 @end
 
@@ -19,7 +22,71 @@
 
 // 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)
+// XXX don't add icons on Puma, they look like ass
+// XXX launch preview on a separate thread (if movies take too long to load, they inhibit the interface responsiveness)
+
+- (NSString *)_defaultKey;
+{
+    NSAssert([self tag] != 0, @"CanÕt track recently selected media for popup with tag 0: please set a tag");
+    return [NSString stringWithFormat: @"NJRQTMediaPopUpButtonMaxRecentItems tag %d", [self tag]];
+}
+
+- (void)_writeRecentMedia;
+{
+    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+    [defaults setObject: recentMediaAliasData forKey: [self _defaultKey]];
+    [defaults synchronize];
+}
+
+- (NSMenuItem *)_addRecentMediaAtPath:(NSString *)path withAlias:(BDAlias *)alias;
+{
+    NSString *title = [[NSFileManager defaultManager] displayNameAtPath: path];
+    NSMenu *menu = [self menu];
+    NSMenuItem *item = [menu insertItemWithTitle: title action: @selector(_aliasSelected:) keyEquivalent: @"" atIndex: [menu indexOfItem: otherItem] + 1];
+    [item setTarget: self];
+    [item setRepresentedObject: alias];
+    [item setImage: [[[NSWorkspace sharedWorkspace] iconForFile: path] bestFitImageForSize: NSMakeSize(16, 16)]];
+    [recentMediaAliasData addObject: [alias aliasData]];
+    if ([recentMediaAliasData count] > NJRQTMediaPopUpButtonMaxRecentItems) {
+        [menu removeItemAtIndex: [menu numberOfItems] - 1];
+        [recentMediaAliasData removeObjectAtIndex: 0];
+    }
+    return item;
+}
+
+- (void)_addRecentMediaFromAliasesData:(NSArray *)aliasesData;
+{
+    NSEnumerator *e = [aliasesData objectEnumerator];
+    NSData *aliasData;
+    BDAlias *alias;
+    while ( (aliasData = [e nextObject]) != nil) {
+        if ( (alias = [[BDAlias alloc] initWithData: aliasData]) != nil) {
+            [self _addRecentMediaAtPath: [alias fullPath] withAlias: alias];
+            [alias release];
+        }
+    }
+}
+
+- (void)_validateRecentMedia;
+{
+    NSEnumerator *e = [recentMediaAliasData objectEnumerator];
+    NSData *aliasData;
+    NSMenuItem *item;
+    BDAlias *itemAlias;
+    int otherIndex = [self indexOfItem: otherItem];
+    int aliasDataCount = [recentMediaAliasData count];
+    int lastItemIndex = [self numberOfItems] - 1;
+    int recentItemCount = lastItemIndex - otherIndex;
+    int recentItemIndex = otherIndex;
+    NSAssert2(recentItemCount == aliasDataCount, @"Counted %d recent menu items, %d of alias data", recentItemCount, aliasDataCount);
+    while ( (aliasData = [e nextObject]) != nil) {
+        recentItemIndex++;
+        item = [self itemAtIndex: recentItemIndex];
+        itemAlias = [item representedObject];
+        if (![itemAlias aliasDataIsEqual: aliasData])
+            NSLog(@"item %d %@: %@", recentItemIndex, [item title], [itemAlias fullPath]);
+        else
+            NSLog(@"ITEM %d %@: %@ != aliasData %@", recentItemIndex, [item title], [itemAlias fullPath], [[BDAlias aliasWithData: aliasData] fullPath]);
+    }
+}
 
 - (void)awakeFromNib;
@@ -29,5 +96,5 @@
     SoundFileManager *sfm = [SoundFileManager sharedSoundFileManager];
     int soundCount = [sfm count];
-    
+
     [self removeAllItems];
     menu = [self menu];
@@ -53,4 +120,164 @@
     item = [menu addItemWithTitle: @"OtherÉ" action: @selector(select:) keyEquivalent: @""];
     [item setTarget: self];
+    otherItem = [item retain];
+
+    recentMediaAliasData = [[NSMutableArray alloc] initWithCapacity: NJRQTMediaPopUpButtonMaxRecentItems + 1];
+    [self _addRecentMediaFromAliasesData: [[NSUserDefaults standardUserDefaults] arrayForKey: [self _defaultKey]]];
+
+    [self registerForDraggedTypes:
+        [NSArray arrayWithObjects: NSFilenamesPboardType, NSURLPboardType, nil]];
+}
+
+- (void)dealloc;
+{
+    [recentMediaAliasData release]; recentMediaAliasData = nil;
+    [otherItem release];
+    [selectedAlias release]; [previousAlias release];
+    [super dealloc];
+}
+
+- (BDAlias *)selectedAlias;
+{
+    return selectedAlias;
+}
+
+- (void)_setAlias:(BDAlias *)alias;
+{
+    
+    BDAlias *oldAlias = [selectedAlias retain];
+    [previousAlias release];
+    previousAlias = oldAlias;
+    if (selectedAlias != alias) {
+        [selectedAlias release];
+        selectedAlias = [alias retain];
+    }
+}
+
+- (void)_setPath:(NSString *)path;
+{
+    [self _setAlias: [BDAlias aliasWithPath: path]];
+}
+
+- (NSMenuItem *)_itemForAlias:(BDAlias *)alias;
+{
+    NSString *path;
+    SoundFile *sf;
+    if (alias == nil) {
+        return [self itemAtIndex: 0];
+    }
+
+    // [self _validateRecentMedia];
+    path = [alias fullPath];
+    sf = [[SoundFileManager sharedSoundFileManager] soundFileFromPath: path];
+    NSLog(@"_itemForAlias: %@", path);
+
+    // selected a system sound?
+    if (sf != nil) {
+        NSLog(@"_itemForAlias: selected system sound");
+        return [self itemAtIndex: [self indexOfItemWithRepresentedObject: sf]];
+    } else {
+        NSEnumerator *e = [recentMediaAliasData objectEnumerator];
+        NSData *aliasData;
+        NSMenuItem *item;
+        int recentIndex = 0;
+
+        while ( (aliasData = [e nextObject]) != nil) {
+            // selected a recently selected, non-system sound?
+            if ([alias aliasDataIsEqual: aliasData]) {
+                int otherIndex = [self indexOfItem: otherItem];
+                int menuIndex = [recentMediaAliasData count] - recentIndex + otherIndex + 1;
+                if (menuIndex == otherIndex + 1) return [self itemAtIndex: menuIndex]; // already at top
+                // remove item, add (at top) later
+                NSLog(@"_itemForAlias removing item: count %d - idx %d + otherItemIndex %d + 1 = %d [%@]", [recentMediaAliasData count], recentIndex, otherIndex, menuIndex, [self itemAtIndex: menuIndex]);
+                [self removeItemAtIndex: menuIndex];
+                [recentMediaAliasData removeObjectAtIndex: recentIndex];
+                break;
+            }
+            recentIndex++;
+        }
+
+        // create the item
+        item = [self _addRecentMediaAtPath: path withAlias: alias];
+        [self _writeRecentMedia];
+        return item;
+    }
+}
+
+- (void)_invalidateSelection;
+{
+    [self _setAlias: previousAlias];
+    [self selectItem: [self _itemForAlias: [self selectedAlias]]];
+}
+
+- (BOOL)_validatePreview;
+{
+    [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 _invalidateSelection];
+                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 _invalidateSelection];
+                [movie release];
+                return NO;
+            }
+        }
+        [movie release];
+        [preview start: self];
+    }
+    return YES;
+}
+
+- (IBAction)stopSoundPreview:(id)sender;
+{
+    [preview stop: self];
+}
+
+- (void)_beepSelected:(NSMenuItem *)sender;
+{
+    [self _setAlias: nil];
+    [self _validatePreview];
+}
+
+- (void)_soundFileSelected:(NSMenuItem *)sender;
+{
+    [self _setPath: [(SoundFile *)[sender representedObject] path]];
+    if (![self _validatePreview]) {
+        [[self menu] removeItem: sender];
+    }
+}
+
+- (void)_aliasSelected:(NSMenuItem *)sender;
+{
+    BDAlias *alias = [sender representedObject];
+    int index = [self indexOfItem: sender], otherIndex = [self indexOfItem: otherItem];
+    [self _setAlias: alias];
+    if (![self _validatePreview]) {
+        [[self menu] removeItem: sender];
+    } else if (index > otherIndex + 1) { // move "other" item to top of list
+        int recentIndex = [recentMediaAliasData count] - index + otherIndex + 1;
+        NSMenuItem *item = [[self itemAtIndex: index] retain];
+        NSData *data = [[recentMediaAliasData objectAtIndex: recentIndex] retain];
+        [self removeItemAtIndex: index];
+        [[self menu] insertItem: item atIndex: otherIndex + 1];
+        [self selectItem: item];
+        [item release];
+        NSAssert(recentIndex >= 0, @"Recent media index invalid");
+        NSLog(@"_aliasSelected removing item %d - %d + %d + 1 = %d of recentMediaAliasData", [recentMediaAliasData count], index, otherIndex, recentIndex);
+        [recentMediaAliasData removeObjectAtIndex: recentIndex];
+        [recentMediaAliasData addObject: data];
+        [data release];
+    } else NSLog(@"_aliasSelected ...already at top");
 }
 
@@ -71,23 +298,4 @@
 }
 
-- (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;
 {
@@ -97,80 +305,62 @@
         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();
+        [self _setPath: [files objectAtIndex: 0]];
+        if ([self _validatePreview]) {
+            [self selectItem: [self _itemForAlias: selectedAlias]];
+        }
     } 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];
-    }
+        // "Other..." item is still selected, revert to previously selected item
+        // XXX issue with cancelling, top item in recent menu is sometimes duplicated!?
+        [self selectItem: [self _itemForAlias: selectedAlias]];
+    }
+    // [self _validateRecentMedia];
+}
+
+@end
+
+@implementation NJRQTMediaPopUpButton (NSDraggingDestination)
+
+- (BOOL)acceptsDragFrom:(id <NSDraggingInfo>)sender;
+{
+    NSURL *url = [NSURL URLFromPasteboard: [sender draggingPasteboard]];
+
+    if (url == nil || ![url isFileURL]) return NO;
     return YES;
 }
 
+- (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 _validatePreview];
+    }
+    return YES;
+}
+
 @end
