Settings screen

This commit is contained in:
Lior Halphon 2023-01-21 19:41:38 +02:00
parent e582e38abc
commit 6ddc3b0f0a
21 changed files with 775 additions and 64 deletions

View File

@ -185,7 +185,7 @@ static void positionView(UIImageView *view, CGPoint position)
}
GB_set_key_mask(_gbView.gb, mask);
if (mask & ~_lastMask) {
if ((mask & ~_lastMask) && ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBButtonHaptics"])) {
[[GBHapticManager sharedManager] doTapHaptic];
}
_lastMask = mask;

View File

@ -1,5 +1,4 @@
#import "GBLoadROMTableViewController.h"
#import "GBTableViewCell.h"
#import "GBROMManager.h"
@interface GBLoadROMTableViewController ()
@ -27,7 +26,7 @@
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
GBTableViewCell *cell = [[GBTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
NSString *rom = [GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]];
cell.textLabel.text = rom;
cell.accessoryType = [rom isEqualToString:[GBROMManager sharedManager].currentROM]? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;

View File

@ -38,7 +38,7 @@
{@"Library", @"bookmark", SelectorString(openLibrary)},
{@"Model", @"ModelTemplate", SelectorString(changeModel)},
{@"States", @"square.stack", nil},
{@"Settings", @"gear", nil},
{@"Settings", @"gear", SelectorString(openSettings)},
{@"About", @"info.circle", SelectorString(showAbout)},
};

View File

@ -58,6 +58,7 @@
- (NSString *)romFileForROM:(NSString *)rom
{
if ([rom isEqualToString:@"Inbox"]) return nil;
if (rom == _currentROM) {
return self.romFile;
}

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
#include <Core/gb.h>
@interface GBSettingsViewController : UITableViewController
+ (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button;
+ (const GB_palette_t *)paletteForTheme:(NSString *)theme;
@end

View File

@ -0,0 +1,595 @@
#import "GBSettingsViewController.h"
#import "GBTemperatureSlider.h"
#import "GBViewBase.h"
#import <objc/runtime.h>
static NSString const *typeSubmenu = @"submenu";
static NSString const *typeOptionSubmenu = @"optionSubmenu";
static NSString const *typeRadio = @"radio";
static NSString const *typeCheck = @"check";
static NSString const *typeDisabled = @"disabled";
static NSString const *typeSeparator = @"separator";
static NSString const *typeSlider = @"slider";
static NSString const *typeLightTemp = @"typeLightTemp";
@implementation GBSettingsViewController
{
NSArray<NSDictionary *> *_structure;
UINavigationController *_detailsNavigation;
}
+ (const GB_palette_t *)paletteForTheme:(NSString *)theme
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([theme isEqualToString:@"Greyscale"]) {
return &GB_PALETTE_GREY;
}
if ([theme isEqualToString:@"Lime (Game Boy)"]) {
return &GB_PALETTE_DMG;
}
if ([theme isEqualToString:@"Olive (Pocket)"]) {
return &GB_PALETTE_MGB;
}
if ([theme isEqualToString:@"Teal (Light)"]) {
return &GB_PALETTE_GBL;
}
static GB_palette_t customPalette;
NSArray *colors = [defaults dictionaryForKey:@"GBThemes"][theme][@"Colors"];
if (colors.count != 5) return &GB_PALETTE_DMG;
unsigned i = 0;
for (NSNumber *color in colors) {
uint32_t c = [color unsignedIntValue];
customPalette.colors[i++] = (struct GB_color_s) {c, c >> 8, c >> 16};
}
return &customPalette;
}
+ (UIColor *) colorFromGBColor:(const struct GB_color_s *)color
{
return [UIColor colorWithRed:color->r / 255.0
green:color->g / 255.0
blue:color->b / 255.0
alpha:1.0];
}
+ (UIImage *)previewImageForTheme:(NSString *)theme
{
const GB_palette_t *palette = [self paletteForTheme:theme];
UIGraphicsBeginImageContextWithOptions((CGSize){29, 29}, false, [UIScreen mainScreen].scale);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 29, 29) cornerRadius:7];
[[self colorFromGBColor:&palette->colors[4]] set];
[path fill];
path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(4, 4, 9, 9) cornerRadius:2];
[[self colorFromGBColor:&palette->colors[0]] set];
[path fill];
path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(16, 4, 9, 9) cornerRadius:2];
[[self colorFromGBColor:&palette->colors[1]] set];
[path fill];
path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(4, 16, 9, 9) cornerRadius:2];
[[self colorFromGBColor:&palette->colors[2]] set];
[path fill];
path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(16, 16, 9, 9) cornerRadius:2];
[[self colorFromGBColor:&palette->colors[3]] set];
[path fill];
UIImage *ret = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return ret;
}
+ (NSArray<NSDictionary *> *)paletteMenu
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *themes = [@[
@"Greyscale",
@"Lime (Game Boy)",
@"Olive (Pocket)",
@"Teal (Light)",
] arrayByAddingObjectsFromArray:[[defaults dictionaryForKey:@"GBThemes"] allKeys]];
NSMutableArray<NSDictionary *> *themeItems = [NSMutableArray arrayWithCapacity:themes.count];
for (NSString *theme in themes) {
[themeItems addObject: @{@"type": typeRadio, @"pref": @"GBCurrentTheme",
@"title": theme, @"value": theme,
@"image": [self previewImageForTheme:theme]}];
}
return @[
@{
@"items": [themeItems subarrayWithRange:(NSRange){0, 4}]
},
@{
@"items": [themeItems subarrayWithRange:(NSRange){4, themeItems.count - 4}]
}
];
}
+ (NSArray<NSDictionary *> *)rootStructure
{
#define QUICK_SUBMENU(title, ...) @{@"type": typeOptionSubmenu, @"title": title, @"submenu": @[@{@"items": __VA_ARGS__}]}
NSArray<NSDictionary *> *emulationMenu = @[
@{
@"header": @"Rewind duration",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"Disabled", @"value": @0,},
@{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"10 Seconds", @"value": @10,},
@{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"30 Seconds", @"value": @30,},
@{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"1 Minute", @"value": @60,},
@{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"2 Minutes", @"value": @120,},
@{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"5 Minutes", @"value": @300,},
@{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"10 Minutes", @"value": @600,},
]
},
@{
@"header": @"Real Time Clock emulation",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBRTCMode", @"title": @"Accurate", @"value": @(GB_RTC_MODE_ACCURATE),},
@{@"type": typeRadio, @"pref": @"GBRTCMode", @"title": @"Sync to system clock", @"value": @(GB_RTC_MODE_SYNC_TO_HOST),},
]
},
@{
@"header": @"Emulated revisions",
@"items": @[
QUICK_SUBMENU(@"Game Boy", @[
@{@"type": typeDisabled, @"title": @"DMG-CPU 0"},
@{@"type": typeDisabled, @"title": @"DMG-CPU A"},
@{@"type": typeRadio, @"pref": @"GBDMGModel", @"title": @"DMG-CPU B", @"value": @(GB_MODEL_DMG_B),},
@{@"type": typeDisabled, @"title": @"DMG-CPU C"},
]),
QUICK_SUBMENU(@"Super Game Boy", @[
@{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy (NTSC)", @"value": @(GB_MODEL_SGB_NTSC),},
@{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy (PAL)", @"value": @(GB_MODEL_SGB_PAL),},
@{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy 2", @"value": @(GB_MODEL_SGB2),},
]),
QUICK_SUBMENU(@"Game Boy Color", @[
@{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB 0 (Experimental)", @"value": @(GB_MODEL_CGB_0),},
@{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB A (Experimental)", @"value": @(GB_MODEL_CGB_A),},
@{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB B (Experimental)", @"value": @(GB_MODEL_CGB_B),},
@{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB C (Experimental)", @"value": @(GB_MODEL_CGB_C),},
@{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB D", @"value": @(GB_MODEL_CGB_D),},
@{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB E", @"value": @(GB_MODEL_CGB_E),},
]),
QUICK_SUBMENU(@"Game Boy Advance", @[
@{@"type": typeDisabled, @"title": @"CPU AGB 0 (Early GBA)",},
@{@"type": typeRadio, @"pref": @"GBAGBModel", @"title": @"CPU AGB A (GBA)", @"value": @(GB_MODEL_AGB_A),},
@{@"type": typeRadio, @"pref": @"GBAGBModel", @"title": @"CPU AGB A (Game Boy Player)", @"value": @(GB_MODEL_GBP_A),},
@{@"type": typeDisabled, @"title": @"CPU AGB B (GBA SP)",},
@{@"type": typeDisabled, @"title": @"CPU AGB E (Late GBA SP)",},
@{@"type": typeDisabled, @"title": @"CPU AGB E (Late Game Boy Player)",},
]),
],
@"footer": @"Changing the emulated revision on active ROMs will take effect after selecting Reset from the menu or changing the emulated model.",
},
];
NSArray<NSDictionary *> *videoMenu = @[
@{
@"items": @[
@{
@"title": @"Graphics filter",
@"type": typeOptionSubmenu,
@"submenu": @[
@{
@"header": @"Standard filters",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Nearest Neighbor (Pixelated)", @"value": @"NearestNeighbor",},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Bilinear (Blurry)", @"value": @"Bilinear",},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Smooth Bilinear (Less blurry)", @"value": @"SmoothBilinear",},
]
},
@{
@"header": @"Screen filters",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Monochrome LCD Display", @"value": @"MonoLCD",},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"LCD Display", @"value": @"LCD",},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"CRT Display", @"value": @"CRT",},
]
},
@{
@"header": @"Upscaling filters",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Scale2x", @"value": @"Scale2x"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Scale4x", @"value": @"Scale4x"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased Scale2x", @"value": @"AAScale2x"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased Scale4x", @"value": @"AAScale4x"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"HQ2x", @"value": @"HQ2x"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale", @"value": @"OmniScale"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale Legacy", @"value": @"OmniScaleLegacy"},
@{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"AA OmniScale Legacy", @"value": @"AAOmniScaleLegacy"},
]
},
]
},
]
},
@{
@"header": @"Color Correction",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Disabled", @"value": @(GB_COLOR_CORRECTION_DISABLED),},
@{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Correct color curves", @"value": @(GB_COLOR_CORRECTION_CORRECT_CURVES),},
@{@"type": typeSeparator},
@{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern Balanced", @"value": @(GB_COLOR_CORRECTION_MODERN_BALANCED),},
@{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern Accurate", @"value": @(GB_COLOR_CORRECTION_MODERN_ACCURATE),},
@{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern Boost contrast", @"value": @(GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST),},
@{@"type": typeSeparator},
@{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Reduce contrast", @"value": @(GB_COLOR_CORRECTION_REDUCE_CONTRAST),},
@{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Harsh reality (low contrast)", @"value": @(GB_COLOR_CORRECTION_LOW_CONTRAST),},
],
@"footer": ^NSString *(){
return (NSString * const[]){
[GB_COLOR_CORRECTION_DISABLED] = @"Colors are directly interpreted as sRGB, resulting in unbalanced colors and inaccurate hues.",
[GB_COLOR_CORRECTION_CORRECT_CURVES] = @"Colors have their brightness corrected, but hues remain unbalanced.",
[GB_COLOR_CORRECTION_MODERN_BALANCED] = @"Emulates a modern display. Blue contrast is moderately enhanced at the cost of slight hue inaccuracy.",
[GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST] = @"Like Modern Balanced, but further boosts the contrast of greens and magentas that is lacking on the original hardware.",
[GB_COLOR_CORRECTION_REDUCE_CONTRAST] = @"Slightly reduce the contrast to better represent the tint and contrast of the original display.",
[GB_COLOR_CORRECTION_LOW_CONTRAST] = @"Harshly reduce the contrast to accurately represent the tint low constrast of the original display.",
[GB_COLOR_CORRECTION_MODERN_ACCURATE] = @"Emulates a modern display. Colors have their hues and brightness corrected.",
}[MIN(GB_COLOR_CORRECTION_MODERN_ACCURATE, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"])];
},
},
@{
@"header": @"Ambient light temperature",
@"items": @[
@{@"type": typeLightTemp, @"pref": @"GBLightTemperature", @"min": @-1, @"max": @1}
],
},
@{
@"header": @"Frame blending",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Disabled", @"value": @(GB_FRAME_BLENDING_MODE_DISABLED),},
@{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Simple", @"value": @(GB_FRAME_BLENDING_MODE_SIMPLE),},
@{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Accurate", @"value": @(GB_FRAME_BLENDING_MODE_ACCURATE),},
]
},
@{
@"items": @[@{
@"title": @"Monochrome palette",
@"type": typeOptionSubmenu,
@"submenu": [self paletteMenu]
}],
@"footer": @"This palette will be used when emulating a monochrome model such as the original Game Boy."
}
];
NSArray<NSDictionary *> *audioMenu = @[
@{
@"header": @"High-pass filter",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Disabled (Keep DC offset)", @"value": @(GB_HIGHPASS_OFF),},
@{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Accurate (Emulate hardware)", @"value": @(GB_HIGHPASS_ACCURATE),},
@{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Preserve waveform", @"value": @(GB_HIGHPASS_REMOVE_DC_OFFSET),},
],
@"footer": ^NSString *(){
return (NSString * const[]){
[GB_HIGHPASS_OFF] = @"No high-pass filter will be applied. DC offset will be kept, pausing and resuming will trigger audio pops.",
[GB_HIGHPASS_ACCURATE] = @"An accurate high-pass filter will be applied, removing the DC offset while somewhat attenuating the bass.",
[GB_HIGHPASS_REMOVE_DC_OFFSET] = @"A high-pass filter will be applied to the DC offset itself, removing the DC offset while preserving the waveform.",
}[MIN(GB_HIGHPASS_REMOVE_DC_OFFSET, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"])];
},
},
@{
@"header": @"Interference volume",
@"items": @[
@{@"type": typeSlider, @"pref": @"GBInterferenceVolume", @"min": @0, @"max": @1, @"minImage": @"speaker.fill", @"maxImage": @"speaker.3.fill"}
],
},
];
NSArray<NSDictionary *> *controlsMenu = @[
@{
@"header": @"D-pad style",
@"items": @[
// TODO: Convert to enum when implemented
@{@"type": typeRadio, @"pref": @"GBDpadMode", @"title": @"Standard", @"value": @(0),},
@{@"type": typeRadio, @"pref": @"GBDpadMode", @"title": @"Swipe", @"value": @(1),},
],
@"footer": ^NSString *(){
return (NSString * const[]){
[0] = @"Directional input is determined by the touch position.",
[1] = @"Directional input is determined by the swipe direction.",
}[MIN(1, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBDpadMode"])];
},
},
@{
@"header": @"Fast-forward and rewind behavior",
@"items": @[
@{@"type": typeCheck, @"pref": @"GBSwipeLock", @"title": @"Lock after swiping"},
],
@"footer": ^NSString *(){
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) {
return @"Swipe right on the Game Boy screen to fast-forward, and swipe left to rewind. Tap on the Game Boy screen to return to normal.";
}
return @"Swipe right on the Game Boy screen to fast-forward, and swipe left to rewind. Raise the touch to return to normal.";
},
},
@{
@"header": @"Quick save and load",
@"items": @[
@{@"type": typeCheck, @"pref": @"GBSwipeState", @"title": @"Swipe to save and load from slot 1"},
],
@"footer": ^NSString *(){
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) {
return @"Swipe down on the Game Boy to save the state into state slot 1. Swipe up to load the state from state slot 1";
}
return @" "; // This space is needed, otherwise UITableView spacing breaks
},
},
@{
@"header": @"Enable rumble",
@"items": @[
@{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Never", @"value": @(GB_RUMBLE_DISABLED),},
@{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"For rumble-enabled Game Paks", @"value": @(GB_RUMBLE_CARTRIDGE_ONLY),},
@{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Always", @"value": @(GB_RUMBLE_ALL_GAMES),},
],
},
@{
@"items": @[
@{@"type": typeCheck, @"pref": @"GBButtonHaptics", @"title": @"Enable button haptics"},
],
},
];
return @[
@{
@"items": @[
@{
@"title": @"Emulation",
@"type": typeSubmenu,
@"submenu": emulationMenu,
@"image": [UIImage imageNamed:@"emulationSettings"],
},
@{
@"title": @"Video",
@"type": typeSubmenu,
@"submenu": videoMenu,
@"image": [UIImage imageNamed:@"videoSettings"],
},
@{
@"title": @"Audio",
@"type": typeSubmenu,
@"submenu": audioMenu,
@"image": [UIImage imageNamed:@"audioSettings"],
},
@{
@"title": @"Controls",
@"type": typeSubmenu,
@"submenu": controlsMenu,
@"image": [UIImage imageNamed:@"controlsSettings"],
},
]
}
];
}
+ (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button
{
GBSettingsViewController *root = [[self alloc] initWithStructure:[self rootStructure] title:@"Settings" style:UITableViewStyleGrouped];
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:root];
[controller.visibleViewController.navigationItem setLeftBarButtonItem:button];
if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) {
return controller;
}
UISplitViewController *split = [[UISplitViewController alloc] init];
UIViewController *blank = [[UIViewController alloc] init];
blank.view.backgroundColor = root.view.backgroundColor;
root->_detailsNavigation = [[UINavigationController alloc] initWithRootViewController:blank];
split.viewControllers = @[controller, root->_detailsNavigation];
split.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
return split;
}
- (instancetype)initWithStructure:(NSArray *)structure title:(NSString *)title style:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (!self) return nil;
self.title = title;
_structure = structure;
return self;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return _structure.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [_structure[section][@"items"] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return _structure[section][@"header"];
}
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
if ([_structure[section][@"footer"] respondsToSelector:@selector(invoke)]) {
return ((NSString *(^)(void))_structure[section][@"footer"])();
}
return _structure[section][@"footer"];
}
- (NSDictionary *)itemForIndexPath:(NSIndexPath *)indexPath
{
return _structure[[indexPath indexAtPosition:0]][@"items"][[indexPath indexAtPosition:1]];
}
- (NSDictionary *)followingItemForIndexPath:(NSIndexPath *)indexPath
{
NSArray *items = _structure[[indexPath indexAtPosition:0]][@"items"];
if ([indexPath indexAtPosition:1] + 1 >= items.count) {
return nil;
}
return items[[indexPath indexAtPosition:1] + 1];
}
+ (void)fixSliderTint:(UIView *)view
{
if ([view isKindOfClass:[UIImageView class]]) {
view.tintColor = [UIColor systemGrayColor];
}
for (UIView *subview in view.subviews) {
[self fixSliderTint:subview];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *item = [self itemForIndexPath:indexPath];
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
cell.textLabel.text = item[@"title"];
if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
if (item[@"type"] == typeOptionSubmenu) {
for (NSDictionary *section in item[@"submenu"]) {
for (NSDictionary *item in section[@"items"]) {
if (item[@"value"] && [([[NSUserDefaults standardUserDefaults] objectForKey:item[@"pref"]] ?: @0) isEqual:item[@"value"]]) {
cell.detailTextLabel.text = item[@"title"];
break;
}
}
}
}
}
else if (item[@"type"] == typeRadio) {
if ([([[NSUserDefaults standardUserDefaults] objectForKey:item[@"pref"]] ?: @0) isEqual:item[@"value"]]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
else if (item[@"type"] == typeCheck) {
UISwitch *button = [[UISwitch alloc] init];
cell.accessoryView = button;
if ([[NSUserDefaults standardUserDefaults] boolForKey:item[@"pref"]]) {
button.on = true;
}
__weak typeof(self) weakSelf = self;
id block = ^(){
[[NSUserDefaults standardUserDefaults] setBool:button.on forKey:item[@"pref"]];
unsigned section = [indexPath indexAtPosition:0];
UITableViewHeaderFooterView *view = [weakSelf.tableView footerViewForSection:section];
view.textLabel.text = [weakSelf tableView:weakSelf.tableView titleForFooterInSection:section];
[UIView setAnimationsEnabled:false];
[weakSelf.tableView beginUpdates];
[view sizeToFit];
[weakSelf.tableView endUpdates];
[UIView setAnimationsEnabled:true];
};
objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
[button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
else if (item[@"type"] == typeDisabled) {
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (@available(iOS 13.0, *)) {
cell.textLabel.textColor = [UIColor separatorColor];
}
else {
cell.textLabel.textColor = [UIColor colorWithWhite:0 alpha:0.75];
}
}
else if (item[@"type"] == typeSeparator) {
cell.backgroundColor = [UIColor clearColor];
cell.separatorInset = UIEdgeInsetsZero;
}
else if (item[@"type"] == typeSlider ||
item[@"type"] == typeLightTemp) {
CGRect rect = cell.contentView.bounds;
rect.size.width -= 24;
rect.size.height -= 24;
rect.origin.x += 12;
rect.origin.y += 12;
UISlider *slider = [item[@"type"] == typeLightTemp? [GBTemperatureSlider alloc] : [UISlider alloc] initWithFrame:rect];
slider.continuous = true;
slider.minimumValue = [item[@"min"] floatValue];
slider.maximumValue = [item[@"max"] floatValue];
slider.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[cell.contentView addSubview:slider];
slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (item[@"minImage"] && item[@"maxImage"]) {
if ([item[@"minImage"] isKindOfClass:[UIImage class]]) {
slider.minimumValueImage = item[@"minImage"];
}
else if (@available(iOS 13.0, *)) {
slider.minimumValueImage = [UIImage systemImageNamed:item[@"minImage"]];
}
if ([item[@"maxImage"] isKindOfClass:[UIImage class]]) {
slider.maximumValueImage = item[@"maxImage"];
}
else if (@available(iOS 13.0, *)) {
slider.maximumValueImage = [UIImage systemImageNamed:item[@"maxImage"]];
}
[GBSettingsViewController fixSliderTint:slider];
}
id block = ^(){
[[NSUserDefaults standardUserDefaults] setDouble:slider.value forKey:item[@"pref"]];
};
objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
[slider addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged];
}
if ([self followingItemForIndexPath:indexPath][@"type"] == typeSeparator) {
cell.separatorInset = UIEdgeInsetsZero;
}
cell.imageView.image = item[@"image"];
return cell;
}
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *item = [self itemForIndexPath:indexPath];
if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu) {
UITableViewStyle style = UITableViewStyleGrouped;
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
if (@available(iOS 13.0, *)) {
style = UITableViewStyleInsetGrouped;
}
}
GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:item[@"submenu"]
title:item[@"title"]
style:style];
if (_detailsNavigation) {
[_detailsNavigation setViewControllers:@[submenu] animated:false];
}
else {
[self.navigationController pushViewController:submenu animated:true];
}
return indexPath;
}
else if (item[@"type"] == typeRadio) {
[[NSUserDefaults standardUserDefaults] setObject:item[@"value"] forKey:item[@"pref"]];
[self.tableView reloadData];
}
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *item = [self itemForIndexPath:indexPath];
if (item[@"type"] == typeSeparator) {
return 8;
}
if (item[@"type"] == typeSlider ||
item[@"type"] == typeLightTemp) {
return 63;
}
return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}
@end

View File

@ -1,5 +0,0 @@
#import <UIKit/UIKit.h>
@interface GBTableViewCell : UITableViewCell
@property double padding;
@end

View File

@ -1,11 +0,0 @@
#import "GBTableViewCell.h"
@implementation GBTableViewCell
-(void )layoutSubviews
{
[super layoutSubviews];
self.imageView.frame = CGRectInset(self.imageView.frame, 0, self.padding);
}
@end

View File

@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
@interface GBTemperatureSlider : UISlider
@end

80
iOS/GBTemperatureSlider.m Normal file
View File

@ -0,0 +1,80 @@
#import "GBTemperatureSlider.h"
static inline void temperature_tint(double temperature, double *r, double *g, double *b)
{
if (temperature >= 0) {
*r = 1;
*g = pow(1 - temperature, 0.375);
if (temperature >= 0.75) {
*b = 0;
}
else {
*b = sqrt(0.75 - temperature) / sqrt(0.75);
}
}
else {
*b = 1;
double squared = pow(temperature, 2);
*g = 0.125 * squared + 0.3 * temperature + 1.0;
*r = 0.21875 * squared + 0.5 * temperature + 1.0;
}
}
@implementation GBTemperatureSlider
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
[self addTarget:self action:@selector(valueChanged) forControlEvents:UIControlEventValueChanged];
return self;
}
- (void)updateTint
{
double r, g, b;
temperature_tint(self.value, &r, &g, &b);
self.thumbTintColor = [UIColor colorWithRed:r green:g blue:b alpha:1.0];
}
- (void)setValue:(float)value
{
[super setValue:value];
[self updateTint];
}
- (void)valueChanged
{
if (fabsf(self.value) < 0.05 && self.value != 0) {
self.value = 0;
}
else {
[self updateTint];
}
}
-(UIImage *)maximumTrackImageForState:(UIControlState)state
{
return [[UIImage alloc] init];
}
-(UIImage *)minimumTrackImageForState:(UIControlState)state
{
return [[UIImage alloc] init];
}
- (void)drawRect:(CGRect)rect
{
CGSize size = self.bounds.size;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2, round(size.height / 2 - 1.5), size.width - 4, 3) cornerRadius:4];
[path appendPath:[UIBezierPath bezierPathWithRoundedRect:CGRectMake(round(size.width / 2 - 1.5), 12, 3, size.height - 24) cornerRadius:4]];
[[UIColor colorWithRed:120 / 255.0
green:120 / 255.0
blue:130 / 255.0
alpha:70 / 255.0] set];
[path fill];
[super drawRect:rect];
}
@end

View File

@ -7,5 +7,6 @@
- (void)start;
- (void)stop;
- (void)changeModel;
- (void)openSettings;
- (void)showAbout;
@end

View File

@ -10,6 +10,7 @@
#import "GBMenuViewController.h"
#import "GBOptionViewController.h"
#import "GBAboutController.h"
#import "GBSettingsViewController.h"
#include <Core/gb.h>
@implementation GBViewController
@ -28,6 +29,7 @@
size_t _audioBufferPosition;
size_t _audioBufferNeeded;
GBAudioClient *_audioClient;
NSMutableSet *_defaultsObservers;
}
static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type)
@ -71,22 +73,60 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)initGameBoy
{
GB_init(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
GB_set_user_data(&_gb, (__bridge void *)(self));
GB_set_boot_rom_load_callback(&_gb, (GB_boot_rom_load_callback_t)loadBootROM);
GB_set_vblank_callback(&_gb, (GB_vblank_callback_t) vblank);
GB_set_log_callback(&_gb, (GB_log_callback_t) consoleLog);
GB_set_color_correction_mode(&_gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]);
GB_set_light_temperature(&_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]);
GB_set_interference_volume(&_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]);
GB_set_border_mode(&_gb, GB_BORDER_NEVER);
[self updatePalette];
GB_set_rgb_encode_callback(&_gb, rgbEncode);
GB_set_highpass_filter_mode(&_gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]);
GB_set_rtc_mode(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]);
GB_apu_set_sample_callback(&_gb, audioCallback);
GB_set_rumble_callback(&_gb, rumbleCallback);
[self updateRumbleMode];
GB_gameboy_t *gb = &_gb;
GB_init(gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
GB_set_user_data(gb, (__bridge void *)(self));
GB_set_boot_rom_load_callback(gb, (GB_boot_rom_load_callback_t)loadBootROM);
GB_set_vblank_callback(gb, (GB_vblank_callback_t) vblank);
GB_set_log_callback(gb, (GB_log_callback_t) consoleLog);
[self addDefaultObserver:^(id newValue) {
GB_set_color_correction_mode(gb, (GB_color_correction_mode_t)[newValue integerValue]);
} forKey:@"GBColorCorrection"];
[self addDefaultObserver:^(id newValue) {
GB_set_light_temperature(gb, [newValue doubleValue]);
} forKey:@"GBLightTemperature"];
[self addDefaultObserver:^(id newValue) {
GB_set_interference_volume(gb, [newValue doubleValue]);
} forKey:@"GBInterferenceVolume"];
GB_set_border_mode(gb, GB_BORDER_NEVER);
__weak typeof(self) weakSelf = self;
[self addDefaultObserver:^(id newValue) {
[weakSelf updatePalette];
} forKey:@"GBCurrentTheme"];
GB_set_rgb_encode_callback(gb, rgbEncode);
[self addDefaultObserver:^(id newValue) {
GB_set_highpass_filter_mode(gb, (GB_highpass_mode_t)[newValue integerValue]);
} forKey:@"GB_HIGHPASS_ACCURATE"];
[self addDefaultObserver:^(id newValue) {
GB_set_rtc_mode(gb, [newValue integerValue]);
} forKey:@"GBRTCMode"];
GB_apu_set_sample_callback(gb, audioCallback);
GB_set_rumble_callback(gb, rumbleCallback);
[self addDefaultObserver:^(id newValue) {
GB_set_rumble_mode(gb, [newValue integerValue]);
} forKey:@"GBRumbleMode"];
[self addDefaultObserver:^(id newValue) {
GB_set_interference_volume(gb, [newValue doubleValue]);
} forKey:@"GBInterferenceVolume"];
}
- (void)addDefaultObserver:(void(^)(id newValue))block forKey:(NSString *)key
{
if (!_defaultsObservers) {
_defaultsObservers = [NSMutableSet set];
}
block = [block copy];
[_defaultsObservers addObject:block];
[[NSUserDefaults standardUserDefaults] addObserver:self
forKeyPath:key
options:NSKeyValueObservingOptionNew
context:(void *)block];
block([[NSUserDefaults standardUserDefaults] objectForKey:key]);
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
((__bridge void(^)(id))context)(change[NSKeyValueChangeNewKey]);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
@ -111,6 +151,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
_gbView.gb = &_gb;
[_gbView screenSizeChanged];
[self addDefaultObserver:^(id newValue) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil];
} forKey:@"GBFilter"];
__weak GBView *weakGBView = _gbView;
[self addDefaultObserver:^(id newValue) {
weakGBView.frameBlendingMode = [newValue integerValue];
} forKey:@"GBFrameBlendingMode"];
[self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
duration:0];
@ -221,6 +271,17 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self presentViewController:controller animated:true completion:nil];
}
- (void)openSettings
{
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
style:UIBarButtonItemStylePlain
target:self
action:@selector(dismissViewController)];
[self presentViewController:[GBSettingsViewController settingsViewControllerWithLeftButton:close]
animated:true
completion:nil];
}
- (void)showAbout
{
[self presentViewController:[[GBAboutController alloc] init] animated:true completion:nil];
@ -383,6 +444,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
[self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
[[GBHapticManager sharedManager] setRumbleStrength:0];
}
- (void)start
@ -463,32 +525,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[[GBHapticManager sharedManager] setRumbleStrength:amp];
}
- (void)updateRumbleMode
{
GB_set_rumble_mode(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRumbleMode"]);
}
- (const GB_palette_t *)userPalette
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
switch ([defaults integerForKey:@"GBColorPalette"]) {
case 1: return &GB_PALETTE_DMG;
case 2: return &GB_PALETTE_MGB;
case 3: return &GB_PALETTE_GBL;
default: return &GB_PALETTE_GREY;
case -1: {
static GB_palette_t customPalette;
NSArray *colors = [defaults dictionaryForKey:@"GBThemes"][[defaults stringForKey:@"GBCurrentTheme"]][@"Colors"];
if (colors.count == 5) {
unsigned i = 0;
for (NSNumber *color in colors) {
uint32_t c = [color unsignedIntValue];
customPalette.colors[i++] = (struct GB_color_s) {c, c >> 8, c >> 16};
}
}
return &customPalette;
}
}
return [GBSettingsViewController paletteForTheme:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]];
}
- (void)updatePalette

BIN
iOS/audioSettings@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
iOS/audioSettings@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
iOS/controlsSettings@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

BIN
iOS/controlsSettings@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -9,7 +9,8 @@ int main(int argc, char * argv[])
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
@"GBFilter": @"NearestNeighbor",
@"GBColorCorrection": @(GB_COLOR_CORRECTION_MODERN_BALANCED),
@"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET),
@"GBHighpassFilter": @(GB_HIGHPASS_ACCURATE),
@"GBRewindLength": @(10),
@"GBFrameBlendingMode": @(GB_FRAME_BLENDING_MODE_ACCURATE),
@"GBDMGModel": @(GB_MODEL_DMG_B),
@ -17,10 +18,9 @@ int main(int argc, char * argv[])
@"GBAGBModel": @(GB_MODEL_AGB_A),
@"GBSGBModel": @(GB_MODEL_SGB2),
@"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY),
@"GBColorPalette": @(1),
@"GBVolume": @(1.0),
@"GBButtonHaptics": @YES,
@"GBCurrentTheme": @"Lime (Game Boy)",
// Default themes
@"GBThemes": @{
@"Desert": @{

BIN
iOS/videoSettings@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

BIN
iOS/videoSettings@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B