RetroArch/apple/OSX/settings.m

515 lines
16 KiB
Objective-C

/* RetroArch - A frontend for libretro.
* Copyright (C) 2013 - Jason Fetters
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#import <objc/runtime.h>
#import "apple/common/RetroArch_Apple.h"
#include "apple/common/setting_data.h"
#include "apple/common/apple_input.h"
#include "driver.h"
#include "input/input_common.h"
struct settings fake_settings;
struct global fake_extern;
static const void* associated_name_tag = (void*)&associated_name_tag;
#define BINDFOR(s) (*(struct retro_keybind*)(&s)->value)
static const char* key_name_for_id(uint32_t hidkey)
{
for (int i = 0; apple_key_name_map[i].hid_id; i ++)
if (apple_key_name_map[i].hid_id == hidkey)
return apple_key_name_map[i].keyname;
return "nul";
}
static uint32_t key_id_for_name(const char* name)
{
for (int i = 0; apple_key_name_map[i].hid_id; i ++)
if (strcmp(name, apple_key_name_map[i].keyname) == 0)
return apple_key_name_map[i].hid_id;
return 0;
}
#define key_name_for_rk(X) key_name_for_id(input_translate_rk_to_keysym(X))
#define key_rk_for_name(X) input_translate_keysym_to_rk(key_id_for_name(X))
static const char* get_input_config_key(const rarch_setting_t* setting, const char* type)
{
static char buffer[32];
if (setting->input_player)
snprintf(buffer, 32, "input_player%d_%s%c%s", setting->input_player, setting->name, type ? '_' : '\0', type);
else
snprintf(buffer, 32, "input_%s%c%s", setting->name, type ? '_' : '\0', type);
return buffer;
}
static const char* get_button_name(const rarch_setting_t* setting)
{
static char buffer[32];
if (BINDFOR(*setting).joykey == NO_BTN)
return "nul";
snprintf(buffer, 32, "%lld", BINDFOR(*setting).joykey);
return buffer;
}
static const char* get_axis_name(const rarch_setting_t* setting)
{
static char buffer[32];
uint32_t joyaxis = BINDFOR(*setting).joyaxis;
if (AXIS_NEG_GET(joyaxis) != AXIS_DIR_NONE)
snprintf(buffer, 8, "-%d", AXIS_NEG_GET(joyaxis));
else if (AXIS_POS_GET(joyaxis) != AXIS_DIR_NONE)
snprintf(buffer, 8, "+%d", AXIS_POS_GET(joyaxis));
else
return "nul";
return buffer;
}
@interface RANumberFormatter : NSNumberFormatter
@end
@implementation RANumberFormatter
- (id)initWithFloatSupport:(bool)allowFloat minimum:(double)min maximum:(double)max
{
self = [super init];
self.allowsFloats = allowFloat;
self.maximumFractionDigits = 10;
if (min || max)
{
self.minimum = @(min);
self.maximum = @(max);
}
return self;
}
- (BOOL)isPartialStringValid:(NSString*)partialString newEditingString:(NSString**)newString errorDescription:(NSString**)error
{
bool hasDot = false;
if (partialString.length)
for (int i = 0; i != partialString.length; i ++)
{
unichar ch = [partialString characterAtIndex:i];
if (self.allowsFloats && !hasDot && ch == '.')
{
hasDot = true;
continue;
}
if (!isnumber(ch))
return NO;
}
return YES;
}
@end
@interface RAInputBinder : NSWindow
@end
@implementation RAInputBinder
- (IBAction)goAway:(id)sender
{
[NSApp endSheet:self];
[self orderOut:nil];
}
// Stop the annoying sound when pressing a key
- (void)keyDown:(NSEvent*)theEvent
{
}
@end
@interface RASettingCell : NSTableCellView
@property (nonatomic) const rarch_setting_t* setting;
@property (nonatomic) NSString* stringValue;
@property (nonatomic) IBOutlet NSNumber* numericValue;
@property (nonatomic) bool booleanValue;
@property (nonatomic) NSTimer* bindTimer;
@end
@implementation RASettingCell
- (void)setSetting:(const rarch_setting_t *)aSetting
{
_setting = aSetting;
if (!_setting)
return;
if (aSetting->type == ST_INT || aSetting->type == ST_FLOAT)
{
self.textField.formatter = [[RANumberFormatter alloc] initWithFloatSupport:aSetting->type == ST_FLOAT
minimum:aSetting->min
maximum:aSetting->max];
}
else
self.textField.formatter = nil;
// Set value
switch (aSetting->type)
{
case ST_INT: self.numericValue = @(*(int*)aSetting->value); break;
case ST_FLOAT: self.numericValue = @(*(float*)aSetting->value); break;
case ST_STRING: self.stringValue = @((const char*)aSetting->value); break;
case ST_PATH: self.stringValue = @((const char*)aSetting->value); break;
case ST_BOOL: self.booleanValue = *(bool*)aSetting->value; break;
case ST_BIND: [self updateInputString]; break;
default: break;
}
}
- (IBAction)doBrowse:(id)sender
{
NSOpenPanel* panel = [NSOpenPanel new];
[panel runModal];
if (panel.URLs.count == 1)
self.stringValue = panel.URL.path;
}
- (void)setNumericValue:(NSNumber *)numericValue
{
_numericValue = numericValue;
if (_setting && _setting->type == ST_INT)
*(int*)_setting->value = _numericValue.intValue;
else if (_setting && _setting->type == ST_FLOAT)
*(float*)_setting->value = _numericValue.floatValue;
}
- (void)setBooleanValue:(bool)booleanValue
{
_booleanValue = booleanValue;
if (_setting && _setting->type == ST_BOOL)
*(bool*)_setting->value = _booleanValue;
}
- (void)setStringValue:(NSString *)stringValue
{
_stringValue = stringValue;
if (_setting && (_setting->type == ST_STRING || _setting->type == ST_PATH))
strlcpy(_setting->value, _stringValue.UTF8String, _setting->size);
}
// Input Binding
- (void)updateInputString
{
self.stringValue = [NSString stringWithFormat:@"[KB:%s] [JS:%s] [AX:%s]", key_name_for_rk(BINDFOR(*_setting).key),
get_button_name(_setting),
get_axis_name(_setting)];
}
- (void)dismissBinder
{
[self.bindTimer invalidate];
self.bindTimer = nil;
[self updateInputString];
[(id)self.window.attachedSheet goAway:nil];
}
- (void)checkBind:(NSTimer*)send
{
// Keyboard
for (int i = 0; apple_key_name_map[i].hid_id; i++)
{
if (g_current_input_data.keys[apple_key_name_map[i].hid_id])
{
BINDFOR(*_setting).key = input_translate_keysym_to_rk(apple_key_name_map[i].hid_id);
[self dismissBinder];
return;
}
}
// Joystick
if (g_current_input_data.pad_buttons[0])
{
for (int i = 0; i != 32; i ++)
{
if (g_current_input_data.pad_buttons[0] & (1 << i))
{
BINDFOR(*_setting).joykey = i;
[self dismissBinder];
return;
}
}
}
// Pad Axis
for (int i = 0; i < 4; i++)
{
int16_t value = g_current_input_data.pad_axis[0][i];
if (abs(value) > 0x4000)
{
BINDFOR(*_setting).joyaxis = (value > 0x1000) ? AXIS_POS(i) : AXIS_NEG(i);
[self dismissBinder];
break;
}
}
}
- (IBAction)doGetBind:(id)sender
{
static NSWindowController* controller;
if (!controller)
controller = [[NSWindowController alloc] initWithWindowNibName:@"InputBinder"];
self.bindTimer = [NSTimer timerWithTimeInterval:.1f target:self selector:@selector(checkBind:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.bindTimer forMode:NSModalPanelRunLoopMode];
[NSApp beginSheet:controller.window modalForWindow:self.window modalDelegate:nil didEndSelector:nil contextInfo:nil];
}
@end
@protocol RASettingView
@property const rarch_setting_t* setting;
@end
@interface RASettingsDelegate : NSObject<NSTableViewDataSource, NSTableViewDelegate,
NSOutlineViewDataSource, NSOutlineViewDelegate,
NSWindowDelegate>
@end
@implementation RASettingsDelegate
{
NSWindow IBOutlet* _inputWindow;
NSTableView IBOutlet* _table;
NSOutlineView IBOutlet* _outline;
NSMutableArray* _settings;
NSMutableArray* _currentGroup;
}
- (void)awakeFromNib
{
NSMutableArray* thisGroup = nil;
NSMutableArray* thisSubGroup = nil;
_settings = [NSMutableArray array];
memcpy(&fake_settings, &g_settings, sizeof(struct settings));
memcpy(&fake_extern, &g_extern, sizeof(struct global));
for (int i = 0; setting_data[i].type; i ++)
{
switch (setting_data[i].type)
{
case ST_GROUP:
{
thisGroup = [NSMutableArray array];
objc_setAssociatedObject(thisGroup, associated_name_tag, [NSString stringWithFormat:@"%s", setting_data[i].name], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
case ST_END_GROUP:
{
[_settings addObject:thisGroup];
thisGroup = nil;
break;
}
case ST_SUB_GROUP:
{
thisSubGroup = [NSMutableArray array];
objc_setAssociatedObject(thisSubGroup, associated_name_tag, [NSString stringWithFormat:@"%s", setting_data[i].name], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
case ST_END_SUB_GROUP:
{
[thisGroup addObject:thisSubGroup];
thisSubGroup = nil;
break;
}
default:
{
[thisSubGroup addObject:[NSNumber numberWithInt:i]];
break;
}
}
}
[self load];
}
- (void)load
{
config_file_t* conf = config_file_new([RetroArch_OSX get].configPath.UTF8String);
for (int i = 0; setting_data[i].type; i ++)
{
switch (setting_data[i].type)
{
case ST_BOOL: config_get_bool (conf, setting_data[i].name, (bool*)setting_data[i].value); break;
case ST_INT: config_get_int (conf, setting_data[i].name, (int*)setting_data[i].value); break;
case ST_FLOAT: config_get_float (conf, setting_data[i].name, (float*)setting_data[i].value); break;
case ST_PATH: config_get_array (conf, setting_data[i].name, (char*)setting_data[i].value, setting_data[i].size); break;
case ST_STRING: config_get_array (conf, setting_data[i].name, (char*)setting_data[i].value, setting_data[i].size); break;
case ST_BIND:
{
input_config_parse_key (conf, "input_player1", setting_data[i].name, setting_data[i].value);
input_config_parse_joy_button(conf, "input_player1", setting_data[i].name, setting_data[i].value);
input_config_parse_joy_axis (conf, "input_player1", setting_data[i].name, setting_data[i].value);
break;
}
case ST_HEX: break;
default: break;
}
}
config_file_free(conf);
}
- (void)windowWillClose:(NSNotification *)notification
{
config_file_t* conf = config_file_new([RetroArch_OSX get].configPath.UTF8String);
conf = conf ? conf : config_file_new(0);
for (int i = 0; setting_data[i].type; i ++)
{
switch (setting_data[i].type)
{
case ST_BOOL: config_set_bool (conf, setting_data[i].name, * (bool*)setting_data[i].value); break;
case ST_INT: config_set_int (conf, setting_data[i].name, * (int*)setting_data[i].value); break;
case ST_FLOAT: config_set_float (conf, setting_data[i].name, *(float*)setting_data[i].value); break;
case ST_PATH: config_set_string(conf, setting_data[i].name, (char*)setting_data[i].value); break;
case ST_STRING: config_set_string(conf, setting_data[i].name, (char*)setting_data[i].value); break;
case ST_BIND:
{
config_set_string(conf, get_input_config_key(&setting_data[i], 0 ), key_name_for_rk(BINDFOR(setting_data[i]).key));
config_set_string(conf, get_input_config_key(&setting_data[i], "btn" ), get_button_name(&setting_data[i]));
config_set_string(conf, get_input_config_key(&setting_data[i], "axis"), get_axis_name(&setting_data[i]));
break;
}
case ST_HEX: break;
default: break;
}
}
config_file_write(conf, [RetroArch_OSX get].configPath.UTF8String);
config_file_free(conf);
apple_refresh_config();
[NSApp stopModal];
}
#pragma mark View Builders
- (NSView*)labelAccessoryFor:(NSString*)text onTable:(NSTableView*)table
{
RASettingCell* result = [table makeViewWithIdentifier:@"RALabelSetting" owner:nil];
result.stringValue = text;
return result;
}
#pragma mark Section Table
- (NSInteger)numberOfRowsInTableView:(NSTableView*)view
{
return _settings.count;
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
return [self labelAccessoryFor:objc_getAssociatedObject(_settings[row], associated_name_tag) onTable:tableView];
}
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification
{
_currentGroup = _settings[_table.selectedRow];
[_outline reloadData];
}
#pragma mark Setting Outline
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
return (item == nil) ? _currentGroup.count : [item count];
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
{
return (item == nil) ? _currentGroup[index] : [item objectAtIndex:index];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
{
return [item isKindOfClass:[NSArray class]];
}
- (BOOL)validateProposedFirstResponder:(NSResponder *)responder forEvent:(NSEvent *)event {
return YES;
}
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
if ([item isKindOfClass:[NSArray class]])
{
if ([tableColumn.identifier isEqualToString:@"title"])
return [self labelAccessoryFor:objc_getAssociatedObject(item, associated_name_tag) onTable:outlineView];
else
return [self labelAccessoryFor:[NSString stringWithFormat:@"%d items", (int)[item count]] onTable:outlineView];
}
else
{
const rarch_setting_t* setting = &setting_data[[item intValue]];
if ([tableColumn.identifier isEqualToString:@"title"])
return [self labelAccessoryFor:@(setting->short_description) onTable:outlineView];
else if([tableColumn.identifier isEqualToString:@"accessory"])
{
RASettingCell* s = nil;
switch (setting->type)
{
case ST_BOOL: s = [outlineView makeViewWithIdentifier:@"RABooleanSetting" owner:nil]; break;
case ST_INT: s = [outlineView makeViewWithIdentifier:@"RANumericSetting" owner:nil]; break;
case ST_FLOAT: s = [outlineView makeViewWithIdentifier:@"RANumericSetting" owner:nil]; break;
case ST_PATH: s = [outlineView makeViewWithIdentifier:@"RAPathSetting" owner:nil]; break;
case ST_STRING: s = [outlineView makeViewWithIdentifier:@"RAStringSetting" owner:nil]; break;
case ST_BIND: s = [outlineView makeViewWithIdentifier:@"RABindSetting" owner:nil]; break;
default: break;
}
s.setting = setting;
return s;
}
}
return nil;
}
@end