diff --git a/Ample.xcodeproj/project.pbxproj b/Ample.xcodeproj/project.pbxproj index 6e3083b..b87b606 100644 --- a/Ample.xcodeproj/project.pbxproj +++ b/Ample.xcodeproj/project.pbxproj @@ -149,6 +149,10 @@ B6841BD7251EC926006A5C39 /* vmnet_helper.c in Sources */ = {isa = PBXBuildFile; fileRef = B6841BCA251EC88E006A5C39 /* vmnet_helper.c */; }; B6841BDA251ECB1C006A5C39 /* mame64 in CopyFiles */ = {isa = PBXBuildFile; fileRef = B66236B824FDA698006CABD7 /* mame64 */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; B6841BDE251ECC29006A5C39 /* vmnet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6841BDD251ECC29006A5C39 /* vmnet.framework */; }; + B68A899026BE18E000B2C8C6 /* MidiManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B68A898F26BE18E000B2C8C6 /* MidiManager.m */; }; + B68A899126BE18E000B2C8C6 /* MidiManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B68A898F26BE18E000B2C8C6 /* MidiManager.m */; }; + B68A899426BF124B00B2C8C6 /* CoreMIDI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B68A899326BF124B00B2C8C6 /* CoreMIDI.framework */; }; + B68A899526BF12A600B2C8C6 /* CoreMIDI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B68A899326BF124B00B2C8C6 /* CoreMIDI.framework */; }; B6A1A1942528EB1700DB0FD7 /* Menu.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A1A1932528EB1700DB0FD7 /* Menu.m */; }; B6A1A1952528EB1700DB0FD7 /* Menu.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A1A1932528EB1700DB0FD7 /* Menu.m */; }; B6B9EA662506A5550080E70D /* EjectButton.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B9EA642506A5550080E70D /* EjectButton.m */; }; @@ -449,6 +453,9 @@ B6841BCA251EC88E006A5C39 /* vmnet_helper.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = vmnet_helper.c; sourceTree = ""; }; B6841BD0251EC913006A5C39 /* vmnet_helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = vmnet_helper; sourceTree = BUILT_PRODUCTS_DIR; }; B6841BDD251ECC29006A5C39 /* vmnet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = vmnet.framework; path = System/Library/Frameworks/vmnet.framework; sourceTree = SDKROOT; }; + B68A898F26BE18E000B2C8C6 /* MidiManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MidiManager.m; sourceTree = ""; }; + B68A899226BE320200B2C8C6 /* MidiManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MidiManager.h; sourceTree = ""; }; + B68A899326BF124B00B2C8C6 /* CoreMIDI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMIDI.framework; path = System/Library/Frameworks/CoreMIDI.framework; sourceTree = SDKROOT; }; B6A1A1932528EB1700DB0FD7 /* Menu.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Menu.m; sourceTree = ""; }; B6A1A1962528EB4600DB0FD7 /* Menu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Menu.h; sourceTree = ""; }; B6B9EA642506A5550080E70D /* EjectButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EjectButton.m; sourceTree = ""; }; @@ -518,6 +525,7 @@ files = ( B65D718625E70BD5008C5F87 /* WebKit.framework in Frameworks */, B635C09D26784A4800B23BFD /* Sparkle.framework in Frameworks */, + B68A899426BF124B00B2C8C6 /* CoreMIDI.framework in Frameworks */, B635C09A26784A1200B23BFD /* Sparkle.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -526,6 +534,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B68A899526BF12A600B2C8C6 /* CoreMIDI.framework in Frameworks */, B65D718725E70BE5008C5F87 /* WebKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -635,6 +644,7 @@ B66236B624FDA686006CABD7 /* Frameworks */ = { isa = PBXGroup; children = ( + B68A899326BF124B00B2C8C6 /* CoreMIDI.framework */, B635C09926784A1200B23BFD /* Sparkle.framework */, B65D718525E70BD5008C5F87 /* WebKit.framework */, B6841BDD251ECC29006A5C39 /* vmnet.framework */, @@ -725,6 +735,8 @@ B6B9EA642506A5550080E70D /* EjectButton.m */, B64AF1F8250EF6A500A09B9B /* Transformers.h */, B64AF1F9250EF6A500A09B9B /* Transformers.m */, + B68A898F26BE18E000B2C8C6 /* MidiManager.m */, + B68A899226BE320200B2C8C6 /* MidiManager.h */, B6BA258124E99BEB005FB8FF /* Assets.xcassets */, B64E15AF24EA365E00E8AD3D /* Resources */, B6BA258624E99BEB005FB8FF /* Info.plist */, @@ -1205,6 +1217,7 @@ B6665C14265A0E3E00254939 /* AutocompleteControl.m in Sources */, B64979C224EF6703008ABD20 /* MediaViewController.m in Sources */, B60A6E1424EE0AE2004B7EEF /* FlippedView.m in Sources */, + B68A899026BE18E000B2C8C6 /* MidiManager.m in Sources */, B6BA258024E99BE9005FB8FF /* AppDelegate.m in Sources */, B6004DF024FB05D600D38596 /* LogWindowController.m in Sources */, B63005332666D6940014C381 /* BookmarkManager.m in Sources */, @@ -1223,6 +1236,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B68A899126BE18E000B2C8C6 /* MidiManager.m in Sources */, B608E1802502FE0C00D53465 /* TransparentScroller.m in Sources */, B64AF1FB250EF6A500A09B9B /* Transformers.m in Sources */, B6E4B5B024FDE2670094A35C /* main.m in Sources */, diff --git a/Ample/Base.lproj/MediaView.xib b/Ample/Base.lproj/MediaView.xib index d678d47..be51d2b 100644 --- a/Ample/Base.lproj/MediaView.xib +++ b/Ample/Base.lproj/MediaView.xib @@ -61,7 +61,7 @@ - + @@ -101,7 +101,7 @@ - + @@ -146,7 +146,7 @@ - + @@ -177,12 +177,12 @@ - /path/to/file + @@ -191,6 +191,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EmptyStringTransformer + + + + + + + + + + + diff --git a/Ample/MediaViewController.m b/Ample/MediaViewController.m index dbbb1f8..b7b0ccf 100644 --- a/Ample/MediaViewController.m +++ b/Ample/MediaViewController.m @@ -9,26 +9,8 @@ #import "MediaViewController.h" #import "TableCellView.h" -enum { - kIndexFloppy525 = 0, - kIndexFloppy35, - kIndexHardDrive, - kIndexCDROM, - kIndexCassette, - kIndexDiskImage, - kIndexBitBanger, - kIndexMidiIn, - kIndexMidiOut, - kIndexPicture, // computer eyes -pic, .png only. - // kIndexPrintout // -prin, .prn extension only? - - kIndexLast -}; - -#define CATEGORY_COUNT 10 #define SIZEOF(x) (sizeof(x) / sizeof(x[0])) -static_assert(kIndexLast == CATEGORY_COUNT, "Invalid Category Count"); @protocol MediaNode -(BOOL)isGroupItem; @@ -285,15 +267,23 @@ static_assert(kIndexLast == CATEGORY_COUNT, "Invalid Category Count"); -(NSString *)viewIdentifier { if (_category == kIndexBitBanger) return @"BBItemView"; - if (_category == kIndexMidiOut) return @"OutputItemView"; - if (_category == kIndexMidiIn) return @"OutputItemView"; + if (_category == kIndexMidiOut) return @"MidiItemView"; + if (_category == kIndexMidiIn) return @"MidiItemView"; return @"ItemView"; } --(void)prepareView: (TablePathView *)view { +-(void)prepareView: (MediaTableCellView *)view { /* set the path tag = category. */ + + [view prepareView: _category]; +#if 0 + if (_category == kIndexMidiIn || _category == kIndexMidiOut || _category == kIndexBitBanger) { + return; + } + NSPathControl *pc = [view pathControl]; [pc setTag: _category + 1]; // to differentiate 0 / no path control. +#endif } -(CGFloat)height { @@ -437,7 +427,7 @@ x = media.name; cat = _data[index]; delta |= [cat setItemCount: x] // So we should build a device list (and pre-populate the default one) // another approach is a separate utility to act as a midi/serial input converter // and midi file / serial converter so the modem/serial port could be used. -#if 0 +#if 1 _(midiin, kIndexMidiIn); _(midiout, kIndexMidiOut); #endif @@ -782,6 +772,9 @@ static NSString *kDragType = @"private.ample.media"; -(IBAction)textAction: (id)sender { [self rebuildArgs]; } +- (IBAction)midiAction:(id)sender { + [self rebuildArgs]; +} -(IBAction)resetMedia:(id)sender { [self resetDiskImages]; diff --git a/Ample/MidiManager.h b/Ample/MidiManager.h new file mode 100644 index 0000000..cd4ef49 --- /dev/null +++ b/Ample/MidiManager.h @@ -0,0 +1,25 @@ +// +// MidiManager.h +// Ample +// +// Created by Kelvin Sherlock on 8/6/2021. +// Copyright © 2021 Kelvin Sherlock. All rights reserved. +// + +#ifndef MidiManager_h +#define MidiManager_h + + +extern NSString *kMidiSourcesChangedNotification; +extern NSString *kMidiDestinationsChangedNotification; + +@interface MidiManager : NSObject + +@property NSArray *sources; +@property NSArray *destinations; + ++(instancetype)sharedManager; + +@end + +#endif /* MidiManager_h */ diff --git a/Ample/MidiManager.m b/Ample/MidiManager.m new file mode 100644 index 0000000..993b5ba --- /dev/null +++ b/Ample/MidiManager.m @@ -0,0 +1,153 @@ +// +// Midi.m +// Ample +// +// Created by Kelvin Sherlock on 8/6/2021. +// Copyright © 2021 Kelvin Sherlock. All rights reserved. +// + +#import +#import + +#import "MidiManager.h" + +static NSArray *MidiSources(void) { + + ItemCount count = MIDIGetNumberOfSources(); + if (count <= 0) return @[]; + + NSMutableArray *rv = [NSMutableArray arrayWithCapacity: count + 1]; + + MIDIEndpointRef ep; + for(int i = 0; i < count; ++i) { + ep = MIDIGetSource(i); + if (!ep) continue; + + // https://developer.apple.com/library/archive/qa/qa1374/_index.html + CFStringRef str = NULL; + MIDIObjectGetStringProperty(ep, kMIDIPropertyDisplayName, &str); + + if (str) { + [rv addObject: (__bridge id _Nonnull)(str)]; + CFRelease(str); + } + } + return rv; +} + + +static NSArray *MidiDestinations(void) { + + ItemCount count = MIDIGetNumberOfDestinations(); + if (count <= 0) return @[]; + + NSMutableArray *rv = [NSMutableArray arrayWithCapacity: count + 1]; + + MIDIEndpointRef ep; + for(int i = 0; i < count; ++i) { + ep = MIDIGetDestination(i); + if (!ep) continue; + + // https://developer.apple.com/library/archive/qa/qa1374/_index.html + CFStringRef str = NULL; + MIDIObjectGetStringProperty(ep, kMIDIPropertyDisplayName, &str); + + if (str) { + [rv addObject: (__bridge id _Nonnull)(str)]; + CFRelease(str); + } + } + return rv; +} + +NSString *kMidiSourcesChangedNotification = @"Midi Sources Changed"; +NSString *kMidiDestinationsChangedNotification = @"Midi Destinations Changed"; + + +@interface MidiManager () { + MIDIClientRef _client; +} + +-(void)objectAddRemove: (const MIDIObjectAddRemoveNotification *)message; +-(void)objectPropertyChanged: (const MIDIObjectPropertyChangeNotification *)message; +@end + + +static MidiManager *singleton = nil; +@implementation MidiManager + +-(void)awakeFromNib { + if (!singleton) singleton = self; +} + ++(instancetype)sharedManager { + if (!singleton) singleton = [MidiManager new]; + return singleton; +} + +-(instancetype)init { + + if (singleton) return singleton; + + OSStatus status; + + + status = MIDIClientCreateWithBlock( + CFSTR("serial_midi"), + &_client, + ^(const MIDINotification *message){ + switch(message->messageID) { + case kMIDIMsgObjectAdded: + case kMIDIMsgObjectRemoved: + [self objectAddRemove: (const MIDIObjectAddRemoveNotification *)message]; + break; + case kMIDIMsgPropertyChanged: + [self objectPropertyChanged: (const MIDIObjectPropertyChangeNotification *)message]; + default: + break; + } + }); + + _sources = MidiSources(); + _destinations = MidiDestinations(); + return self; +} + +-(void)objectAddRemove: (const MIDIObjectAddRemoveNotification *)message { + + const MIDIObjectAddRemoveNotification *m = (const MIDIObjectAddRemoveNotification *)message; + + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + if (m->childType == kMIDIObjectType_Source) { + [self setSources: MidiSources()]; + [nc postNotificationName: kMidiSourcesChangedNotification object: self]; + } + + if (m->childType == kMIDIObjectType_Destination) { + [self setDestinations: MidiDestinations()]; + [nc postNotificationName: kMidiDestinationsChangedNotification object: self]; + } + +} +-(void)objectPropertyChanged: (const MIDIObjectPropertyChangeNotification *)message { + + const MIDIObjectPropertyChangeNotification *m = (const MIDIObjectPropertyChangeNotification *)message; + if (m->propertyName == kMIDIPropertyDisplayName) { + [self setSources: MidiSources()]; + [self setDestinations: MidiDestinations()]; + + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName: kMidiSourcesChangedNotification object: self]; + [nc postNotificationName: kMidiDestinationsChangedNotification object: self]; + } +} + + +-(void)dealloc { + + if (_client) + MIDIClientDispose(_client); +} +@end + diff --git a/Ample/TableCellView.h b/Ample/TableCellView.h index d115eb2..5bb2fbe 100644 --- a/Ample/TableCellView.h +++ b/Ample/TableCellView.h @@ -11,12 +11,40 @@ //NS_ASSUME_NONNULL_BEGIN +enum { + kIndexFloppy525 = 0, + kIndexFloppy35, + kIndexHardDrive, + kIndexCDROM, + kIndexCassette, + kIndexDiskImage, + kIndexBitBanger, + kIndexMidiIn, + kIndexMidiOut, + kIndexPicture, // computer eyes -pic, .png only. + // kIndexPrintout // -prin, .prn extension only? + + kIndexLast +}; +#define CATEGORY_COUNT 10 +static_assert(kIndexLast == CATEGORY_COUNT, "Invalid Category Count"); -@interface TablePathView : NSTableCellView -@property (weak) IBOutlet NSPathControl *pathControl; + +@interface MediaTableCellView : NSTableCellView @property (weak) IBOutlet NSButton *ejectButton; @property (weak) IBOutlet NSImageView *dragHandle; @property BOOL movable; + +-(void)prepareView: (NSInteger)category; +@end + +@interface PathTableCellView : MediaTableCellView +@property (weak) IBOutlet NSPathControl *pathControl; +@end + + +@interface MidiTableCellView : MediaTableCellView +@property (weak) IBOutlet NSPopUpButton *popUpButton; @end //NS_ASSUME_NONNULL_END diff --git a/Ample/TableCellView.m b/Ample/TableCellView.m index 89ddf37..dffcbf1 100644 --- a/Ample/TableCellView.m +++ b/Ample/TableCellView.m @@ -7,13 +7,16 @@ // #import "TableCellView.h" +#import "MidiManager.h" +#import "Menu.h" - -@implementation TablePathView { +@implementation MediaTableCellView +#if 0 +{ NSTrackingRectTag _trackingRect; } - +#endif -(void)awakeFromNib { // need to do it here for 10.11 compatibility. @@ -31,6 +34,8 @@ } +-(void)prepareView: (NSInteger)category { +} #if 0 -(void)awakeFromNib { @@ -65,3 +70,143 @@ @end +@implementation PathTableCellView + +-(void)prepareView: (NSInteger)category { + [_pathControl setTag: category + 1]; +} + +- (void)pathControl:(NSPathControl *)pathControl willPopUpMenu:(NSMenu *)menu { + // if this is an output path, replace the "choose..." button with a save panel. + NSMenuItem *item = [menu itemAtIndex: 0]; + if (item) { + [item setTarget: self]; + [item setAction: @selector(choosePath:)]; + } +} + +-(IBAction)choosePath:(id)sender { + NSPathControl *pc = _pathControl; + NSURL *url = [pc URL]; + + NSSavePanel *p = [NSSavePanel savePanel]; + + if (url) { + NSFileManager *fm = [NSFileManager defaultManager]; + BOOL dir = NO; + NSString *str = [NSString stringWithCString: [url fileSystemRepresentation] encoding: NSUTF8StringEncoding]; + [fm fileExistsAtPath: str isDirectory: &dir]; + + if (!dir) { + [p setNameFieldStringValue: [str lastPathComponent]]; + url = [url URLByDeletingLastPathComponent]; + } + [p setDirectoryURL: url]; + } + [p setExtensionHidden: NO]; + + [p beginWithCompletionHandler: ^(NSModalResponse response){ + if (response != NSModalResponseOK) return; + NSURL *url = [p URL]; + [pc setURL: url]; + }]; + +} + +@end + + +@interface EmptyStringTransformer : NSValueTransformer + +@end + +static NSString *kNone = @"—None—"; + +@implementation EmptyStringTransformer + ++(void)load { + [self setValueTransformer: [self new] forName: @"EmptyStringTransformer"]; +} + ++ (Class)transformedValueClass { + return [NSString class]; +} ++ (BOOL)allowsReverseTransformation { + return YES; +} +- (id)transformedValue:(id)value { + if (value == nil) return kNone; + if ([kNone isEqualToString: value]) return nil; + return value; +} + +@end + +@implementation MidiTableCellView { + NSInteger _category; +} + +/* binding should be able to handle the menu but i couldn't make it work. */ + + +-(void)prepareView: (NSInteger)category { + _category = category; + + // 10.11 + doesn't need to remove the observer in the -dealloc + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc addObserver: self selector: @selector(midiChanged:) name: category == kIndexMidiIn ? kMidiSourcesChangedNotification : kMidiDestinationsChangedNotification object: nil]; + + [self updateMenus: NO]; +} + +-(void)updateMenus: (BOOL)notification { + NSMenu *menu = [_popUpButton menu]; + MidiManager *mgr = [MidiManager sharedManager]; + + NSArray *array = _category == kIndexMidiIn ? [mgr sources] : [mgr destinations]; + + [menu removeAllItems]; + NSString *selected = [[_popUpButton selectedItem] representedObject]; + int selectedIndex = -1; + NSMenuItem *item; + + item = [[NSMenuItem alloc] initWithTitle: kNone action: NULL keyEquivalent: @""]; + [item setAttributedTitle: ItalicMenuString(kNone)]; + [menu addItem: item]; + selectedIndex = 0; +#if 0 + if (!selected || [@"" isEqualToString: selected]) { + selectedIndex = 0; + } +#endif + + int ix = 1; + for (NSString *s in array) { + item = [[NSMenuItem alloc] initWithTitle: s action: NULL keyEquivalent: @""]; + [item setRepresentedObject: s]; + [menu addItem: item]; + if ([s isEqualToString: selected]) { + selectedIndex = ix; + } + ++ix; + } + + // does this propogate? + [_popUpButton selectItemAtIndex: selectedIndex]; + if (notification) [_popUpButton sendAction: [_popUpButton action] to: [_popUpButton target]]; +} + +-(void)midiChanged: (NSNotification *)notification { + + [self updateMenus: YES]; +} + + +-(void)prepareForReuse { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver: self]; + _category = 0; + [super prepareForReuse]; +} + +@end