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.