mkxp-z/macos/views/SettingsMenuController.mm
Wayward Heart 2622a84c53 Add @autoreleasepool blocks to the macOS specific files.
Without these blocks the strings aren't released until the thread that created them is closed, which means file accesses especially leak memory.

Also refocus the game when closing the keybindings window on macOS.
2024-08-02 09:26:51 -05:00

521 lines
16 KiB
Text

//
// 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 <GameController/GameController.h>
#include <Availability.h>
#import <SDL_scancode.h>
#import <SDL_keyboard.h>
#import <SDL_video.h>
#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<NSNumber*> 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<NSNumber*, BindingIndexArray*> *nsbinds;
NSMutableDictionary<NSNumber*, NSString*> *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];
[s checkController];
return s;
}
-(SettingsMenu*)raise {
if (_window)
[_window makeKeyAndOrderFront:self];
return self;
}
-(void)closeWindow {
[self setNotListening:true];
[_window close];
SDL_RaiseWindow(shState->rtData().window);
}
-(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)
#if defined(__MAC_10_14_1) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14_1
// Requires macOS 10.14.1+
checkButtonAlt(leftThumbstickButton, SDL_CONTROLLER_BUTTON_LEFTSTICK, 14)
checkButtonAlt(rightThumbstickButton, SDL_CONTROLLER_BUTTON_RIGHTSTICK, 14)
#else
#warning "This SDK doesn't support the detection of thumbstick buttons. You will not be able to rebind them from the menu on 10.14.1 or higher."
#endif
#if defined(__MAC_10_15) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_15
// Requires macOS 10.15+
checkButton(Menu, SDL_CONTROLLER_BUTTON_START, 15)
checkButton(Options, SDL_CONTROLLER_BUTTON_BACK, 15)
#else
#warning "This SDK doesn't support the detection of Start and Back buttons. You will not be able to rebind them from the menu on 10.15 or higher."
#endif
#if defined(__MAC_11_0) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_11_0
// Requires macOS 11.0+
checkButton(Home, SDL_CONTROLLER_BUTTON_GUIDE, 16)
#else
#warning "This SDK doesn't support the detection of the Guide button. You will not be able to rebind it from the menu on 11.0 or higher."
#endif
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<NSString*> *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<NSNumber*> *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;
[self checkController];
}
- (void)checkController {
NSArray<GCController*>* 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