SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
iOS/GBMenuViewController.m
1 #import <objc/runtime.h>
2 #import "GBMenuViewController.h"
3 #import "GBMenuButton.h"
4 #import "GBViewController.h"
5 #import "GBROMManager.h"
6
7 static NSString *const tips[] = {
8 @"Tip: AirDrop ROM files from a Mac or another device to play them.",
9 @"Tip: Swipe right on the Game Boy screen to fast forward emulation.",
10 @"Tip: The D-pad can be replaced with a Swipe-pad in Control Settings. Give it a try!",
11 @"Tip: Swipe left on the Game Boy screen to rewind.",
12 @"Tip: Enable Quick Save and Load in Control Settings to use save state gestures.",
13 @"Tip: The turbo and rewind speeds can be changed in Emulation Settings.",
14 @"Tip: Change turbo and rewind behavior to locking in Controls Settings.",
15 @"Tip: Single Touch A+B combo can be enabled in Controls Settings.",
16 @"Tip: Try different scaling filters in Display Settings.",
17 @"Tip: Dynamically control turbo and rewind speed by enabling Dynamic Control in Control Settings.",
18 @"Tip: Rumble can be enabled even for games without rumble support in Control Settings.",
19 @"Tip: Try different color palettes for monochrome models in Display Settings.",
20 @"Tip: Use an external game controller and Screen Mirroring for a big screen experience!",
21 @"Did you know? The Game Boy uses a Sharp SM83 CPU.",
22 @"Did you know? The Game Boy Color has 6 different hardware revisions.",
23 @"Did you know? The Game Boy's frame rate is approximately 59.73 frames per second.",
24 @"Did you know? The original Super Game Boy runs slightly faster than other Game Boys.",
25 @"Did you know? The Game Boy generates audio at a sample rate of over 2MHz!",
26 };
27
28 @implementation GBMenuViewController
29 {
30 UILabel *_tipLabel;
31 UIVisualEffectView *_effectView;
32 NSMutableArray<UIButton *> *_buttons;
33 }
34
35 + (instancetype)menu
36 {
37 UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
38 UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
39 GBMenuViewController *ret = [self alertControllerWithTitle:nil
40 message:nil
41 preferredStyle:style];
42 [ret addAction:[UIAlertAction actionWithTitle:@"Close"
43 style:UIAlertActionStyleCancel
44 handler:nil]];
45 ret.selectedButtonIndex = -1;
46 ret->_buttons = [[NSMutableArray alloc] init];
47 return ret;
48 }
49
50 // The redundant sizeof forces the compiler to validate the selector exists
51 #define SelectorString(x) (sizeof(@selector(x))? @#x : nil)
52
53 - (void)viewWillAppear:(BOOL)animated
54 {
55 [super viewWillAppear:true];
56 if (_effectView) return;
57 static const struct {
58 NSString *label;
59 NSString *image;
60 NSString *selector;
61 bool requireRunning;
62 } buttons[] = {
63 {@"Reset", @"arrow.2.circlepath", SelectorString(reset), true},
64 {@"Library", @"bookmark", SelectorString(openLibrary)},
65 {@"Connect", @"LinkCableTemplate", SelectorString(openConnectMenu), true},
66 {@"Model", @"ModelTemplate", SelectorString(changeModel)},
67 {@"States", @"square.stack", SelectorString(openStates), true},
68 {@"Cheats", @"CheatsTemplate", SelectorString(openCheats), true},
69 {@"Settings", @"gear", SelectorString(openSettings)},
70 {@"About", @"info.circle", SelectorString(showAbout)},
71 };
72
73 double width = self.view.frame.size.width / 4;
74 double height = 88;
75 for (unsigned i = 0; i < 8; i++) {
76 unsigned x = i % 4;
77 unsigned y = i / 4;
78 GBMenuButton *button = [GBMenuButton buttonWithType:UIButtonTypeSystem];
79 [button setTitle:buttons[i].label forState:UIControlStateNormal];
80 if (@available(iOS 13.0, *)) {
81 UIImage *image = [UIImage imageNamed:buttons[i].image] ?: [UIImage systemImageNamed:buttons[i].image];
82 [button setImage:image forState:UIControlStateNormal];
83 }
84 button.frame = CGRectMake(round(width * x), height * y, round(width), height);
85 button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
86 [self.view addSubview:button];
87 [_buttons addObject:button];
88
89 if (!buttons[i].selector) {
90 button.enabled = false;
91 continue;
92 }
93 SEL selector = NSSelectorFromString(buttons[i].selector);
94 if (buttons[i].requireRunning && ![GBROMManager sharedManager].currentROM) {
95 button.enabled = false;
96 continue;
97 }
98 id block = ^(){
99 [self.presentingViewController dismissViewControllerAnimated:true completion:^{
100 #pragma clang diagnostic push
101 #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
102 (void)[[UIApplication sharedApplication].delegate performSelector:selector];
103 #pragma clang diagnostic pop
104 }];
105 };
106 objc_setAssociatedObject(button, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
107 [button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside];
108 }
109
110 self.menuButtons = [_buttons copy];
111 [self updateSelectedButton];
112
113 UIVisualEffect *effect = nil;
114 /*
115 // Unfortunately, UIGlassEffect is still very buggy.
116 if (@available(iOS 19.0, *)) {
117 effect = [[objc_getClass("UIGlassEffect") alloc] init];
118 }
119 else */ {
120 effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent];
121 }
122
123 _effectView = [[UIVisualEffectView alloc] initWithEffect:nil];
124 _effectView.layer.cornerRadius = 8;
125 _effectView.layer.masksToBounds = true;
126 [self.view.window addSubview:_effectView];
127 _tipLabel = [[UILabel alloc] init];
128 unsigned tipIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBTipIndex"];
129 _tipLabel.text = tips[tipIndex % (sizeof(tips) / sizeof(tips[0]))];
130 if (@available(iOS 13.0, *)) {
131 _tipLabel.textColor = [UIColor labelColor];
132 }
133 _tipLabel.font = [UIFont systemFontOfSize:14];
134 _tipLabel.alpha = 0;
135 [[NSUserDefaults standardUserDefaults] setInteger:tipIndex + 1 forKey:@"GBTipIndex"];
136 _tipLabel.lineBreakMode = NSLineBreakByWordWrapping;
137 _tipLabel.numberOfLines = 3;
138 [_effectView.contentView addSubview:_tipLabel];
139 [self layoutTip];
140
141 [UIView animateWithDuration:0.25 animations:^{
142 _effectView.effect = effect;
143 _tipLabel.alpha = 0.8;
144 }];
145
146 }
147
148 - (void)layoutTip
149 {
150 [_effectView.superview addSubview:_effectView];
151 UIView *view = self.view.superview;
152 CGSize outerSize = view.frame.size;
153 CGSize size = [_tipLabel textRectForBounds:(CGRect){{0, 0},
154 {outerSize.width - 32,
155 outerSize.height - 32}}
156 limitedToNumberOfLines:3].size;
157 size.width = ceil(size.width);
158 _tipLabel.frame = (CGRect){{8, 8}, size};
159 unsigned topInset = view.window.safeAreaInsets.top;
160 if (!topInset && [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
161 topInset = 32; // iPadOS is buggy af
162 }
163 _effectView.frame = (CGRect) {
164 {round((outerSize.width - size.width - 16) / 2), topInset + 12},
165 {size.width + 16, size.height + 16}
166 };
167 }
168
169
170 - (void)viewWillDisappear:(BOOL)animated
171 {
172 [UIView animateWithDuration:0.25 animations:^{
173 _effectView.effect = nil;
174 _tipLabel.alpha = 0;
175 } completion:^(BOOL finished) {
176 [_effectView removeFromSuperview];
177 }];
178 [super viewWillDisappear:animated];
179 }
180
181 - (void)viewDidLayoutSubviews
182 {
183 CGRect frame = self.view.frame;
184 if (frame.size.height < 88 * 2) {
185 [self.view.heightAnchor constraintEqualToConstant:frame.size.height + 88 * 2].active = true;
186 }
187 double width = MIN(MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height) - 16, 400);
188 /* Damn I hate NSLayoutConstraints */
189 if (frame.size.width != width) {
190 for (UIView *subview in self.view.subviews) {
191 if (![subview isKindOfClass:[GBMenuButton class]]) {
192 for (NSLayoutConstraint *constraint in subview.constraints) {
193 if (constraint.constant == frame.size.width) {
194 constraint.active = false;
195 }
196 }
197 [subview.widthAnchor constraintEqualToConstant:width].active = true;
198 for (UIView *subsubview in subview.subviews) {
199 for (NSLayoutConstraint *constraint in subsubview.constraints) {
200 if (constraint.constant == frame.size.width) {
201 constraint.active = false;
202 }
203 }
204 [subsubview.widthAnchor constraintEqualToConstant:width].active = true;
205 }
206 }
207 }
208 [self.view.widthAnchor constraintEqualToConstant:width].active = true;
209 }
210 [self layoutTip];
211 [super viewDidLayoutSubviews];
212 }
213
214 // This is a hack that forces the iPad controller to display the button separator
215 - (UIViewController *)contentViewController
216 {
217 return [[UIViewController alloc] init];
218 }
219
220 #pragma mark - Vim Navigation
221
222 - (BOOL)canBecomeFirstResponder
223 {
224 return YES;
225 }
226
227 - (NSArray<UIKeyCommand *> *)keyCommands
228 {
229 return @[
230 [UIKeyCommand keyCommandWithInput:@"h" modifierFlags:0 action:@selector(moveLeft)],
231 [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:0 action:@selector(moveLeft)],
232 [UIKeyCommand keyCommandWithInput:@"j" modifierFlags:0 action:@selector(moveDown)],
233 [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(moveDown)],
234 [UIKeyCommand keyCommandWithInput:@"k" modifierFlags:0 action:@selector(moveUp)],
235 [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(moveUp)],
236 [UIKeyCommand keyCommandWithInput:@"l" modifierFlags:0 action:@selector(moveRight)],
237 [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:0 action:@selector(moveRight)],
238 [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(activateSelected)],
239 [UIKeyCommand keyCommandWithInput:@" " modifierFlags:0 action:@selector(activateSelected)],
240 [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(dismissSelf)],
241 ];
242 }
243
244 - (void)moveLeft
245 {
246 if (self.selectedButtonIndex == -1) {
247 self.selectedButtonIndex = 0;
248 }
249 else if (self.selectedButtonIndex % 4 > 0) {
250 self.selectedButtonIndex--;
251 }
252 [self updateSelectedButton];
253 }
254
255 - (void)moveRight
256 {
257 if (self.selectedButtonIndex == -1) {
258 self.selectedButtonIndex = 0;
259 }
260 else if (self.selectedButtonIndex % 4 < 3 && self.selectedButtonIndex + 1 < self.menuButtons.count) {
261 self.selectedButtonIndex++;
262 }
263 [self updateSelectedButton];
264
265 }
266
267 - (void)moveUp
268 {
269 if (self.selectedButtonIndex == -1) {
270 self.selectedButtonIndex = 0;
271 }
272 else if (self.selectedButtonIndex >= 4) {
273 self.selectedButtonIndex -= 4;
274 }
275 [self updateSelectedButton];
276 }
277
278 - (void)moveDown
279 {
280 if (self.selectedButtonIndex == -1) {
281 self.selectedButtonIndex = 0;
282 }
283 else if (self.selectedButtonIndex + 4 < self.menuButtons.count) {
284 self.selectedButtonIndex += 4;
285 }
286 [self updateSelectedButton];
287 }
288
289 - (void)activateSelected
290 {
291 if (self.selectedButtonIndex >= 0 && self.selectedButtonIndex < self.menuButtons.count) {
292 UIButton *button = self.menuButtons[self.selectedButtonIndex];
293 if (button.enabled) {
294 [button sendActionsForControlEvents:UIControlEventTouchUpInside];
295 }
296 }
297 }
298
299 - (void)updateSelectedButton
300 {
301 for (NSInteger i = 0; i < self.menuButtons.count; i++) {
302 UIButton *button = self.menuButtons[i];
303 if (i == self.selectedButtonIndex) {
304 button.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.3];
305 button.layer.borderWidth = 2.0;
306 button.layer.borderColor = [UIColor systemBlueColor].CGColor;
307 if (@available(iOS 19.0, *)) {
308 button.layer.cornerRadius = 32.0;
309 }
310 else {
311 button.layer.cornerRadius = 8.0;
312 }
313 }
314 else {
315 button.backgroundColor = [UIColor clearColor];
316 button.layer.borderWidth = 0.0;
317 button.layer.borderColor = [UIColor clearColor].CGColor;
318 }
319 }
320 }
321
322 - (void)viewDidAppear:(BOOL)animated
323 {
324 [super viewDidAppear:animated];
325 [self becomeFirstResponder];
326 }
327
328 - (void)dismissSelf
329 {
330 [self.presentingViewController dismissViewControllerAnimated:true completion:nil];
331 }
332 @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.