git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

iOS/GBSettingsViewController.m

      1 #import "GBSettingsViewController.h"
      2 #import "GBSlider.h"
      3 #import "GBViewBase.h"
      4 #import "GBThemesViewController.h"
      5 #import "GBPalettePicker.h"
      6 #import "GBHapticManager.h"
      7 #import "GCControllerGetElements.h"
      8 #import <objc/runtime.h>
      9 
     10 static NSString const *typeSubmenu = @"submenu";
     11 static NSString const *typeOptionSubmenu = @"optionSubmenu";
     12 static NSString const *typeBlock = @"block";
     13 static NSString const *typeRadio = @"radio";
     14 static NSString const *typeCheck = @"check";
     15 static NSString const *typeDisabled = @"disabled";
     16 static NSString const *typeSeparator = @"separator";
     17 static NSString const *typeSlider = @"slider";
     18 static NSString const *typeLightTemp = @"lightTemp";
     19 static NSString const *typeTurboSlider = @"turboSlider";
     20 
     21 @implementation GBSettingsViewController
     22 {
     23     NSArray<NSDictionary *> *_structure;
     24     UINavigationController *_detailsNavigation;
     25     NSArray<NSArray<GBTheme *> *> *_themes; // For prewarming
     26     bool _iPadRoot;
     27     __weak UISlider *_turboSlider;
     28 }
     29 
     30 + (UIImage *)settingsImageNamed:(NSString *)name
     31 {
     32     UIImage *base = [UIImage imageNamed:name];
     33     UIGraphicsBeginImageContextWithOptions(base.size, false, base.scale);
     34     UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:(CGRect){{0, 0}, base.size} cornerRadius:8];
     35     CGContextSaveGState(UIGraphicsGetCurrentContext());
     36     [path addClip];
     37     [base drawInRect:path.bounds];
     38     if (@available(iOS 19.0, *)) {
     39         CGContextRestoreGState(UIGraphicsGetCurrentContext());
     40         UIImage *overlay = [UIImage imageNamed:@"settingsOverlay"];
     41         [overlay drawInRect:path.bounds];
     42     }
     43     UIImage *ret = UIGraphicsGetImageFromCurrentImageContext();
     44     UIGraphicsEndImageContext();
     45     return ret;
     46 }
     47 
     48 + (NSArray<NSDictionary *> *)rootStructure
     49 {
     50 #define QUICK_SUBMENU(title, ...) @{@"type": typeOptionSubmenu, @"title": title, @"submenu": @[@{@"items": __VA_ARGS__}]}
     51     NSArray<NSDictionary *> *emulationMenu = @[
     52         @{
     53             @"header": @"Rewind Duration",
     54             @"items": @[
     55                 @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"Disabled",   @"value": @0,},
     56                 @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"10 Seconds", @"value": @10,},
     57                 @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"30 Seconds", @"value": @30,},
     58                 @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"1 Minute",   @"value": @60,},
     59                 @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"2 Minutes",  @"value": @120,},
     60                 @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"5 Minutes",  @"value": @300,},
     61                 @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"10 Minutes", @"value": @600,},
     62             ]
     63         },
     64         @{
     65             @"header": @"Real Time Clock Emulation",
     66             @"items": @[
     67                 @{@"type": typeRadio, @"pref": @"GBRTCMode", @"title": @"Accurate",   @"value": @(GB_RTC_MODE_ACCURATE),},
     68                 @{@"type": typeRadio, @"pref": @"GBRTCMode", @"title": @"Sync to System Clock", @"value": @(GB_RTC_MODE_SYNC_TO_HOST),},
     69             ]
     70         },
     71         @{
     72             @"header": @"Turbo Speed Cap",
     73             @"items": @[
     74                 @{@"type": typeCheck, @"pref": @"GBTurboCap", @"title": @"Cap Turbo Speed", @"block": ^void(GBSettingsViewController *controller) {
     75                     if ([[NSUserDefaults standardUserDefaults] floatForKey:@"GBTurboCap"] == 1.0) {
     76                         if (controller->_turboSlider) {
     77                             [[NSUserDefaults standardUserDefaults] setFloat:controller->_turboSlider.value forKey:@"GBTurboCap"];
     78                             controller->_turboSlider.enabled = true;
     79                         }
     80                         else {
     81                             [[NSUserDefaults standardUserDefaults] setFloat:2.0 forKey:@"GBTurboCap"];
     82                         }
     83                     }
     84                     else {
     85                         controller->_turboSlider.enabled = false;
     86                     }
     87                 }},
     88                     @{@"type": typeTurboSlider, @"pref": @"GBTurboCap", @"min": @1.5, @"max": @4, @"minImage": @"tortoise.fill", @"maxImage": @"hare.fill"},
     89             ],
     90             @"footer": ^NSString *(){
     91                 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
     92                     return @"This setting will have no effect because horizontal swipes are configured to dynamically control speed in the “Controls” settings.";
     93                 }
     94                 if ([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]) {
     95                     return [NSString stringWithFormat:@"Turbo speed is capped to %u%%", (unsigned)round([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"] * 100)];
     96                 }
     97                 return @"Turbo speed is not capped";
     98             },
     99         },
    100         @{
    101             @"header": @"Rewind Speed",
    102             @"items": @[
    103                     @{@"type": typeRadio, @"pref": @"GBRewindSpeed", @"title": @"100%", @"value": @1,},
    104                     @{@"type": typeRadio, @"pref": @"GBRewindSpeed", @"title": @"200%", @"value": @2,},
    105                     @{@"type": typeRadio, @"pref": @"GBRewindSpeed", @"title": @"400%", @"value": @4,},
    106             ],
    107             @"footer": ^NSString *(){
    108                 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    109                     return @"This setting will have no effect because horizontal swipes are configured to dynamically control speed in the “Controls” settings";
    110                 }
    111                 return @"";
    112             }
    113         },
    114         @{
    115             @"header": @"Boot ROMs",
    116             @"items": @[
    117                 @{@"type": typeRadio, @"pref": @"GBCustomBootROMs", @"title": @"Use Built-in Boot ROMs", @"value": @NO,},
    118                 @{@"type": typeRadio, @"pref": @"GBCustomBootROMs", @"title": @"Use Boot ROMs from Files", @"value": @YES,},
    119                 @{@"type": typeBlock, @"title": @"Open Boot ROMs Folder", @"block": ^bool(GBSettingsViewController *controller) {
    120                     
    121                     NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0];
    122                     path = [path stringByAppendingPathComponent:@"Boot ROMs"];
    123                     [[NSFileManager defaultManager] createDirectoryAtPath:path
    124                                               withIntermediateDirectories:true
    125                                                                attributes:nil
    126                                                                     error:nil];
    127                     
    128                     
    129                     [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"shareddocuments://%@", [path stringByReplacingOccurrencesOfString:@" "
    130                                                                                                                                                                         withString:@"%20"]]]
    131                                                        options:nil
    132                                              completionHandler:nil];
    133 
    134                     return false;
    135                 }},
    136             ],
    137             @"footer": @"Put your boot ROM files (dmg_boot.bin, cgb_boot.bin, etc.) in the Boot ROMs folder to use them"
    138         },
    139         @{
    140             @"header": @"Emulated Revisions",
    141             @"items": @[
    142                 QUICK_SUBMENU(@"Game Boy", @[
    143                     @{@"type": typeDisabled, @"title": @"DMG-CPU 0"},
    144                     @{@"type": typeDisabled, @"title": @"DMG-CPU A"},
    145                     @{@"type": typeRadio, @"pref": @"GBDMGModel", @"title": @"DMG-CPU B", @"value": @(GB_MODEL_DMG_B),},
    146                     @{@"type": typeDisabled, @"title": @"DMG-CPU C"},
    147                 ]),
    148                 QUICK_SUBMENU(@"Super Game Boy", @[
    149                     @{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy (NTSC)", @"value": @(GB_MODEL_SGB_NTSC),},
    150                     @{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy (PAL)",  @"value": @(GB_MODEL_SGB_PAL),},
    151                     @{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy 2",      @"value": @(GB_MODEL_SGB2),},
    152                 ]),
    153                 QUICK_SUBMENU(@"Game Boy Color", @[
    154                     @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB 0", @"value": @(GB_MODEL_CGB_0),},
    155                     @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB A", @"value": @(GB_MODEL_CGB_A),},
    156                     @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB B", @"value": @(GB_MODEL_CGB_B),},
    157                     @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB C", @"value": @(GB_MODEL_CGB_C),},
    158                     @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB D", @"value": @(GB_MODEL_CGB_D),},
    159                     @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB E", @"value": @(GB_MODEL_CGB_E),},
    160                 ]),
    161                 QUICK_SUBMENU(@"Game Boy Advance", @[
    162                     @{@"type": typeDisabled, @"title": @"CPU AGB 0 (Early GBA)",},
    163                     @{@"type": typeRadio, @"pref": @"GBAGBModel", @"title": @"CPU AGB A (GBA)",             @"value": @(GB_MODEL_AGB_A),},
    164                     @{@"type": typeRadio, @"pref": @"GBAGBModel", @"title": @"CPU AGB A (Game Boy Player)", @"value": @(GB_MODEL_GBP_A),},
    165                     @{@"type": typeDisabled, @"title": @"CPU AGB B (GBA SP)",},
    166                     @{@"type": typeDisabled, @"title": @"CPU AGB E (Late GBA SP)",},
    167                     @{@"type": typeDisabled, @"title": @"CPU AGB E (Late Game Boy Player)",},
    168                 ]),
    169             ],
    170             @"footer": @"Changing the emulated revision on active ROMs will take effect after selecting Reset from the menu or changing the emulated model.",
    171         },
    172     ];
    173     
    174     NSArray<NSDictionary *> *videoMenu = @[
    175         @{
    176             @"items": @[
    177                 @{
    178                     @"title": @"Graphics Filter",
    179                     @"type": typeOptionSubmenu,
    180                     @"submenu": @[
    181                         @{
    182                             @"header": @"Standard Filters",
    183                             @"items": @[
    184                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Nearest Neighbor (Pixelated)", @"value": @"NearestNeighbor",},
    185                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Bilinear (Blurry)", @"value": @"Bilinear",},
    186                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Smooth Bilinear (Less blurry)", @"value": @"SmoothBilinear",},
    187                             ]
    188                         },
    189                         @{
    190                             @"header": @"Screen Filters",
    191                             @"items": @[
    192                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Monochrome LCD Display", @"value": @"MonoLCD",},
    193                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"LCD Display", @"value": @"LCD",},
    194                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"CRT Display", @"value": @"CRT",},
    195                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Flat CRT Display", @"value": @"FlatCRT",},
    196                             ]
    197                         },
    198                         @{
    199                             @"header": @"Upscaling Filters",
    200                             @"items": @[
    201                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Scale2x", @"value": @"Scale2x"},
    202                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Scale4x", @"value": @"Scale4x"},
    203                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased Scale2x", @"value": @"AAScale2x"},
    204                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased Scale4x", @"value": @"AAScale4x"},
    205                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"HQ2x", @"value": @"HQ2x"},
    206                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale", @"value": @"OmniScale"},
    207                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale Legacy", @"value": @"OmniScaleLegacy"},
    208                                 @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased OmniScale Legacy", @"value": @"AAOmniScaleLegacy"},
    209                             ]
    210                         },
    211                     ]
    212                 },
    213             ]
    214         },
    215         @{
    216             @"header": @"Color Correction",
    217             @"items": @[
    218                 @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Disabled",                     @"value": @(GB_COLOR_CORRECTION_DISABLED),},
    219                 @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Correct Color Curves",         @"value": @(GB_COLOR_CORRECTION_CORRECT_CURVES),},
    220                 @{@"type": typeSeparator},
    221                 @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern – Balanced",            @"value": @(GB_COLOR_CORRECTION_MODERN_BALANCED),},
    222                 @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern – Accurate",            @"value": @(GB_COLOR_CORRECTION_MODERN_ACCURATE),},
    223                 @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern – Boost contrast",      @"value": @(GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST),},
    224                 @{@"type": typeSeparator},
    225                 @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Reduce Contrast",              @"value": @(GB_COLOR_CORRECTION_REDUCE_CONTRAST),},
    226                 @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Harsh Reality (Low Contrast)", @"value": @(GB_COLOR_CORRECTION_LOW_CONTRAST),},
    227             ],
    228             @"footer": ^NSString *(){
    229                 return GB_inline_const(NSString *[], {
    230                     [GB_COLOR_CORRECTION_DISABLED] = @"Colors are directly interpreted as sRGB, resulting in unbalanced colors and inaccurate hues.",
    231                     [GB_COLOR_CORRECTION_CORRECT_CURVES] = @"Colors have their brightness corrected, but hues remain unbalanced.",
    232                     [GB_COLOR_CORRECTION_MODERN_BALANCED] = @"Emulates a modern display. Blue contrast is moderately enhanced at the cost of slight hue inaccuracy.",
    233                     [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.",
    234                     [GB_COLOR_CORRECTION_REDUCE_CONTRAST] = @"Slightly reduce the contrast to better represent the tint and contrast of the original display.",
    235                     [GB_COLOR_CORRECTION_LOW_CONTRAST] = @"Harshly reduce the contrast to accurately represent the tint and low contrast of the original display.",
    236                     [GB_COLOR_CORRECTION_MODERN_ACCURATE] = @"Emulates a modern display. Colors have their hues and brightness corrected.",
    237                 })[MIN(GB_COLOR_CORRECTION_MODERN_ACCURATE, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"])];
    238             },
    239         },
    240         @{
    241             @"header": @"Ambient Light Temperature",
    242             @"items": @[
    243                 @{@"type": typeLightTemp, @"pref": @"GBLightTemperature", @"min": @-1, @"max": @1}
    244             ],
    245         },
    246         @{
    247             @"header": @"Frame Blending",
    248             @"items": @[
    249                 @{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Disabled", @"value": @(GB_FRAME_BLENDING_MODE_DISABLED),},
    250                 @{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Simple",   @"value": @(GB_FRAME_BLENDING_MODE_SIMPLE),},
    251                 @{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Accurate", @"value": @(GB_FRAME_BLENDING_MODE_ACCURATE),},
    252             ]
    253         },
    254         @{
    255             @"items": @[@{
    256                 @"title": @"Monochrome Palette",
    257                 @"type": typeBlock,
    258                 @"block": ^bool(GBSettingsViewController *controller) {
    259                     UITableViewStyle style = UITableViewStyleGrouped;
    260                     if (@available(iOS 13.0, *)) {
    261                         style = UITableViewStyleInsetGrouped;
    262                     }
    263                     [controller.navigationController pushViewController:[[GBPalettePicker alloc] initWithStyle:style] animated:true];
    264                     return true;
    265                 },
    266                 @"pref": @"GBCurrentTheme",
    267             }],
    268             @"footer": @"This palette will be used when emulating a monochrome model such as the original Game Boy."
    269         }
    270     ];
    271     
    272     NSArray<NSDictionary *> *audioMenu = @[
    273         @{
    274             @"header": @"Enable Audio",
    275             @"items": @[
    276                 @{@"type": typeRadio, @"pref": @"GBAudioMode", @"title": @"Never",                     @"value": @"off",},
    277                 @{@"type": typeRadio, @"pref": @"GBAudioMode", @"title": @"Controlled by Silent Mode", @"value": @"switch",},
    278                 @{@"type": typeRadio, @"pref": @"GBAudioMode", @"title": @"Always",                    @"value": @"on",},
    279             ],
    280 
    281         },
    282         @{
    283             @"header": @"High-pass Filter",
    284             @"items": @[
    285                 @{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Disabled (Keep DC Offset)",   @"value": @(GB_HIGHPASS_OFF),},
    286                 @{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Accurate (Emulate Hardware)", @"value": @(GB_HIGHPASS_ACCURATE),},
    287                 @{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Preserve Waveform",           @"value": @(GB_HIGHPASS_REMOVE_DC_OFFSET),},
    288             ],
    289             @"footer": ^NSString *(){
    290                 return GB_inline_const(NSString *[], {
    291                     [GB_HIGHPASS_OFF] = @"No high-pass filter will be applied. DC offset will be kept, pausing and resuming will trigger audio pops.",
    292                     [GB_HIGHPASS_ACCURATE] = @"An accurate high-pass filter will be applied, removing the DC offset while somewhat attenuating the bass.",
    293                     [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.",
    294                 })[MIN(GB_HIGHPASS_REMOVE_DC_OFFSET, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"])];
    295             },
    296         },
    297         @{
    298             @"header": @"Interference volume",
    299             @"items": @[
    300                 @{@"type": typeSlider, @"pref": @"GBInterferenceVolume", @"min": @0, @"max": @1, @"minImage": @"speaker.fill", @"maxImage": @"speaker.3.fill"}
    301             ],
    302         },
    303     ];
    304     
    305     NSArray<NSDictionary *> *controlsMenu = @[
    306         @{
    307             @"items": @[
    308                 @{@"type": typeBlock, @"title": @"Configure Game Controllers", @"block": ^bool(GBSettingsViewController *controller){
    309                     return [controller configureGameControllers];
    310                 }},
    311             ],
    312         },
    313         @{
    314             @"header": @"D-pad Style",
    315             @"items": @[
    316                 @{@"type": typeRadio, @"pref": @"GBSwipeDpad", @"title": @"Standard", @"value": @NO,},
    317                 @{@"type": typeRadio, @"pref": @"GBSwipeDpad", @"title": @"Swipe",    @"value": @YES,},
    318             ],
    319             @"footer": ^NSString *(){
    320                 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeDpad"]) {
    321                     return @"Directional input is determined by the swipe direction.";
    322                 }
    323                 return @"Directional input is determined by the touch position.";
    324             },
    325         },
    326         @{
    327             @"items": @[
    328                 @{@"type": typeCheck, @"pref": @"GBEnableABCombo", @"title": @"Single-Touch A+B"},
    329             ],
    330             @"footer": @"Enable this option to allow pressing A+B by touching the space between the two buttons",
    331         },
    332         @{
    333             @"header": @"Horizontal Swipe Behavior",
    334             @"items": @[
    335                 @{@"type": typeCheck, @"pref": @"GBDynamicSpeed", @"title": @"Dynamically Control Speed"},
    336                 @{@"type": typeCheck, @"pref": @"GBSwipeLock", @"title": @"Lock After Swiping"},
    337             ],
    338             @"footer": ^NSString *(){
    339                 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) {
    340                     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    341                         return @"Swipe right on the Game Boy screen to play forward, and swipe left to rewind. Tap on the Game Boy screen to return to normal. The forward and rewind speeds are determinied by the swipe distance.";
    342                     }
    343                     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. The turbo and rewind speeds can be configured under “Emulation” settings.";
    344                 }
    345                 
    346                 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    347                     return @"Swipe right on the Game Boy screen to play forward, and swipe left to rewind. Raise the touch to return to normal. The forward and rewind speeds are determinied by the swipe distance.";
    348                 }
    349                 return @"Swipe right on the Game Boy screen to fast-forward, and swipe left to rewind. Raise the touch to return to normal. The turbo and rewind speeds can be configured under “Emulation” settings.";
    350             },
    351         },
    352         @{
    353             @"header": @"Quick Save and Load",
    354             @"items": @[
    355                 @{@"type": typeCheck, @"pref": @"GBSwipeState", @"title": @"Swipe to Save and Load from Slot 1"},
    356             ],
    357             @"footer": ^NSString *(){
    358                 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) {
    359                     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.";
    360                 }
    361                 return @" "; // This space is needed, otherwise UITableView spacing breaks
    362             },
    363         },
    364         @{
    365             @"header": @"Enable Rumble",
    366             @"items": @[
    367                 @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Never",                        @"value": @(GB_RUMBLE_DISABLED),},
    368                 @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"For Rumble-Enabled Game Paks", @"value": @(GB_RUMBLE_CARTRIDGE_ONLY),},
    369                 @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Always",                       @"value": @(GB_RUMBLE_ALL_GAMES),},
    370             ],
    371         },
    372         @{
    373             @"header": @"Rumble Strength",
    374             @"items": @[
    375                 @{@"type": typeSlider, @"pref": @"GBRumbleStrength", @"min": @0.125, @"max": @1, @"minImage": @"waveform.weak", @"maxImage": @"waveform"}
    376             ],
    377         },
    378         @{
    379             @"items": @[
    380                 @{@"type": typeCheck, @"pref": @"GBButtonHaptics", @"title": @"Enable Button Haptics"},
    381                 @{@"type": typeSlider, @"pref": @"GBHapticsStrength", @"min": @0.25, @"max": @1, @"minImage": @"waveform.weak", @"maxImage": @"waveform",
    382                   @"previewBlock": ^void(void){
    383                       [[GBHapticManager sharedManager] doTapHaptic];
    384                   }
    385                 }
    386             ],
    387         },
    388         @{
    389             @"header": @"While Using a Controller…",
    390             @"items": @[
    391                 @{@"type": typeRadio, @"pref": @"GBControllersHideInterface", @"title": @"Keep the UI Visible", @"value": @(GBControllerFocusOff)},
    392                 @{@"type": typeRadio, @"pref": @"GBControllersHideInterface", @"title": @"Hide the UI", @"value": @(GBControllerFocusDoNotCenter)},
    393                 @{@"type": typeRadio, @"pref": @"GBControllersHideInterface", @"title": @"Hide the UI and Center the Screen", @"value": @(GBControllerFocusOn)},
    394             ],
    395         },
    396         @{
    397             @"header": @"Controller Joystick Behavior",
    398             @"items": @[
    399                 @{@"type": typeRadio, @"pref": @"GBFauxAnalogInputs", @"title": @"Digital",     @"value": @NO},
    400                 @{@"type": typeRadio, @"pref": @"GBFauxAnalogInputs", @"title": @"Faux Analog", @"value": @YES},
    401             ],
    402         },
    403 
    404     ];
    405     
    406     
    407     NSArray *rootItems = @[
    408                     @{
    409                         @"title": @"Emulation",
    410                         @"type": typeSubmenu,
    411                         @"submenu": emulationMenu,
    412                         @"image": [self settingsImageNamed:@"emulationSettings"],
    413                     },
    414                     @{
    415                         @"title": @"Video",
    416                         @"type": typeSubmenu,
    417                         @"submenu": videoMenu,
    418                         @"image": [self settingsImageNamed:@"videoSettings"],
    419                     },
    420                     @{
    421                         @"title": @"Audio",
    422                         @"type": typeSubmenu,
    423                         @"submenu": audioMenu,
    424                         @"image": [self settingsImageNamed:@"audioSettings"],
    425                     },
    426                     @{
    427                         @"title": @"Controls",
    428                         @"type": typeSubmenu,
    429                         @"submenu": controlsMenu,
    430                         @"image": [self settingsImageNamed:@"controlsSettings"],
    431                     },
    432                     @{
    433                         @"title": @"Themes",
    434                         @"type": typeSubmenu,
    435                         @"class": [GBThemesViewController class],
    436                         @"image": [self settingsImageNamed:@"themeSettings"],
    437                     },
    438     ];
    439     
    440 
    441      return @[
    442         @{
    443             @"items": rootItems,
    444         }
    445     ];
    446 }
    447 
    448 
    449 + (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button
    450 {
    451     UITableViewStyle style = UITableViewStyleGrouped;
    452     if (@available(iOS 13.0, *)) {
    453         style = UITableViewStyleInsetGrouped;
    454     }
    455     GBSettingsViewController *root = [[self alloc] initWithStructure:[self rootStructure] title:@"Settings" style:style];
    456     [root preloadThemePreviews];
    457     UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:root];
    458     [controller.visibleViewController.navigationItem setLeftBarButtonItem:button];
    459     if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) {
    460         return controller;
    461     }
    462     
    463     UISplitViewController *split = nil;
    464     if (@available(iOS 14.5, *)) {
    465         split = [[UISplitViewController alloc] initWithStyle:UISplitViewControllerStyleDoubleColumn];
    466         split.displayModeButtonVisibility = UISplitViewControllerDisplayModeButtonVisibilityNever;
    467     }
    468     else {
    469         split = [[UISplitViewController alloc] init];
    470     }
    471     UIViewController *blank = [[UIViewController alloc] init];
    472     blank.view.backgroundColor = root.view.backgroundColor;
    473     root->_detailsNavigation = [[UINavigationController alloc] initWithRootViewController:blank];
    474     split.viewControllers = @[controller, root->_detailsNavigation];
    475     split.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
    476     root->_iPadRoot = true;
    477     return split;
    478 }
    479 
    480 static UIImage *ImageForController(GCController *controller)
    481 {
    482     if (@available(iOS 13.0, *)) {
    483         
    484         NSString *symbolName = @"gamecontroller.fill";
    485         UIColor *color = [UIColor grayColor];
    486         
    487         if (@available(iOS 14.5, *)) {
    488             if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
    489                 symbolName = @"logo.playstation";
    490                 color = [UIColor colorWithRed:0 green:0x30 / 255.0 blue:0x87 / 255.0 alpha:1.0];
    491             }
    492         }
    493         if (@available(iOS 14.0, *)) {
    494             if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) {
    495                 symbolName = @"logo.playstation";
    496                 color = [UIColor colorWithRed:0 green:0x30 / 255.0 blue:0x87 / 255.0 alpha:1.0];
    497             }
    498             if ([controller.extendedGamepad isKindOfClass:[GCXboxGamepad class]]) {
    499                 symbolName = @"logo.xbox";
    500                 color = [UIColor colorWithRed:0xe / 255.0 green:0x7a / 255.0 blue:0xd / 255.0 alpha:1.0];
    501             }
    502         }
    503         
    504         UIImage *glyph = [[UIImage systemImageNamed:symbolName] imageWithTintColor:[UIColor whiteColor]];
    505         if (!glyph) {
    506             glyph = [[UIImage systemImageNamed:@"gamecontroller.fill"] imageWithTintColor:[UIColor whiteColor]];
    507         }
    508         
    509         UIGraphicsBeginImageContextWithOptions((CGSize){29, 29}, false, [UIScreen mainScreen].scale);
    510         [color setFill];
    511         [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 29, 29) cornerRadius:7] fill];
    512         double height = 25 / glyph.size.width * glyph.size.height;
    513         [glyph drawInRect:CGRectMake(2, (29 - height) / 2, 25, height)];
    514         
    515         UIImage *ret = UIGraphicsGetImageFromCurrentImageContext();
    516         UIGraphicsEndImageContext();
    517         return ret;
    518     }
    519     return nil;
    520     
    521 }
    522 
    523 + (GBButton)controller:(GCController *)controller convertUsageToButton:(GBControllerUsage)usage
    524 {
    525     bool isSony = false;
    526     if (@available(iOS 14.5, *)) {
    527         if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
    528             isSony = true;
    529         }
    530     }
    531     if (@available(iOS 14.0, *)) {
    532         if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) {
    533             isSony = true;
    534         }
    535     }
    536     
    537     NSNumber *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBControllerMappings"][controller.vendorName][[NSString stringWithFormat:@"%u", usage]];
    538     if (mapping) {
    539         return mapping.intValue;
    540     }
    541     
    542     switch (usage) {
    543         case GBUsageButtonA: return isSony? GBB : GBA;
    544         case GBUsageButtonB: return isSony? GBA : GBB;
    545         case GBUsageButtonX: return isSony? GBSelect : GBStart;
    546         case GBUsageButtonY: return isSony? GBStart : GBSelect;
    547         case GBUsageButtonMenu: return GBStart;
    548         case GBUsageButtonOptions: return GBSelect;
    549         case GBUsageButtonHome: return GBStart;
    550         case GBUsageLeftShoulder: return GBRewind;
    551         case GBUsageRightShoulder: return GBTurbo;
    552         case GBUsageLeftTrigger: return GBUnderclock;
    553         case GBUsageRightTrigger: return GBTurbo;
    554         default: return GBUnusedButton;
    555     }
    556 }
    557 
    558 static NSString *LocalizedNameForElement(GCControllerElement *element, GBControllerUsage usage)
    559 {
    560     if (@available(iOS 14.0, *)) {
    561         NSString *ret = element.localizedName;
    562         if (ret) {
    563             return element.localizedName;
    564         }
    565     }
    566     switch (usage) {
    567         case GBUsageDpad: return @"D-Pad";
    568         case GBUsageButtonA: return @"A";
    569         case GBUsageButtonB: return @"B";
    570         case GBUsageButtonX: return @"X";
    571         case GBUsageButtonY: return @"Y";
    572         case GBUsageButtonMenu: return @"Menu";
    573         case GBUsageButtonOptions: return @"Options";
    574         case GBUsageButtonHome: return @"Home";
    575         case GBUsageLeftThumbstick: return @"Left Thumbstick";
    576         case GBUsageRightThumbstick: return @"Right Thumbstick";
    577         case GBUsageLeftShoulder: return @"Left Shoulder";
    578         case GBUsageRightShoulder: return @"Right Shoulder";
    579         case GBUsageLeftTrigger: return @"Left Trigger";
    580         case GBUsageRightTrigger: return @"Right Trigger";
    581         case GBUsageLeftThumbstickButton: return @"Left Thumbstick Button";
    582         case GBUsageRightThumbstickButton: return @"Right Thumbstick Button";
    583         case GBUsageTouchpadButton: return @"Touchpad Button";
    584         default: {
    585             if (@available(iOS 14.0, *)) {
    586                 NSArray *aliases = [element.aliases.allObjects sortedArrayUsingSelector:@selector(compare:)];
    587                 if (aliases.count) {
    588                     return aliases[0];
    589                 }
    590             }
    591         }
    592     }
    593     
    594     return @"Button";
    595 }
    596 
    597 - (void)configureGameController:(GCController *)controller
    598 {
    599     NSMutableArray *items = [NSMutableArray array];
    600     NSDictionary <NSNumber *, GCControllerElement *> *elementsDict = GCControllerGetElements(controller);
    601     for (NSNumber *usage in [[elementsDict allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
    602         GCControllerElement *element = elementsDict[usage];
    603         if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
    604  
    605         id (^getter)(void) = ^id(void) {
    606             return @([GBSettingsViewController controller:controller convertUsageToButton:usage.intValue]);
    607         };
    608         
    609         void (^setter)(id) = ^void(id value) {
    610             NSMutableDictionary *mapping = ([[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBControllerMappings"] ?: @{}).mutableCopy;
    611             
    612             NSMutableDictionary *vendorMapping = ((NSDictionary *)mapping[controller.vendorName] ?: @{}).mutableCopy;
    613             vendorMapping[usage.stringValue] = value;
    614             mapping[controller.vendorName] = vendorMapping;
    615             [[NSUserDefaults standardUserDefaults] setObject:mapping forKey:@"GBControllerMappings"];
    616         };
    617 
    618         bool isDpadElement = [element.collection isKindOfClass:[GCControllerDirectionPad class]];
    619         
    620         NSDictionary *item = @{
    621             @"title": LocalizedNameForElement(element, usage.unsignedIntValue),
    622             @"type": typeOptionSubmenu,
    623             @"submenu": @[@{@"items": @[
    624                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": isDpadElement? @"D-pad" : @"None", @"value": @(GBUnusedButton)},
    625                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Right",                          @"value": @(GBRight)},
    626                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Left",                           @"value": @(GBLeft)},
    627                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Up",                             @"value": @(GBUp)},
    628                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Down",                           @"value": @(GBDown)},
    629                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"A",                              @"value": @(GBA)},
    630                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"B",                              @"value": @(GBB)},
    631                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Select",                         @"value": @(GBSelect)},
    632                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Start",                          @"value": @(GBStart)},
    633                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rapid A",                        @"value": @(GBRapidA)},
    634                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rapid B",                        @"value": @(GBRapidB)},
    635                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Turbo",                          @"value": @(GBTurbo)},
    636                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rewind",                         @"value": @(GBRewind)},
    637                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Slow-motion",                    @"value": @(GBUnderclock)},
    638                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Save State 1",                   @"value": @(GBSaveState1)},
    639                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Load State 1",                   @"value": @(GBLoadState1)},
    640                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Open Menu",                      @"value": @(GBOpenMenu)},
    641                 @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Reset",                          @"value": @(GBReset)},
    642             ]}],
    643         };
    644         if (@available(iOS 14.0, *)) {
    645             UIImage *image = [[UIImage systemImageNamed:element.sfSymbolsName] imageWithTintColor:UIColor.labelColor renderingMode:UIImageRenderingModeAlwaysOriginal];
    646             if (image) {
    647                 item = [item mutableCopy];
    648                 ((NSMutableDictionary *)item)[@"image"] = image;
    649             }
    650         }
    651         [items addObject:item];
    652     }
    653     
    654     UITableViewStyle style = UITableViewStyleGrouped;
    655     if (@available(iOS 13.0, *)) {
    656         style = UITableViewStyleInsetGrouped;
    657     }
    658     
    659     GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:@[@{@"items": items}]
    660                                                                                       title:controller.vendorName
    661                                                                                       style:style];
    662     [self.navigationController pushViewController:submenu animated:true];
    663 }
    664 
    665 - (bool)configureGameControllers
    666 {
    667     
    668     NSMutableArray *items = [NSMutableArray array];
    669     for (GCController *controller in [GCController controllers]) {
    670         if (!GCControllerGetElements(controller)) continue;
    671         NSDictionary *item = @{
    672             @"title": controller.vendorName,
    673             @"type": typeBlock,
    674             @"block": ^bool(void) {
    675                 [self configureGameController:controller];
    676                 return true;
    677             }
    678         };
    679         UIImage *image = ImageForController(controller);
    680         if (image) {
    681             item = [item mutableCopy];
    682             ((NSMutableDictionary *)item)[@"image"] = image;
    683         }
    684             
    685         [items addObject:item];
    686     }
    687     if (items.count) {
    688         UITableViewStyle style = UITableViewStyleGrouped;
    689         if (@available(iOS 13.0, *)) {
    690             style = UITableViewStyleInsetGrouped;
    691         }
    692         
    693         GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:@[@{@"items": items}]
    694                                                                                            title:@"Configure Game Controllers"
    695                                                                                            style:style];
    696         [self.navigationController pushViewController:submenu animated:true];
    697     }
    698     else {
    699         UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No Controllers Connected"
    700                                                                        message:@"There are no connected game controllers to configure"
    701                                                                 preferredStyle:UIAlertControllerStyleAlert];
    702         [alert addAction:[UIAlertAction actionWithTitle:@"Close"
    703                                                   style:UIAlertActionStyleCancel
    704                                                 handler:nil]];
    705         [self presentViewController:alert animated:true completion:nil];
    706         return false;
    707         
    708     }
    709     return true;
    710 }
    711 
    712 - (instancetype)initWithStructure:(NSArray *)structure title:(NSString *)title style:(UITableViewStyle)style
    713 {
    714     self = [super initWithStyle:style];
    715     if (!self) return nil;
    716     self.title = title;
    717     _structure = structure;
    718     return self;
    719 }
    720 
    721 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    722 {
    723     return _structure.count;
    724 }
    725 
    726 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    727 {
    728     return [_structure[section][@"items"] count];
    729 }
    730 
    731 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
    732 {
    733     return _structure[section][@"header"];
    734 }
    735 
    736 - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
    737 {
    738     if ([_structure[section][@"footer"] respondsToSelector:@selector(invoke)]) {
    739         return ((NSString *(^)(void))_structure[section][@"footer"])();
    740     }
    741     return _structure[section][@"footer"];
    742 }
    743 
    744 - (NSDictionary *)itemForIndexPath:(NSIndexPath *)indexPath
    745 {
    746     return _structure[[indexPath indexAtPosition:0]][@"items"][[indexPath indexAtPosition:1]];
    747 }
    748 
    749 - (NSDictionary *)followingItemForIndexPath:(NSIndexPath *)indexPath
    750 {
    751     NSArray *items = _structure[[indexPath indexAtPosition:0]][@"items"];
    752     if ([indexPath indexAtPosition:1] + 1 >= items.count) {
    753         return nil;
    754     }
    755     return items[[indexPath indexAtPosition:1] + 1];
    756 }
    757 
    758 + (void)fixSliderTint:(UIView *)view
    759 {
    760     if ([view isKindOfClass:[UIImageView class]]) {
    761         view.tintColor = [UIColor systemGrayColor];
    762     }
    763     for (UIView *subview in view.subviews) {
    764         [self fixSliderTint:subview];
    765     }
    766 }
    767 
    768 static id ValueForItem(NSDictionary *item)
    769 {
    770     if (item[@"getter"]) {
    771         return ((id(^)(void))item[@"getter"])();
    772     }
    773     return [[NSUserDefaults standardUserDefaults] objectForKey:item[@"pref"]] ?: @0;
    774 }
    775 
    776 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    777 {
    778     NSDictionary *item = [self itemForIndexPath:indexPath];
    779     
    780     UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
    781     cell.textLabel.text = item[@"title"];
    782     NSString *type = item[@"type"];
    783     if (type == typeSubmenu || type == typeOptionSubmenu || type == typeBlock) {
    784         cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    785         cell.selectionStyle = UITableViewCellSelectionStyleBlue;
    786         if (type == typeOptionSubmenu) {
    787             for (NSDictionary *section in item[@"submenu"]) {
    788                 for (NSDictionary *item in section[@"items"]) {
    789                     if (item[@"value"] && [ValueForItem(item) isEqual:item[@"value"]]) {
    790                         cell.detailTextLabel.text = item[@"title"];
    791                         break;
    792                     }
    793                 }
    794             }
    795         }
    796         else if (item[@"pref"]) {
    797             cell.detailTextLabel.text = [[NSUserDefaults standardUserDefaults] stringForKey:item[@"pref"]];
    798         }
    799     }
    800     else if (type == typeRadio) {
    801         id settingValue = ValueForItem(item);
    802         id itemValue = item[@"value"];
    803         if (settingValue == itemValue || [settingValue isEqual:itemValue]) {
    804             cell.accessoryType = UITableViewCellAccessoryCheckmark;
    805         }
    806     }
    807     else if (type == typeCheck) {
    808         UISwitch *button = [[UISwitch alloc] init];
    809         cell.accessoryView = button;
    810         if ([[NSUserDefaults standardUserDefaults] boolForKey:item[@"pref"]]) {
    811             button.on = true;
    812         }
    813         
    814         __weak typeof(self) weakSelf = self;
    815         id block = ^(){
    816             [[NSUserDefaults standardUserDefaults] setBool:button.on forKey:item[@"pref"]];
    817             if (item[@"block"]) {
    818                 ((void(^)(GBSettingsViewController *))item[@"block"])(self);
    819             }
    820             unsigned section = [indexPath indexAtPosition:0];
    821             UITableViewHeaderFooterView *view = [weakSelf.tableView footerViewForSection:section];
    822             view.textLabel.text = [weakSelf tableView:weakSelf.tableView titleForFooterInSection:section];
    823             [UIView setAnimationsEnabled:false];
    824             [weakSelf.tableView beginUpdates];
    825             [view sizeToFit];
    826             [weakSelf.tableView endUpdates];
    827             [UIView setAnimationsEnabled:true];
    828         };
    829         objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
    830         
    831         [button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged];
    832         cell.selectionStyle = UITableViewCellSelectionStyleNone;
    833     }
    834     else if (type == typeDisabled) {
    835         cell.selectionStyle = UITableViewCellSelectionStyleNone;
    836         if (@available(iOS 13.0, *)) {
    837             cell.textLabel.textColor = [UIColor separatorColor];
    838         }
    839         else {
    840             cell.textLabel.textColor = [UIColor colorWithWhite:0 alpha:0.75];
    841         }
    842     }
    843     else if (type == typeSeparator) {
    844         cell.backgroundColor = [UIColor clearColor];
    845         cell.separatorInset = UIEdgeInsetsZero;
    846     }
    847     else if (type == typeSlider ||
    848              type == typeLightTemp ||
    849              type == typeTurboSlider) {
    850         CGRect rect = cell.contentView.bounds;
    851         rect.size.width -= 24;
    852         rect.size.height -= 24;
    853         rect.origin.x += 12;
    854         rect.origin.y += 12;
    855         UISlider *slider = [type == typeLightTemp? [GBSlider alloc] : [UISlider alloc] initWithFrame:rect];
    856         slider.continuous = true;
    857         slider.minimumValue = [item[@"min"] floatValue];
    858         slider.maximumValue = [item[@"max"] floatValue];
    859         slider.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    860         [cell.contentView addSubview:slider];
    861         if (type == typeTurboSlider) {
    862             slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]] ?: 2.0;
    863             _turboSlider = slider;
    864             if (![[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]]) {
    865                 slider.enabled = false;
    866             }
    867         }
    868         else {
    869             slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]];
    870         }
    871         cell.selectionStyle = UITableViewCellSelectionStyleNone;
    872         
    873         if (@available(iOS 13.0, *)) {
    874             if (item[@"minImage"] && item[@"maxImage"]) {
    875                 slider.minimumValueImage = [UIImage systemImageNamed:item[@"minImage"]] ?: [[UIImage imageNamed:item[@"minImage"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    876                 slider.maximumValueImage = [UIImage systemImageNamed:item[@"maxImage"]] ?: [[UIImage imageNamed:item[@"maxImage"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    877                 [GBSettingsViewController fixSliderTint:slider];
    878             }
    879         }
    880         
    881         __weak typeof(self) weakSelf = self;
    882         id block = ^(){
    883             [[NSUserDefaults standardUserDefaults] setDouble:slider.value forKey:item[@"pref"]];
    884             if (type == typeTurboSlider) {
    885                 unsigned section = [indexPath indexAtPosition:0];
    886                 UITableViewHeaderFooterView *view = [weakSelf.tableView footerViewForSection:section];
    887                 view.textLabel.text = [weakSelf tableView:weakSelf.tableView titleForFooterInSection:section];
    888                 [UIView setAnimationsEnabled:false];
    889                 [weakSelf.tableView beginUpdates];
    890                 [view sizeToFit];
    891                 [weakSelf.tableView endUpdates];
    892                 [UIView setAnimationsEnabled:true];
    893             }
    894         };
    895         objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
    896 
    897         [slider addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged];
    898         if (item[@"previewBlock"]) {
    899             [slider addTarget:item[@"previewBlock"] action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchDown];
    900         }
    901     }
    902     
    903     if ([self followingItemForIndexPath:indexPath][@"type"] == typeSeparator) {
    904         cell.separatorInset = UIEdgeInsetsZero;
    905     }
    906     cell.imageView.image = item[@"image"];
    907     if (@available(iOS 19.0, *)) {
    908         if (_iPadRoot) {
    909             cell.textLabel.textColor = [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) {
    910                 return cell.isSelected? [UIColor whiteColor] : [UIColor labelColor];
    911             }];
    912         }
    913     }
    914     return cell;
    915 }
    916 
    917 - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
    918 {
    919     NSDictionary *item = [self itemForIndexPath:indexPath];
    920     NSString *type = item[@"type"];
    921     if (type == typeSubmenu || type == typeOptionSubmenu) {
    922         UITableViewStyle style = UITableViewStyleGrouped;
    923         if (@available(iOS 13.0, *)) {
    924             style = UITableViewStyleInsetGrouped;
    925         }
    926         UITableViewController *submenu = nil;
    927         
    928         if (item[@"class"]) {
    929             submenu = [(UITableViewController *)[item[@"class"] alloc] initWithStyle:style];
    930             submenu.title = item[@"title"];
    931         }
    932         else {
    933             submenu = [[GBSettingsViewController alloc] initWithStructure:item[@"submenu"]
    934                                                                     title:item[@"title"]
    935                                                                     style:style];
    936         }
    937         if (_detailsNavigation) {
    938             [_detailsNavigation setViewControllers:@[submenu] animated:false];
    939         }
    940         else {
    941             [self.navigationController pushViewController:submenu animated:true];
    942         }
    943         return indexPath;
    944     }
    945     else if (type == typeRadio) {
    946         if (item[@"setter"]) {
    947             ((void(^)(id))item[@"setter"])(item[@"value"]);
    948         }
    949         else {
    950             [[NSUserDefaults standardUserDefaults] setObject:item[@"value"] forKey:item[@"pref"]];
    951         }
    952         [self.tableView reloadData];
    953     }
    954     else if (type == typeBlock) {
    955         if (((bool(^)(GBSettingsViewController *))item[@"block"])(self)) {
    956             return indexPath;
    957         }
    958     }
    959     return nil;
    960 }
    961 
    962 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    963 {
    964     NSDictionary *item = [self itemForIndexPath:indexPath];
    965     NSString *type = item[@"type"];
    966     if (type == typeSeparator) {
    967         return 8;
    968     }
    969     if (type == typeSlider ||
    970         type == typeLightTemp ||
    971         type == typeTurboSlider) {
    972         return 63;
    973     }
    974     return [super tableView:tableView heightForRowAtIndexPath:indexPath];
    975 
    976 }
    977 
    978 - (void)viewWillAppear:(BOOL)animated
    979 {
    980     [super viewWillAppear:animated];
    981     [self.tableView reloadData];
    982 }
    983 
    984 - (void)preloadThemePreviews
    985 {
    986     /* These take some time to render, preload them when loading the root controller */
    987     _themes = [GBThemesViewController themes];
    988     double time = 0;
    989     for (NSArray *section in _themes) {
    990         for (GBTheme *theme in section) {
    991             /* Sadly they can't be safely rendered outside the main thread, but we can
    992                queue each of them individually to not block the main quote for too long. */
    993             time += 0.1;
    994             dispatch_after(dispatch_time(DISPATCH_TIME_NOW, time * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    995                 [theme verticalPreview];
    996                 [theme horizontalPreview];
    997             });
    998         }
    999     }
   1000 }
   1001 
   1002 + (GBTheme *)themeNamed:(NSString *)name
   1003 {
   1004     NSArray *themes = [GBThemesViewController themes];
   1005     for (NSArray *section in themes) {
   1006         for (GBTheme *theme in section) {
   1007             if ([theme.name isEqualToString:name]) {
   1008                 return theme;
   1009             }
   1010         }
   1011     }
   1012     
   1013     return [themes.firstObject firstObject];
   1014 }
   1015 
   1016 @end

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.