// // LaunchWindowController.m // Ample // // Created by Kelvin Sherlock on 8/29/2020. // Copyright © 2020 Kelvin Sherlock. All rights reserved. // #import "Ample.h" #import "LaunchWindowController.h" #import "MediaViewController.h" #import "SlotViewController.h" #import "NewMachineViewController.h" #import "LogWindowController.h" #import "AutocompleteControl.h" #import "SoftwareList.h" #import "BookmarkManager.h" #include #include static NSString *kMyContext = @"kMyContext"; static NSString *kContextMachine = @"kContextMachine"; @interface LaunchWindowController () { BOOL _loadingBookmark; NSString *_machine; NSDictionary *_machineDescription; } @property (strong) IBOutlet MediaViewController *mediaController; @property (strong) IBOutlet SlotViewController *slotController; @property (strong) IBOutlet NewMachineViewController *machineViewController; @property (weak) IBOutlet NSView *machineView; @property (weak) IBOutlet NSView *slotView; @property (weak) IBOutlet NSView *mediaView; /* kvo */ @property NSString *commandLine; @property NSArray *args; @property NSString *machine; //@property NSString *machineName; @property BOOL mameDebug; @property BOOL mameSquarePixels; @property BOOL mameMouse; @property BOOL mameSamples; @property BOOL mameBGFX; @property BOOL mameAVI; @property BOOL mameWAV; @property BOOL mameVGM; @property BOOL mameBitBanger; @property BOOL mameShareDirectory; @property NSString *mameAVIPath; @property NSString *mameWAVPath; @property NSString *mameVGMPath; @property NSString *mameShareDirectoryPath; @property NSString *mameBitBangerPath; @property NSInteger mameSpeed; @property NSInteger mameBackend; @property NSInteger mameEffects; @property NSInteger mameWindowMode; @property (weak) IBOutlet AutocompleteControl *softwareListControl; @property SoftwareSet *softwareSet; @property Software *software; @property (strong) IBOutlet NSWindow *addBookmarkWindow; @property (strong) NSString *bookmarkName; @property (weak) IBOutlet NSTextField *bookmarkTextField; @property BOOL optionKey; @end @interface LaunchWindowController (SoftwareList) -(void)updateSoftwareList; @end @interface LaunchWindowController (Bookmark) -(IBAction)addBookmark:(id)sender; -(IBAction)defaultLoad:(id)sender; @end #define SIZEOF(x) (sizeof(x) / sizeof(x[0])) static NSString *BackendStrings[] = { @"", @"metal", @"opengl", }; static NSString *EffectsStrings[] = { @"-", @"unfiltered", @"hlsl", @"crt-geom", @"crt-geom-deluxe", @"lcd-grid", }; static int BackEndIndex(NSString *str) { if (!str) return -1; for (int i = 1; i < SIZEOF(BackendStrings); ++i) { if ([str isEqualToString: BackendStrings[i]]) return i; } return -1; } static int EffectsIndex(NSString *str) { if (!str) return -1; for (int i = 1; i < SIZEOF(EffectsStrings); ++i) { if ([str isEqualToString: EffectsStrings[i]]) return i; } return -1; } @implementation LaunchWindowController -(NSString *)windowNibName { return @"LaunchWindow"; } -(void)reset { // handled elsewhere. //[self setMachine: nil]; [self setMameSpeed: 1]; [self setMameBGFX: YES]; [self setMameMouse: NO]; [self setMameSamples: NO]; [self setMameSquarePixels: NO]; [self setMameDebug: NO]; [self setMameWindowMode: 1]; // default = 1x window. [self setMameBackend: 0]; [self setMameEffects: 0]; [self setMameBitBangerPath: nil]; [self setMameShareDirectoryPath: nil]; [self setMameAVIPath: nil]; [self setMameWAVPath: nil]; [self setMameVGMPath: nil]; [self setMameAVI: NO]; [self setMameWAV: NO]; [self setMameVGM: NO]; [self setMameBitBanger: NO]; [self setMameShareDirectory: NO]; #if 0 [self setSoftware: nil]; //_softwareSet = nil; [_softwareListControl setObjectValue: nil]; #endif } -(void)resetSoftware { [self setSoftware: nil]; //_softwareSet = nil; [_softwareListControl setObjectValue: nil]; } -(void)windowWillLoad { // if this calls [self window], it will recurse. that is bad. //[self defaultLoad: nil]; [self reset]; } static void AddSubview(NSView *parent, NSView *child) { [child setFrame: [parent bounds]]; [parent addSubview: child]; } - (void)windowDidLoad { [super windowDidLoad]; // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. AddSubview(_slotView, [_slotController view]); AddSubview(_mediaView, [_mediaController view]); AddSubview(_machineView, [_machineViewController view]); // can't be done until above views are set up. [self defaultLoad: nil]; NSArray *keys = @[ //@"mameMachine", // - handled @"mameSquarePixels", @"mameWindowMode", @"mameMouse", @"mameSamples", @"mameDebug", @"mameSpeed", @"mameAVI", @"mameAVIPath", @"mameWAV", @"mameWAVPath", @"mameVGM", @"mameVGMPath", @"mameShareDirectory", @"mameShareDirectoryPath", @"mameBitBanger", @"mameBitBangerPath", @"mameBGFX", @"mameBackend", @"mameEffects", @"software", ]; for (NSString *key in keys) { [self addObserver: self forKeyPath: key options:0 context: (__bridge void * _Nullable)(kMyContext)]; } [_slotController addObserver: self forKeyPath: @"args" options: 0 context: (__bridge void * _Nullable)(kMyContext)]; [_mediaController addObserver: self forKeyPath: @"args" options: 0 context: (__bridge void * _Nullable)(kMyContext)]; [_mediaController bind: @"media" toObject: _slotController withKeyPath: @"media" options: 0]; [_machineViewController addObserver: self forKeyPath: @"machine" options: 0 context: (__bridge void * _Nullable)kContextMachine]; [_softwareListControl setMinWidth: 250]; [_softwareListControl setHidden: YES]; [self buildCommandLine]; } -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == (__bridge void *)kMyContext) { if (_loadingBookmark) return; [self buildCommandLine]; } else if (context == (__bridge void *)kContextMachine) { if (_loadingBookmark) return; NSString *machine = [_machineViewController machine]; [self setMachine: machine]; [_slotController setMachine: machine]; [self buildCommandLine]; } else { [super observeValueForKeyPath: keyPath ofObject: object change: change context: context]; } } -(NSString *)machine { return _machine; } -(void)setMachine:(NSString *)machine { if (_machine == machine) return; _machine = machine; _machineDescription = MameMachine(machine); #if 0 [self setMachineName: [_machineDescription objectForKey: @"description"]]; #else NSString *title = _machineDescription ? [NSString stringWithFormat: @"Ample – %@", [_machineDescription objectForKey: @"description"]] : @"Ample"; [[self window] setTitle: title]; #endif // enable/disable the right-click menu NSWindow *window = [self window]; NSView *view = [window contentView]; if (_machine) [view setMenu: [window menu]]; else [view setMenu: nil]; // software list. [self updateSoftwareList]; } static NSString * JoinArguments(NSArray *argv, NSString *argv0) { static NSCharacterSet *safe = nil; static NSCharacterSet *unsafe = nil; if (!safe) { NSString *str = @"%+-./:=_" @"0123456789" @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ; safe = [NSCharacterSet characterSetWithCharactersInString: str]; unsafe = [safe invertedSet]; } NSMutableString *rv = [NSMutableString new]; //unsigned ix = 0; //[rv appendString: @"mame"]; if (argv0) { [rv appendString: argv0]; } else { NSString *path = MamePath(); path = path ? [path lastPathComponent] : @"mame"; [rv appendString: path]; } for (NSString *s in argv) { [rv appendString: @" "]; NSUInteger l = [s length]; if (!l) { [rv appendString: @"''"]; continue; } if (!CFStringFindCharacterFromSet((CFStringRef)s, (CFCharacterSetRef)unsafe, CFRangeMake(0, l), 0, NULL)) { [rv appendString: s]; continue; } unichar *buffer = malloc(sizeof(unichar) * l); [s getCharacters: buffer range: NSMakeRange(0, l)]; [rv appendString: @"'"]; for (NSUInteger i = 0; i < l; ++i) { unichar c = buffer[i]; switch (c) { case '\'': [rv appendString: @"\\'"]; break; case '\\': [rv appendString: @"\\\\"]; break; case 0x7f: [rv appendString: @"\\177"]; break; default: { NSString *cc; if (c < 0x20) { cc = [NSString stringWithFormat: @"\\%o", c]; } else { cc = [NSString stringWithCharacters: &c length: 1]; } [rv appendString: cc]; break; } } } [rv appendString: @"'"]; free(buffer); } return rv; } static NSString *ShellQuote(NSString *s) { static NSCharacterSet *safe = nil; static NSCharacterSet *unsafe = nil; if (!safe) { NSString *str = @"%+-./:=_" @"0123456789" @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ; safe = [NSCharacterSet characterSetWithCharactersInString: str]; unsafe = [safe invertedSet]; } NSUInteger l = [s length]; if (!l) { return @"''"; } if (!CFStringFindCharacterFromSet((CFStringRef)s, (CFCharacterSetRef)unsafe, CFRangeMake(0, l), 0, NULL)) { return s; } NSMutableString *rv = [NSMutableString new]; unichar *buffer = malloc(sizeof(unichar) * l); [s getCharacters: buffer range: NSMakeRange(0, l)]; [rv appendString: @"'"]; for (NSUInteger i = 0; i < l; ++i) { unichar c = buffer[i]; switch (c) { case '\'': [rv appendString: @"\\'"]; break; case '\\': [rv appendString: @"\\\\"]; break; case 0x7f: [rv appendString: @"\\177"]; break; default: { NSString *cc; if (c < 0x20) { cc = [NSString stringWithFormat: @"\\%o", c]; } else { cc = [NSString stringWithCharacters: &c length: 1]; } [rv appendString: cc]; break; } } } [rv appendString: @"'"]; free(buffer); return rv; } -(void)buildCommandLine { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; if (!_machine) { [self setCommandLine: @""]; return; } NSMutableArray *argv = [NSMutableArray new]; //[argv addObject: @"mame"]; [argv addObject: _machine]; if (_software) { NSString *name = [_softwareSet nameForSoftware: _software]; if (name) [argv addObject: name]; } // -confirm_quit? [argv addObject: @"-skip_gameinfo"]; if (_mameMouse) [argv addObject: @"-mouse"]; // capture the mouse cursor when over the window. if (!_mameSamples) [argv addObject: @"-nosamples"]; if (_mameDebug) [argv addObject: @"-debug"]; /* * -window -nomax uses a 4:3 aspect ratio - ie, height = width * 3 / 4 (since height is always the limiting factor) * for square pixels, should pass the true size and true aspect ratio. */ NSSize screen = [_slotController resolution]; switch(_mameWindowMode) { case 0: // full screen; // no uneven stretch doesn't do anything in full-screen mode. break; case 1: // 1x // make the command-line a bit shorter and more pleasant. if (!_mameSquarePixels) { [argv addObject: @"-window"]; [argv addObject: @"-nomax"]; break; } // drop through. case 2: // 2x case 3: // 3x if (_mameSquarePixels) { // NSString *aspect = [NSString stringWithFormat: @"%u:%u", (unsigned)screen.width, (unsigned)screen.height]; // [argv addObject: @"-aspect"]; // [argv addObject: aspect]; [argv addObject: @"-nounevenstretch"]; } else { screen.height = round(screen.width * 3 / 4); } [argv addObject: @"-window"]; NSString *res = [NSString stringWithFormat: @"%ux%u", (unsigned)(_mameWindowMode * screen.width), (unsigned)(_mameWindowMode * screen.height) ]; [argv addObject: @"-resolution"]; [argv addObject: res]; break; } if (_mameBGFX) { if (_mameBackend) { [argv addObject: @"-bgfx_backend"]; [argv addObject: BackendStrings[_mameBackend]]; } if (_mameEffects) { [argv addObject: @"-bgfx_screen_chains"]; [argv addObject: EffectsStrings[_mameEffects]]; } } else { [argv addObject: @"-video"]; [argv addObject: @"soft"]; } // -speed n // -scale n NSArray *tmp; tmp = [_slotController args]; if ([tmp count]) { [argv addObjectsFromArray: tmp]; } tmp = [_mediaController args]; if ([tmp count]) { [argv addObjectsFromArray: tmp]; } if (_mameSpeed < 0) { [argv addObject: @"-nothrottle"]; } else if (_mameSpeed > 1) { [argv addObject: @"-speed"]; [argv addObject: [NSString stringWithFormat: @"%d", (int)_mameSpeed]]; } // audio video. if (_mameAVI && [_mameAVIPath length]) { [argv addObject: @"-aviwrite"]; [argv addObject: _mameAVIPath]; } if (_mameWAV && [_mameWAVPath length]) { [argv addObject: @"-wavwrite"]; [argv addObject: _mameWAVPath]; } // vgm only valid for custom mame. if (![defaults boolForKey: kUseCustomMame]) { if (_mameVGM && [_mameVGMPath length]) { [argv addObject: @"-vgmwrite"]; [argv addObject: _mameVGMPath]; } } if (_mameShareDirectory && [_mameShareDirectoryPath length]) { [argv addObject: @"-share_directory"]; [argv addObject: _mameShareDirectoryPath]; } if (_mameBitBanger && [_mameBitBangerPath length]) { [argv addObject: @"-bitbanger"]; [argv addObject: _mameBitBangerPath]; } [self setCommandLine: JoinArguments(argv, nil)]; [self setArgs: argv]; } -(BOOL)validateMenuItem:(NSMenuItem *)menuItem { SEL cmd = [menuItem action]; if (cmd == @selector(exportShellScript:)) { return [_args count] ? YES : NO; } if (cmd == @selector(addBookmark:)) { return _machine ? YES : NO; } return YES; //return [super validateMenuItem: menuItem]; // not implemented? } # pragma mark - IBActions - (IBAction)launchAction:(id)sender { [[self window] makeFirstResponder: nil]; // in case text is being edited... if (![_args count]) return; [LogWindowController controllerForArgs: _args]; } - (IBAction)listMedia:(id)sender { [[self window] makeFirstResponder: nil]; // in case text is being edited... if (!_machine) return; NSMutableArray *argv = [NSMutableArray new]; [argv addObject: _machine]; [argv addObject: @"-listmedia"]; NSArray *tmp; tmp = [_slotController args]; if ([tmp count]) { [argv addObjectsFromArray: tmp]; } #if 0 tmp = [_mediaController args]; if ([tmp count]) { [argv addObjectsFromArray: tmp]; } #endif [LogWindowController controllerForArgs: argv close: NO]; } - (IBAction)listSlots:(id)sender { [[self window] makeFirstResponder: nil]; // in case text is being edited... if (!_machine) return; NSMutableArray *argv = [NSMutableArray new]; [argv addObject: _machine]; [argv addObject: @"-listslots"]; NSArray *tmp; tmp = [_slotController args]; if ([tmp count]) { [argv addObjectsFromArray: tmp]; } #if 0 tmp = [_mediaController args]; if ([tmp count]) { [argv addObjectsFromArray: tmp]; } #endif [LogWindowController controllerForArgs: argv close: NO]; } -(IBAction)exportShellScript: (id)sender { NSSavePanel *p = [NSSavePanel savePanel]; NSString *defaultName = [_machine stringByAppendingString: @".sh"]; [p setTitle: @"Export Shell Script"]; [p setExtensionHidden: NO]; [p setNameFieldStringValue: defaultName]; //[p setDelegate: self]; NSWindow *w = [self window]; NSMutableString *data = [NSMutableString new]; [data appendString: @"#!/bin/sh\n\n"]; [data appendFormat: @"MAME=%@\n", ShellQuote(MamePath())]; [data appendFormat: @"cd %@\n", ShellQuote(MameWorkingDirectoryPath())]; [data appendString: JoinArguments(_args, @"$MAME")]; [data appendString: @"\n\n"]; [p beginSheetModalForWindow: w completionHandler: ^(NSModalResponse r) { if (r != NSModalResponseOK) return; NSURL *url = [p URL]; NSError *error = nil; [data writeToURL: url atomically: YES encoding: NSUTF8StringEncoding error: &error]; [p orderOut: nil]; if (error) { [self presentError: error]; return; } // chmod 755... int ok = chmod([url fileSystemRepresentation], 0755); if (ok < 0) { // ... } }]; } -(IBAction)resetMachine:(id)sender { [self reset]; } -(IBAction)resetAll:(id)sender { [self reset]; [self resetSoftware]; [_slotController resetSlots: sender]; [_mediaController resetMedia: sender]; } -(IBAction)resetMedia:(id)sender { [_mediaController resetMedia: sender]; [_softwareListControl setObjectValue: nil]; [self setSoftware: nil]; } @end @implementation LaunchWindowController (SoftwareList) -(void)updateSoftwareList { _softwareSet = [SoftwareSet softwareSetForMachine: _machine]; [_softwareListControl setAutocompleteDelegate: _softwareSet]; if (_softwareSet) { [_softwareListControl invalidate]; [_softwareListControl setHidden: NO]; } else { _software = nil; [_softwareListControl setHidden: YES]; } } - (IBAction)softwareChanged:(id)sender { id o = [(NSControl *)sender objectValue]; //NSLog(@"%@", o); [self setSoftware: o]; } @end @implementation LaunchWindowController (Bookmark) -(IBAction)defaultSave:(id)sender { BookmarkManager *bm = [BookmarkManager sharedManager]; NSDictionary *d = [self makeBookmark]; [bm saveDefault: d]; } -(IBAction)defaultLoad:(id)sender { BookmarkManager *bm = [BookmarkManager sharedManager]; NSDictionary *d = [bm loadDefault]; if (!d) { [self resetAll: sender]; [self setMachine: nil]; [_machineViewController reset]; [_slotController setMachine: nil]; return; } [self loadBookmark: d]; } -(IBAction)addBookmark:(id)sender { if (!_machine) return; NSString *name = nil; if (_machineDescription) name = [_machineDescription objectForKey:@"description"]; if (!name) name = _machine; if (_software) { name = [name stringByAppendingFormat: @" - %@", [_software title]]; } [self setBookmarkName: name]; [_bookmarkTextField selectText: nil]; [[self window] beginSheet: _addBookmarkWindow completionHandler: nil]; } -(IBAction)bookmarkCancel:(id)sender { [[self window] endSheet: _addBookmarkWindow]; [_addBookmarkWindow orderOut: nil]; } -(IBAction)bookmarkSave:(id)sender { BookmarkManager *bm = [BookmarkManager sharedManager]; if (![bm validateName: _bookmarkName]) { [_bookmarkTextField selectText: nil]; NSBeep(); return; } NSDictionary *d = [self makeBookmark]; [bm saveBookmark: d name: _bookmarkName]; [[self window] endSheet: _addBookmarkWindow]; [_addBookmarkWindow orderOut: nil]; [self setBookmarkName: nil]; } -(IBAction)bookmarkMenu:(id)sender { NSURL *url = [sender representedObject]; if (!url) return; NSDictionary *d = [NSDictionary dictionaryWithContentsOfURL: url]; if (!d) return; // oops... [self loadBookmark: d]; } -(void)loadBookmark: (NSDictionary *)d { Class StringClass = [NSString class]; Class NumberClass = [NSNumber class]; NSString *machine = [d objectForKey: @"machine"]; if (!machine) return; _loadingBookmark = YES; [_machineViewController willLoadBookmark: d]; [_slotController willLoadBookmark: d]; [_mediaController willLoadBookmark: d]; [self reset]; [self resetSoftware]; [self setMachine: machine]; [self updateSoftwareList]; [_softwareListControl setObjectValue: nil]; // will reload the completion list. NSString *str; str = [d objectForKey: @"software"]; if ([str isKindOfClass: StringClass]) { Software *s = [_softwareSet softwareForName: str]; if (s) { [_softwareListControl setObjectValue: s]; [self setSoftware: s]; } } // Boolean values. NSNumber *n; #undef _ #define _(a,b) n = [d objectForKey: a]; if ([n isKindOfClass: NumberClass]) [self b : [n boolValue]] _(@"debug", setMameDebug); _(@"squarePixels", setMameSquarePixels); _(@"mouse", setMameMouse); _(@"samples", setMameSamples); _(@"bgfx", setMameBGFX); // numeric values // check if in range? #undef _ #define _(a,b) n = [d objectForKey: a]; if ([n isKindOfClass: NumberClass]) [self b : [n intValue]] _(@"windowMode", setMameWindowMode); _(@"speed", setMameSpeed); // string values #undef _ #define _(a,b) str = [d objectForKey: a]; if ([str isKindOfClass: StringClass]) [self b : str] _(@"shareDirectory", setMameShareDirectoryPath); _(@"bitBanger", setMameBitBangerPath); if ([_mameShareDirectoryPath length]) [self setMameShareDirectory: YES]; if ([_mameBitBangerPath length]) [self setMameBitBanger: YES]; _(@"AVIPath", setMameAVIPath); _(@"WAVPath", setMameWAVPath); _(@"VGMPath", setMameVGMPath); if ([_mameAVIPath length]) [self setMameAVI: YES]; if ([_mameVGMPath length]) [self setMameVGM: YES]; if ([_mameWAVPath length]) [self setMameWAV: YES]; str = [d objectForKey: @"backend"]; if ([str isKindOfClass: [NSString class]]) { int ix = BackEndIndex(str); if (ix >= 0) [self setMameBackend: ix]; } str = [d objectForKey: @"effects"]; if ([str isKindOfClass: [NSString class]]) { int ix = EffectsIndex(str); if (ix >= 0) [self setMameEffects: ix]; } [_machineViewController loadBookmark: d]; [_slotController loadBookmark: d]; [_mediaController loadBookmark: d]; [_machineViewController didLoadBookmark: d]; [_slotController didLoadBookmark: d]; [_mediaController didLoadBookmark: d]; _loadingBookmark = NO; [self buildCommandLine]; } -(NSDictionary *)makeBookmark { [[self window] makeFirstResponder: nil]; NSMutableDictionary *dict = [NSMutableDictionary new]; [dict setObject: _machine forKey: @"machine"]; [dict setObject: @232 forKey: @"version"]; [_machineViewController saveBookmark: dict]; [_slotController saveBookmark: dict]; [_mediaController saveBookmark: dict]; // Boolean values #undef _ #define _(v,k) [dict setObject: v ? (NSObject *)kCFBooleanTrue : (NSObject *)kCFBooleanFalse forKey: k] _(_mameDebug, @"debug"); _(_mameSquarePixels, @"squarePixels"); _(_mameMouse, @"mouse"); _(_mameSamples, @"samples"); _(_mameBGFX, @"bgfx"); // numeric values #undef _ #define _(v,k) [dict setObject: @(v) forKey: k] _(_mameWindowMode, @"windowMode"); _(_mameSpeed, @"speed"); // String values #undef _ #define _(v,k) [dict setObject: v forKey: k] if (_mameAVI && [_mameAVIPath length]) _(_mameAVIPath, @"AVIPath"); if (_mameWAV && [_mameWAVPath length]) _(_mameWAVPath, @"WAVPath"); if (_mameVGM && [_mameVGMPath length]) _(_mameVGMPath, @"VGMPath"); if (_mameShareDirectory && [_mameShareDirectoryPath length]) _(_mameShareDirectoryPath, @"shareDirectory"); if (_mameBitBanger && [_mameBitBangerPath length]) _(_mameBitBangerPath, @"bitBanger"); if (_software) _([_software fullName], @"software"); if (_mameBackend) _(BackendStrings[_mameBackend], @"backend"); if (_mameEffects) _(EffectsStrings[_mameEffects], @"effects"); return dict; #undef _ } #pragma mark - NSMenuDelegate #if 1 -(void)menuNeedsUpdate:(NSMenu *)menu { NSEventModifierFlags modifiers = [NSEvent modifierFlags]; [self setOptionKey: modifiers & NSEventModifierFlagOption ? YES : NO]; } #else /* doesn't trigger when menu is the first responder. */ -(void)flagsChanged:(NSEvent *)event { NSEventModifierFlags modifiers = [event modifierFlags]; [self setOptionKey: modifiers & NSEventModifierFlagOption ? YES : NO]; [super flagsChanged: event]; } #endif @end