// // SettingsMenuController.m // mkxp-z // // Created by ゾロアーク on 1/15/21. // // This is a pretty rudimentary keybinding menu, and it replaces the normal one // for macOS. The normal one basically just doesn't seem to work with ANGLE, // so I cooked this one up in a hurry despite knowing next to zero about Xcode's // interface builder in general. // Yes, it is still a mess, but it is working. #import #import #import #import #import "sdl_codes.h" #import "SettingsMenuController.h" #import "input/input.h" #import "input/keybindings.h" #import "eventthread.h" #import "sharedstate.h" #import "config.h" #import "assert.h" static const int inputMapRowToCode[] { Input::Down, Input::Left, Input::Right, Input::Up, Input::A, Input:: B, Input::C, Input::X, Input::Y, Input::Z, Input::L, Input::R }; typedef NSMutableArray BindingIndexArray; @implementation SettingsMenu { __weak IBOutlet NSWindow *_window; __weak IBOutlet NSTableView *_table; __weak IBOutlet NSBox *bindingBox; // Binding buttons __weak IBOutlet NSButton *bindingButton1; __weak IBOutlet NSButton *bindingButton2; __weak IBOutlet NSButton *bindingButton3; __weak IBOutlet NSButton *bindingButton4; // For keeping track of which buttons to check for int sysver; // MKXP Keybindings BDescVec *binds; int currentButtonCode; // Whether currently waiting for some kind of input bool isListening; // For the current binding selection when the table is // reloaded from deleting/adding keybinds bool keepCurrentButtonSelection; NSMutableDictionary *nsbinds; NSMutableDictionary *bindingNames; } +(SettingsMenu*)openWindow { SettingsMenu *s = [[SettingsMenu alloc] initWithNibName:@"settingsmenu" bundle:NSBundle.mainBundle]; // Show the window as a sheet, window events will be sucked up by SDL though //[NSApplication.sharedApplication.mainWindow beginSheet:s.view.window completionHandler:^(NSModalResponse _){}]; // Show the view in a new window instead, so key and controller events // can be captured without SDL's interference NSWindow *win = [NSWindow windowWithContentViewController:s]; win.styleMask &= ~NSWindowStyleMaskResizable; win.styleMask &= ~NSWindowStyleMaskFullScreen; win.styleMask &= NSWindowStyleMaskTitled; win.title = @"Keybindings"; [s setWindow:win]; [win makeKeyAndOrderFront:self]; return s; } -(SettingsMenu*)raise { if (_window) [_window makeKeyAndOrderFront:self]; return self; } -(void)closeWindow { [self setNotListening:true]; [_window close]; } -(SettingsMenu*)setWindow:(NSWindow*)window { _window = window; return self; } - (IBAction)acceptButton:(NSButton *)sender { shState->rtData().bindingUpdateMsg.post(*binds); storeBindings(*binds, shState->config()); [self closeWindow]; } - (IBAction)cancelButton:(NSButton *)sender { [self closeWindow]; } - (IBAction)resetBindings:(NSButton *)sender { binds->clear(); BDescVec tmp = genDefaultBindings(shState->config()); binds->assign(tmp.begin(), tmp.end()); [self setNotListening:false]; } - (void)viewDidLoad { [super viewDidLoad]; sysver = 12; if (@available(macOS 10.14, *)) sysver += 2; if (@available(macOS 10.15, *)) sysver++; if (@available(macOS 11.0, *)) sysver++; isListening = false; keepCurrentButtonSelection = false; [self initDelegateWithTable:_table]; _table.delegate = self; _table.dataSource = self; [self setNotListening:true]; _table.enabled = true; bindingBox.title = @""; [self setButtonNames:0]; } - (void)keyDown:(NSEvent *)event { [super keyDown:event]; if (!isListening) return; BindingDesc d; d.target = (Input::ButtonCode)currentButtonCode; SourceDesc s; s.type = Key; s.d.scan = darwin_scancode_table[event.keyCode]; d.src = s; binds->push_back(d); [self setNotListening:true]; } #define checkButtonStart if (0) {} #define checkButtonEnd else { return; } #define checkButtonElement(e, b, n, min) \ else if (sysver >= min && element == gamepad.e && gamepad.b.isPressed) { \ s.type = CButton; \ s.d.cb = n; \ } #define checkButtonAlt(b, n, min) checkButtonElement(b, b, n, min) #define checkButton(b, n, min) checkButtonAlt(button##b, n, min) #define setAxisData(a, n) \ GCControllerAxisInput *axis = gamepad.a; \ s.type = CAxis; \ s.d.ca.axis = n; \ s.d.ca.dir = (axis.value >= 0) ? AxisDir::Positive : AxisDir::Negative; #define checkAxis(el, a, n) else if (element == gamepad.el && (gamepad.el.a.value >= 0.5 || gamepad.el.a.value <= -0.5)) { setAxisData(el.a, n); } - (void)registerJoystickAction:(GCExtendedGamepad*)gamepad element:(GCControllerElement*)element { if (!isListening) return; BindingDesc d; d.target = (Input::ButtonCode)currentButtonCode; SourceDesc s; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" checkButtonStart checkButton(A, SDL_CONTROLLER_BUTTON_A, 12) checkButton(B, SDL_CONTROLLER_BUTTON_B, 12) checkButton(X, SDL_CONTROLLER_BUTTON_X, 12) checkButton(Y, SDL_CONTROLLER_BUTTON_Y, 12) checkButtonElement(dpad, dpad.up, SDL_CONTROLLER_BUTTON_DPAD_UP, 12) checkButtonElement(dpad, dpad.down, SDL_CONTROLLER_BUTTON_DPAD_DOWN, 12) checkButtonElement(dpad, dpad.left, SDL_CONTROLLER_BUTTON_DPAD_LEFT, 12) checkButtonElement(dpad, dpad.right, SDL_CONTROLLER_BUTTON_DPAD_RIGHT, 12) checkButtonAlt(leftShoulder, SDL_CONTROLLER_BUTTON_LEFTSHOULDER, 12) checkButtonAlt(rightShoulder, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, 12) // Requires macOS 10.14.1+ checkButtonAlt(leftThumbstickButton, SDL_CONTROLLER_BUTTON_LEFTSTICK, 14) checkButtonAlt(rightThumbstickButton, SDL_CONTROLLER_BUTTON_RIGHTSTICK, 14) // Requires macOS 10.15+ checkButton(Menu, SDL_CONTROLLER_BUTTON_START, 15) checkButton(Options, SDL_CONTROLLER_BUTTON_BACK, 15) // Requires macOS 11.0+ checkButton(Home, SDL_CONTROLLER_BUTTON_GUIDE, 16) checkAxis(leftThumbstick, xAxis, SDL_CONTROLLER_AXIS_LEFTX) checkAxis(leftThumbstick, yAxis, SDL_CONTROLLER_AXIS_LEFTY) checkAxis(rightThumbstick, xAxis, SDL_CONTROLLER_AXIS_RIGHTX) checkAxis(rightThumbstick, yAxis, SDL_CONTROLLER_AXIS_RIGHTY) else if (element == gamepad.leftTrigger && (gamepad.leftTrigger.value >= 0.5 || gamepad.leftTrigger.value <= -0.5)) { s.type = CAxis; s.d.ca.axis = SDL_CONTROLLER_AXIS_TRIGGERLEFT; s.d.ca.dir = AxisDir::Positive; } else if (element == gamepad.rightTrigger && (gamepad.rightTrigger.value >= 0.5 || gamepad.rightTrigger.value <= -0.5)) { s.type = CAxis; s.d.ca.axis = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; s.d.ca.dir = AxisDir::Positive; } checkButtonEnd; #pragma clang diagnostic pop d.src = s; binds->push_back(d); [self setNotListening:true]; } +(NSString*)nameForBinding:(SourceDesc&)desc { switch (desc.type) { case Key: if (desc.d.scan == SDL_SCANCODE_LSHIFT) return @"Shift"; return @(SDL_GetScancodeName(desc.d.scan)); case CAxis: return [NSString stringWithFormat:@"%s%s", shState->input().getAxisName(desc.d.ca.axis), desc.d.ca.dir == Negative ? "-" : "+"]; case CButton: return @(shState->input().getButtonName(desc.d.cb)); default: return @"Invalid"; } } -(id)initDelegateWithTable:(NSTableView*)tbl { binds = new BDescVec; nsbinds = [NSMutableDictionary new]; bindingNames = [NSMutableDictionary new]; RGSSThreadData &data = shState->rtData(); #define SET_BINDING(code) bindingNames[@(Input::code)] = @(#code) #define SET_BINDING_CUSTOM(code, value) bindingNames[@(Input::code)] = @(value) SET_BINDING(Down); SET_BINDING(Left); SET_BINDING(Right); SET_BINDING(Up); SET_BINDING(A); SET_BINDING(B); SET_BINDING(C); SET_BINDING(X); SET_BINDING(Y); SET_BINDING(Z); SET_BINDING(L); SET_BINDING(R); #define SET_BINDING_CONF(code, value) \ if (!data.config.kbActionNames.value.empty()) bindingNames[@(Input::code)] = \ @(data.config.kbActionNames.value.c_str()) SET_BINDING_CONF(A,a); SET_BINDING_CONF(B,b); SET_BINDING_CONF(C,c); SET_BINDING_CONF(X,x); SET_BINDING_CONF(Y,y); SET_BINDING_CONF(Z,z); SET_BINDING_CONF(L,l); SET_BINDING_CONF(R,r); BDescVec oldBinds; data.bindingUpdateMsg.get(oldBinds); if (oldBinds.size() <= 0) { BDescVec defaults = genDefaultBindings(data.config); binds->assign(defaults.begin(), defaults.end()); } else { binds->assign(oldBinds.begin(), oldBinds.end()); } [self loadBinds]; [self enableButtons:false]; return self; } - (void) loadBinds { [nsbinds removeAllObjects]; for (int i = 0; i < sizeof(inputMapRowToCode) / sizeof(Input::ButtonCode); i++) nsbinds[@(inputMapRowToCode[i])] = [NSMutableArray new]; for (int i = 0; i < binds->size(); i++) { NSNumber *key = @(binds->at(i).target); if (nsbinds[key] == nil) { nsbinds[key] = [NSMutableArray new]; } NSMutableArray *b = nsbinds[key]; [b addObject:@(i)]; } } - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { return nsbinds.count; } - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { NSString *tableID = tableColumn.identifier; int buttonCode = inputMapRowToCode[row]; NSTableCellView *cell = [tableView makeViewWithIdentifier:tableID owner:self]; if ([tableID isEqualToString:@"action"]) { cell.textField.stringValue = bindingNames[@(inputMapRowToCode[row])]; cell.textField.font = [NSFont boldSystemFontOfSize:cell.textField.font.pointSize]; return cell; } long col = tableID.integerValue; if (nsbinds[@(buttonCode)].count < col) { cell.textField.stringValue = @""; return cell; } NSNumber *d = nsbinds[@(buttonCode)][col-1]; BindingDesc &bind = binds->at(d.intValue); cell.textField.stringValue = [SettingsMenu nameForBinding:bind.src]; return cell; } - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { NSString *tableID = tableColumn.identifier; int buttonCode = inputMapRowToCode[row]; if ([tableID isEqualToString:@"action"]) { return @(buttonCode); } long col = tableID.integerValue; if (nsbinds[@(buttonCode)].count < col) { return 0; } NSNumber *d = nsbinds[@(buttonCode)][col-1]; BindingDesc &bind = binds->at(d.intValue); return @(bind.src.d.scan); } - (void)tableViewSelectionDidChange:(NSNotification *)notification { if (isListening) isListening = false; if (keepCurrentButtonSelection) { if (keepCurrentButtonSelection) keepCurrentButtonSelection = false; [_table deselectRow:_table.selectedRow]; [self setButtonNames:currentButtonCode]; bindingBox.title = bindingNames[@(currentButtonCode)]; return; } if (_table.selectedRow > -1) { int buttonCode = inputMapRowToCode[_table.selectedRow]; currentButtonCode = buttonCode; [self setButtonNames:buttonCode]; bindingBox.title = bindingNames[@(currentButtonCode)]; } else { [self setButtonNames:0]; bindingBox.title = @""; } } - (int)setButtonNames:(int)input { if (!input) { bindingButton1.title = @""; bindingButton2.title = bindingButton1.title; bindingButton3.title = bindingButton1.title; bindingButton4.title = bindingButton1.title; [self enableButtons:false]; return 0; } BindingIndexArray *nsbind = nsbinds[@(input)]; NSMutableArray *pnames = [NSMutableArray new]; for (int i = 0; i < 4; i++) { if (!nsbind.count || i > nsbind.count - 1) { [pnames addObject:@"(Empty)"]; } else { BindingDesc &b = binds->at(nsbind[i].intValue); NSString *bindingName = [SettingsMenu nameForBinding:b.src]; [pnames addObject:bindingName]; } } bindingButton1.title = pnames[0]; bindingButton2.title = pnames[1]; bindingButton3.title = pnames[2]; bindingButton4.title = pnames[3]; [self enableButtons:true]; return (int)pnames.count; } - (void)enableButtons:(bool)defaultSetting { BindingIndexArray *currentBind = nsbinds[@(currentButtonCode)]; bindingButton1.enabled = defaultSetting; bindingButton2.enabled = defaultSetting && currentBind.count > 0; bindingButton3.enabled = defaultSetting && currentBind.count > 1; bindingButton4.enabled = defaultSetting && currentBind.count > 2; } - (IBAction)binding1Clicked:(NSButton *)sender { if (nsbinds[@(currentButtonCode)].count > 0) { [self removeBinding:0 forInput:currentButtonCode]; return; } [self setListening:sender]; } - (IBAction)binding2Clicked:(NSButton *)sender { if (nsbinds[@(currentButtonCode)].count > 1) { [self removeBinding:1 forInput:currentButtonCode]; return; } [self setListening:sender]; } - (IBAction)binding3Clicked:(NSButton *)sender { if (nsbinds[@(currentButtonCode)].count > 2) { [self removeBinding:2 forInput:currentButtonCode]; return; } [self setListening:sender]; } - (IBAction)binding4Clicked:(NSButton *)sender { if (nsbinds[@(currentButtonCode)].count > 3) { [self removeBinding:3 forInput:currentButtonCode]; return; } [self setListening:sender]; } - (void)removeBinding:(int)bindIndex forInput:(int)input { NSMutableArray *bind = nsbinds[@(input)]; int bi = bind[bindIndex].intValue; binds->erase(binds->begin() + bi); [self setNotListening:true]; } - (void)setListening:(NSButton*)src { if (isListening) { [self setNotListening:true]; return; } // Stops receiving keyDown events if it's disabled, apparently //_table.enabled = false; [self enableButtons:false]; if (src == nil) return; src.title = @"Click to Cancel..."; src.enabled = true; isListening = true; NSArray* controllers = [GCController controllers]; if (controllers.count <= 0) return; GCController *gamepad = controllers[0]; if (gamepad.extendedGamepad == nil || gamepad.extendedGamepad.valueChangedHandler != nil) return; gamepad.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {[self registerJoystickAction:gamepad element:element];}; } - (void)setNotListening:(bool)keepCurrentSelection { [self loadBinds]; keepCurrentButtonSelection = keepCurrentSelection; isListening = false; [_table reloadData]; [self setButtonNames:currentButtonCode]; } -(void)dealloc { delete binds; } @end