// // Slot.m // Ample // // Created by Kelvin Sherlock on 3/6/2021. // Copyright © 2021 Kelvin Sherlock. All rights reserved. // #import #import "Slot.h" static NSArray *MapArray(NSArray *src, id(^fn)(id)) { NSMutableArray *rv = [NSMutableArray arrayWithCapacity: [src count]]; for (id x in src) { [rv addObject: fn(x)]; } return rv; } static NSArray *DeepCopyArray(NSArray *src) { if (!src) return nil; return [[NSArray alloc] initWithArray: src copyItems: YES]; } @interface Slot () { NSArray *_options; //NSArray *_menuItems; } -(SlotOption *)selectedItem; -(void)setKeyPath; -(void)setKeyPath: (NSString *)path; //-(NSArray *)buildArgs: (NSMutableArray *)args prefix: (NSString *)prefix; //-(void)buildMedia: (MediaBuilder *)builder; //-(NSArray *)buildSerial: (NSMutableArray *)array; -(instancetype)initWithDictionary: (NSDictionary *)dictionary devices: (NSDictionary *)devices; @end @interface SlotOption() { //NSArray *_children; //NSDictionary *_media; Media _media; NSString *_keyPath; //NSString *_devName; BOOL _default; } @property (readonly) NSArray *children; -(instancetype)initWithDictionary: (NSDictionary *)dictionary devices: (NSDictionary *)devices; -(NSMenuItem *)menuItem; -(void)reset; -(NSString *)keyPath; -(void)setKeyPath: (NSString *)path; -(void)buildArgs: (NSMutableArray *)args; -(void)buildMedia: (Media *)media; -(void)buildSerial: (NSMutableDictionary *)array; -(void)reserialize: (NSDictionary *)dict; //-(BOOL)loadDeviceSlots: (NSDictionary *)devices; @end @implementation Slot @synthesize type = _type; static NSDictionary *TypeMap = nil; +(void)load { TypeMap = @{ @"ramsize": @(kSlotRAM), @"smartport": @(kSlotFDC), @"bios": @(kSlotBIOS), }; } -(NSString *)description { return [NSString stringWithFormat: @"", _name]; } -(void)reset { [self setSelectedIndex: _defaultIndex >= 0 ? _defaultIndex : 0]; for (SlotOption *s in _options) { [s reset]; } } -(BOOL)selectValue: (NSString *)value { if (value) { NSInteger index = 0; for (SlotOption *item in _options) { if ([[item value] isEqualToString: value]) { [self setSelectedIndex: index]; return YES; } ++index; } } return NO; //[self setSelectedIndex: _defaultIndex >= 0 ? _defaultIndex : 0]; } -(SlotOption *)selectedItem { if (_selectedIndex < 0) return nil; if (_selectedIndex >= [_options count]) return nil; return [_options objectAtIndex: _selectedIndex]; } -(NSArray *)args { if (_selectedIndex < 0) return nil; if (_selectedIndex >= [_options count]) return nil; NSMutableArray *rv = [NSMutableArray new]; SlotOption *option = [_options objectAtIndex: _selectedIndex]; [option buildArgs: rv]; return rv; } -(NSDictionary *)serialize { if (_selectedIndex < 0) return nil; if (_selectedIndex >= [_options count]) return nil; NSMutableDictionary *d = [NSMutableDictionary new]; SlotOption *option = [_options objectAtIndex: _selectedIndex]; [option buildSerial: d]; //if (![d count]) return nil; //? return d; } -(void)reserialize: (NSDictionary *)dict { // { 'sl3' : 'uthernet' } // special case for smartport since the name isn't used. if (_type == kSlotFDC) { SlotOption *option = [_options objectAtIndex: _selectedIndex]; [option reserialize: dict]; return; } // special case for child options since _name is incorrect. // _name is :rs232. should be set to -sl3:ssc:rs232 :/ #if 0 if (!_title) { BOOL found = NO; unsigned ix = 0; for (SlotOption *option in _options) { NSString *keyPath = [option keyPath]; NSString *value = [dict objectForKey: keyPath]; if (value && [value isEqualToString: [option value]]) { [self setSelectedIndex: ix]; [option reserialize: dict]; found = YES; break; } ++ix; } return; } #endif NSString *value = [dict objectForKey: _name]; if (!value) { //[self reset]; return; } // find it... BOOL found = NO; unsigned ix = 0; for (SlotOption *option in _options) { if ([value isEqualToString: [option value]]) { [self setSelectedIndex: ix]; [option reserialize: dict]; found = YES; break; } ++ix; } } -(Media)selectedMedia { if (_selectedIndex < 0) return EmptyMedia; if (_selectedIndex >= [_options count]) return EmptyMedia; Media media = { 0 }; SlotOption *option = [_options objectAtIndex: _selectedIndex]; [option buildMedia: &media]; return media; } -(NSString *)selectedValue { if (_selectedIndex < 0) return nil; if (_selectedIndex >= [_options count]) return nil; return [[_options objectAtIndex: _selectedIndex] value]; } -(NSArray *)selectedChildren { if (_selectedIndex < 0) return nil; if (_selectedIndex >= [_options count]) return nil; return [[_options objectAtIndex: _selectedIndex] children]; } -(id)copyWithZone:(NSZone *)zone { Slot *child = [Slot new]; child->_index = _index; child->_type = _type; child->_defaultIndex = _defaultIndex; child->_selectedIndex = _selectedIndex; child->_name = [_name copyWithZone: zone]; child->_title = [_title copyWithZone: zone]; child->_options = DeepCopyArray(_options); #if 0 // menu could still be in use by an off-screen pop up button, so it can't be cached. child->_menuItems = DeepCopyArray(_menuItems); // update represented object. NSInteger index = 0; for (NSMenuItem *item in child->_menuItems) { [item setRepresentedObject: child->_options[index]]; ++index; } #endif return child; } -(void)setKeyPath { if (![_name length]) return; for (SlotOption *o in _options) [o setKeyPath: _name]; } -(void)setKeyPath: (NSString *)path { // extra logic for -fdc:0, -0, -sl6:0, etc, built-in slots. unichar c = [_name characterAtIndex: 0]; NSString *p = nil; if (c == ':') p = [path stringByAppendingString: _name]; else if (c == '-') p = _name; else p = [@"-" stringByAppendingString: _name]; for (SlotOption *o in _options) { [o setKeyPath: p]; } // set up child name so bookmarks work. //if (c == ':') _name = p; if (c != '-') _name = p; } -(instancetype)initWithDictionary: (NSDictionary *)data devices: (NSDictionary *)devices { return [self initWithDictionary: data devices: devices index: 0]; } -(instancetype)initWithDictionary: (NSDictionary *)data devices: (NSDictionary *)devices index: (NSInteger)index { BOOL topLevel = NO; _selectedIndex = -1; _defaultIndex = -1; _index = index; _name = [data objectForKey: @"name"]; _title = [data objectForKey: @"description"]; NSNumber *x = [TypeMap objectForKey: _name]; if (x) _type = [x intValue]; if (index && index < 0x10000) { topLevel = YES; _name = [@"-" stringByAppendingString: _name]; _title = [_title stringByAppendingString: @":"]; } NSArray *op = [data objectForKey: @"options"]; NSMutableArray *options = [NSMutableArray arrayWithCapacity: [op count]]; NSInteger ix = 0; for (NSDictionary *d in op) { SlotOption *o = [[SlotOption alloc] initWithDictionary: d devices: devices]; if ([o isDefault]) { _defaultIndex = ix; } ++ix; if (topLevel) { [o setKeyPath: _name]; NSArray *tmp = [o children]; for (Slot *x in tmp) { [x setIndex: _index | 0x10000]; } } [options addObject: o]; } _options = options; _selectedIndex = _defaultIndex; if (_selectedIndex < 0) _selectedIndex = 0; //if (topLevel) [self setKeyPath]; return self; } -(NSArray *)menuItems { //if (_menuItems) return _menuItems; NSMutableArray *menuItems = [NSMutableArray arrayWithCapacity: [_options count]]; for (SlotOption *o in _options) { [menuItems addObject: [o menuItem]]; } //_menuItems = tmp; return menuItems; } #if 0 -(void)loadDeviceSlots: (NSDictionary *)devices { for (SlotOption *s in _options) { [s loadDeviceSlots: devices]; } } #endif -(void)prepareView: (SlotTableCellView *)view { // can't cache the menu items since they // may still be in use. NSButton *hb = [view hamburgerButton]; NSPopUpButton *button = [view menuButton]; NSTextField *text = [view textField]; [view setObjectValue: self]; [text setObjectValue: _title]; [button unbind: @"selectedIndex"]; NSMenu *menu = [button menu]; NSArray *menuItems = [self menuItems]; // [menu setItemArray: ] doesn't work prior to 10.14, apparently. [menu removeAllItems]; if (_type == kSlotFDC) { //[menu setItemArray: @[]]; [button setHidden: YES]; } else { //[menu setItemArray: menuItems]; for (NSMenuItem *x in menuItems) [menu addItem: x]; [button bind: @"selectedIndex" toObject: self withKeyPath: @"selectedIndex" options: nil]; [button setHidden: NO]; } [button setTag: _index]; [hb setTag: _index]; // hb visible status bound in xib. } /* https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVODependentKeys.html */ + (NSSet *)keyPathsForValuesAffectingSelectedItem { return [NSSet setWithObject: @"selectedIndex"]; } @end @implementation SlotOption @synthesize isDefault = _default; #if 0 -(instancetype)initWithDictionary: (NSDictionary *)dictionary { _default = [(NSNumber *)[dictionary objectForKey: @"default"] boolValue]; _disabled = [(NSNumber *)[dictionary objectForKey: @"disabled"] boolValue]; _value = [dictionary objectForKey: @"value"]; //_devName = [dictionary objectForKey: @"devName"]; _title = [dictionary objectForKey: @"description"]; _media = MediaFromDictionary([dictionary objectForKey: @"media"]); //_media = [dictionary objectForKey: @"media"]; return self; } #endif -(instancetype)initWithDictionary: (NSDictionary *)data devices: (NSDictionary *)devices { _default = [(NSNumber *)[data objectForKey: @"default"] boolValue]; _disabled = [(NSNumber *)[data objectForKey: @"disabled"] boolValue]; _value = [data objectForKey: @"value"]; _title = [data objectForKey: @"description"]; _media = MediaFromDictionary([data objectForKey: @"media"]); NSString *devName = [data objectForKey: @"devname"]; if (devName && devices) { NSArray *tmp = [devices objectForKey: devName]; if (tmp) _children = DeepCopyArray(tmp); } return self; } -(void)reset { for (Slot *s in _children) { [s reset]; } } -(NSMenuItem *)menuItem { NSMenuItem *item; extern NSAttributedString *ItalicMenuString(NSString *); item = [[NSMenuItem alloc] initWithTitle: _title action: NULL keyEquivalent: @""]; if (_disabled) { [item setEnabled: NO]; } if (_default) { [item setAttributedTitle: ItalicMenuString(_title)]; } [item setRepresentedObject: self]; return item; } -(id)copyWithZone:(NSZone *)zone { SlotOption *child = [SlotOption new]; child->_default = _default; child->_disabled = _disabled; child->_media = _media; child->_value = [_value copyWithZone: zone]; //child->_devName = [_devName copyWithZone: zone]; child->_title = [_title copyWithZone: zone]; //child->_media = [_media copyWithZone: zone]; child->_keyPath = [_keyPath copyWithZone: zone]; child->_children = DeepCopyArray(_children); return child; } -(void)buildArgs: (NSMutableArray *)args { BOOL explicit = !_default; Slot *s = [_children firstObject]; BOOL bios = [s type] == kSlotBIOS && ![[s selectedItem] isDefault]; if (bios) explicit = YES; if (explicit) { NSString *value = _value; [args addObject: _keyPath]; if (bios) { value = [NSString stringWithFormat: @"%@,bios=%@", _value, [s selectedValue]]; } [args addObject: value]; } for (Slot *s in _children) { if ([s type] == kSlotBIOS) continue; [[s selectedItem] buildArgs: args]; } } -(void)buildMedia: (Media *)media { #undef _ #define _(name) media->name += _media.name _(cass); _(cdrom); _(hard); _(floppy_3_5); _(floppy_5_25); _(floppy_8); _(pseudo_disk); _(bitbanger); _(midiin); _(midiout); _(picture); _(rom); #undef _ for (Slot *s in _children) { [[s selectedItem] buildMedia: media]; } } -(void)reserialize: (NSDictionary *)dict { #if 0 NSString *value = [dict objectForKey: _keyPath]; if (value) { // don't need to do anything since set by slot. } #endif for (Slot *s in _children) { [s reserialize: dict]; } } -(void)buildSerial: (NSMutableDictionary *)dict { if (!_default) [dict setObject: _value forKey: _keyPath]; for (Slot *s in _children) [[s selectedItem] buildSerial: dict]; } // propogate -(void)setKeyPath: (NSString *)path { _keyPath = path; if (!_children) return; NSString *p = path; if ([_value length]) p = [path stringByAppendingFormat: @":%@", _value]; for (Slot *s in _children) { [s setKeyPath: p]; } } -(NSString *)keyPath { return _keyPath; } #if 0 -(BOOL)loadDeviceSlots: (NSDictionary *)devices { NSArray *o = [devices objectForKey: _devName]; if (!o) return NO; _children = DeepCopyArray(o); return YES; } #endif @end @implementation SlotTableCellView @end extern NSString *InternString(NSString *); NSDictionary *BuildDevices(NSArray *array) { #if 0 static NSCache *cache = nil; if (!cache) { cache = [NSCache new]; } #endif NSMutableDictionary *rv = [NSMutableDictionary dictionaryWithCapacity: [array count]]; for (NSDictionary *d in array) { NSString *name = [d objectForKey: @"name"]; NSArray *slots = [d objectForKey: @"slots"]; if (!name) continue; if (!slots) continue; #if 0 name = InternString(name); id x = [cache objectForKey: name]; if (x) { [rv setObject: x forKey: name]; continue; } #endif NSArray *data = MapArray(slots, ^(id o){ Slot *s = [[Slot alloc] initWithDictionary: o devices: nil index: 0]; return s; }); [rv setObject: data forKey: name]; } return rv; } NSArray *BuildSlots(NSString *name, NSDictionary *data) { static NSCache *cache = nil; if (!cache) { cache = [NSCache new]; } name = InternString(name); NSArray *x = [cache objectForKey: name]; if (x) { return x; } NSArray *slots = [data objectForKey: @"slots"]; NSMutableArray *rv = [NSMutableArray arrayWithCapacity: [slots count]]; NSDictionary *devices = BuildDevices([data objectForKey: @"devices"]); unsigned ix = 0; for (NSDictionary *d in slots) { Slot *s = [[Slot alloc] initWithDictionary: d devices: devices index: ++ix]; [rv addObject: s]; } [cache setObject: rv forKey: name]; return rv; }