mirror of https://github.com/LIJI32/SameBoy.git
333 lines
13 KiB
Objective-C
333 lines
13 KiB
Objective-C
#import <objc/runtime.h>
|
|
#import "GBMenuViewController.h"
|
|
#import "GBMenuButton.h"
|
|
#import "GBViewController.h"
|
|
#import "GBROMManager.h"
|
|
|
|
static NSString *const tips[] = {
|
|
@"Tip: AirDrop ROM files from a Mac or another device to play them.",
|
|
@"Tip: Swipe right on the Game Boy screen to fast forward emulation.",
|
|
@"Tip: The D-pad can be replaced with a Swipe-pad in Control Settings. Give it a try!",
|
|
@"Tip: Swipe left on the Game Boy screen to rewind.",
|
|
@"Tip: Enable Quick Save and Load in Control Settings to use save state gestures.",
|
|
@"Tip: The turbo and rewind speeds can be changed in Emulation Settings.",
|
|
@"Tip: Change turbo and rewind behavior to locking in Controls Settings.",
|
|
@"Tip: Single Touch A+B combo can be enabled in Controls Settings.",
|
|
@"Tip: Try different scaling filters in Display Settings.",
|
|
@"Tip: Dynamically control turbo and rewind speed by enabling Dynamic Control in Control Settings.",
|
|
@"Tip: Rumble can be enabled even for games without rumble support in Control Settings.",
|
|
@"Tip: Try different color palettes for monochrome models in Display Settings.",
|
|
@"Tip: Use an external game conrtoller and Screen Mirroring for a big screen experience!",
|
|
@"Did you know? The Game Boy uses a Sharp SM83 CPU.",
|
|
@"Did you know? The Game Boy Color has 6 different hardware revisions.",
|
|
@"Did you know? The Game Boy's frame rate is approximately 59.73 frames per second.",
|
|
@"Did you know? The original Super Game Boy runs slightly faster than other Game Boys.",
|
|
@"Did you know? The Game Boy generates audio at a sample rate of over 2MHz!",
|
|
};
|
|
|
|
@implementation GBMenuViewController
|
|
{
|
|
UILabel *_tipLabel;
|
|
UIVisualEffectView *_effectView;
|
|
NSMutableArray<UIButton *> *_buttons;
|
|
}
|
|
|
|
+ (instancetype)menu
|
|
{
|
|
UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
|
|
UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
|
|
GBMenuViewController *ret = [self alertControllerWithTitle:nil
|
|
message:nil
|
|
preferredStyle:style];
|
|
[ret addAction:[UIAlertAction actionWithTitle:@"Close"
|
|
style:UIAlertActionStyleCancel
|
|
handler:nil]];
|
|
ret.selectedButtonIndex = -1;
|
|
ret->_buttons = [[NSMutableArray alloc] init];
|
|
return ret;
|
|
}
|
|
|
|
// The redundant sizeof forces the compiler to validate the selector exists
|
|
#define SelectorString(x) (sizeof(@selector(x))? @#x : nil)
|
|
|
|
- (void)viewWillAppear:(BOOL)animated
|
|
{
|
|
[super viewWillAppear:true];
|
|
if (_effectView) return;
|
|
static const struct {
|
|
NSString *label;
|
|
NSString *image;
|
|
NSString *selector;
|
|
bool requireRunning;
|
|
} buttons[] = {
|
|
{@"Reset", @"arrow.2.circlepath", SelectorString(reset), true},
|
|
{@"Library", @"bookmark", SelectorString(openLibrary)},
|
|
{@"Connect", @"LinkCableTemplate", SelectorString(openConnectMenu), true},
|
|
{@"Model", @"ModelTemplate", SelectorString(changeModel)},
|
|
{@"States", @"square.stack", SelectorString(openStates), true},
|
|
{@"Cheats", @"CheatsTemplate", SelectorString(openCheats), true},
|
|
{@"Settings", @"gear", SelectorString(openSettings)},
|
|
{@"About", @"info.circle", SelectorString(showAbout)},
|
|
};
|
|
|
|
double width = self.view.frame.size.width / 4;
|
|
double height = 88;
|
|
for (unsigned i = 0; i < 8; i++) {
|
|
unsigned x = i % 4;
|
|
unsigned y = i / 4;
|
|
GBMenuButton *button = [GBMenuButton buttonWithType:UIButtonTypeSystem];
|
|
[button setTitle:buttons[i].label forState:UIControlStateNormal];
|
|
if (@available(iOS 13.0, *)) {
|
|
UIImage *image = [UIImage imageNamed:buttons[i].image] ?: [UIImage systemImageNamed:buttons[i].image];
|
|
[button setImage:image forState:UIControlStateNormal];
|
|
}
|
|
button.frame = CGRectMake(round(width * x), height * y, round(width), height);
|
|
button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
|
|
[self.view addSubview:button];
|
|
[_buttons addObject:button];
|
|
|
|
if (!buttons[i].selector) {
|
|
button.enabled = false;
|
|
continue;
|
|
}
|
|
SEL selector = NSSelectorFromString(buttons[i].selector);
|
|
if (buttons[i].requireRunning && ![GBROMManager sharedManager].currentROM) {
|
|
button.enabled = false;
|
|
continue;
|
|
}
|
|
id block = ^(){
|
|
[self.presentingViewController dismissViewControllerAnimated:true completion:^{
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
(void)[[UIApplication sharedApplication].delegate performSelector:selector];
|
|
#pragma clang diagnostic pop
|
|
}];
|
|
};
|
|
objc_setAssociatedObject(button, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
|
|
[button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside];
|
|
}
|
|
|
|
self.menuButtons = [_buttons copy];
|
|
[self updateSelectedButton];
|
|
|
|
UIVisualEffect *effect = nil;
|
|
/*
|
|
// Unfortunately, UIGlassEffect is still very buggy.
|
|
if (@available(iOS 19.0, *)) {
|
|
effect = [[objc_getClass("UIGlassEffect") alloc] init];
|
|
}
|
|
else */ {
|
|
effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent];
|
|
}
|
|
|
|
_effectView = [[UIVisualEffectView alloc] initWithEffect:nil];
|
|
_effectView.layer.cornerRadius = 8;
|
|
_effectView.layer.masksToBounds = true;
|
|
[self.view.window addSubview:_effectView];
|
|
_tipLabel = [[UILabel alloc] init];
|
|
unsigned tipIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBTipIndex"];
|
|
_tipLabel.text = tips[tipIndex % (sizeof(tips) / sizeof(tips[0]))];
|
|
if (@available(iOS 13.0, *)) {
|
|
_tipLabel.textColor = [UIColor labelColor];
|
|
}
|
|
_tipLabel.font = [UIFont systemFontOfSize:14];
|
|
_tipLabel.alpha = 0;
|
|
[[NSUserDefaults standardUserDefaults] setInteger:tipIndex + 1 forKey:@"GBTipIndex"];
|
|
_tipLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
|
_tipLabel.numberOfLines = 3;
|
|
[_effectView.contentView addSubview:_tipLabel];
|
|
[self layoutTip];
|
|
|
|
[UIView animateWithDuration:0.25 animations:^{
|
|
_effectView.effect = effect;
|
|
_tipLabel.alpha = 0.8;
|
|
}];
|
|
|
|
}
|
|
|
|
- (void)layoutTip
|
|
{
|
|
[_effectView.superview addSubview:_effectView];
|
|
UIView *view = self.view.superview;
|
|
CGSize outerSize = view.frame.size;
|
|
CGSize size = [_tipLabel textRectForBounds:(CGRect){{0, 0},
|
|
{outerSize.width - 32,
|
|
outerSize.height - 32}}
|
|
limitedToNumberOfLines:3].size;
|
|
size.width = ceil(size.width);
|
|
_tipLabel.frame = (CGRect){{8, 8}, size};
|
|
unsigned topInset = view.window.safeAreaInsets.top;
|
|
if (!topInset && [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
|
topInset = 32; // iPadOS is buggy af
|
|
}
|
|
_effectView.frame = (CGRect) {
|
|
{round((outerSize.width - size.width - 16) / 2), topInset + 12},
|
|
{size.width + 16, size.height + 16}
|
|
};
|
|
}
|
|
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
{
|
|
[UIView animateWithDuration:0.25 animations:^{
|
|
_effectView.effect = nil;
|
|
_tipLabel.alpha = 0;
|
|
} completion:^(BOOL finished) {
|
|
[_effectView removeFromSuperview];
|
|
}];
|
|
[super viewWillDisappear:animated];
|
|
}
|
|
|
|
- (void)viewDidLayoutSubviews
|
|
{
|
|
CGRect frame = self.view.frame;
|
|
if (frame.size.height < 88 * 2) {
|
|
[self.view.heightAnchor constraintEqualToConstant:frame.size.height + 88 * 2].active = true;
|
|
}
|
|
double width = MIN(MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height) - 16, 400);
|
|
/* Damn I hate NSLayoutConstraints */
|
|
if (frame.size.width != width) {
|
|
for (UIView *subview in self.view.subviews) {
|
|
if (![subview isKindOfClass:[GBMenuButton class]]) {
|
|
for (NSLayoutConstraint *constraint in subview.constraints) {
|
|
if (constraint.constant == frame.size.width) {
|
|
constraint.active = false;
|
|
}
|
|
}
|
|
[subview.widthAnchor constraintEqualToConstant:width].active = true;
|
|
for (UIView *subsubview in subview.subviews) {
|
|
for (NSLayoutConstraint *constraint in subsubview.constraints) {
|
|
if (constraint.constant == frame.size.width) {
|
|
constraint.active = false;
|
|
}
|
|
}
|
|
[subsubview.widthAnchor constraintEqualToConstant:width].active = true;
|
|
}
|
|
}
|
|
}
|
|
[self.view.widthAnchor constraintEqualToConstant:width].active = true;
|
|
}
|
|
[self layoutTip];
|
|
[super viewDidLayoutSubviews];
|
|
}
|
|
|
|
// This is a hack that forces the iPad controller to display the button separator
|
|
- (UIViewController *)contentViewController
|
|
{
|
|
return [[UIViewController alloc] init];
|
|
}
|
|
|
|
#pragma mark - Vim Navigation
|
|
|
|
- (BOOL)canBecomeFirstResponder
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (NSArray<UIKeyCommand *> *)keyCommands
|
|
{
|
|
return @[
|
|
[UIKeyCommand keyCommandWithInput:@"h" modifierFlags:0 action:@selector(moveLeft)],
|
|
[UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:0 action:@selector(moveLeft)],
|
|
[UIKeyCommand keyCommandWithInput:@"j" modifierFlags:0 action:@selector(moveDown)],
|
|
[UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(moveDown)],
|
|
[UIKeyCommand keyCommandWithInput:@"k" modifierFlags:0 action:@selector(moveUp)],
|
|
[UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(moveUp)],
|
|
[UIKeyCommand keyCommandWithInput:@"l" modifierFlags:0 action:@selector(moveRight)],
|
|
[UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:0 action:@selector(moveRight)],
|
|
[UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(activateSelected)],
|
|
[UIKeyCommand keyCommandWithInput:@" " modifierFlags:0 action:@selector(activateSelected)],
|
|
[UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(dismissSelf)],
|
|
];
|
|
}
|
|
|
|
- (void)moveLeft
|
|
{
|
|
if (self.selectedButtonIndex == -1) {
|
|
self.selectedButtonIndex = 0;
|
|
}
|
|
else if (self.selectedButtonIndex % 4 > 0) {
|
|
self.selectedButtonIndex--;
|
|
}
|
|
[self updateSelectedButton];
|
|
}
|
|
|
|
- (void)moveRight
|
|
{
|
|
if (self.selectedButtonIndex == -1) {
|
|
self.selectedButtonIndex = 0;
|
|
}
|
|
else if (self.selectedButtonIndex % 4 < 3 && self.selectedButtonIndex + 1 < self.menuButtons.count) {
|
|
self.selectedButtonIndex++;
|
|
}
|
|
[self updateSelectedButton];
|
|
|
|
}
|
|
|
|
- (void)moveUp
|
|
{
|
|
if (self.selectedButtonIndex == -1) {
|
|
self.selectedButtonIndex = 0;
|
|
}
|
|
else if (self.selectedButtonIndex >= 4) {
|
|
self.selectedButtonIndex -= 4;
|
|
}
|
|
[self updateSelectedButton];
|
|
}
|
|
|
|
- (void)moveDown
|
|
{
|
|
if (self.selectedButtonIndex == -1) {
|
|
self.selectedButtonIndex = 0;
|
|
}
|
|
else if (self.selectedButtonIndex + 4 < self.menuButtons.count) {
|
|
self.selectedButtonIndex += 4;
|
|
}
|
|
[self updateSelectedButton];
|
|
}
|
|
|
|
- (void)activateSelected
|
|
{
|
|
if (self.selectedButtonIndex >= 0 && self.selectedButtonIndex < self.menuButtons.count) {
|
|
UIButton *button = self.menuButtons[self.selectedButtonIndex];
|
|
if (button.enabled) {
|
|
[button sendActionsForControlEvents:UIControlEventTouchUpInside];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)updateSelectedButton
|
|
{
|
|
for (NSInteger i = 0; i < self.menuButtons.count; i++) {
|
|
UIButton *button = self.menuButtons[i];
|
|
if (i == self.selectedButtonIndex) {
|
|
button.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.3];
|
|
button.layer.borderWidth = 2.0;
|
|
button.layer.borderColor = [UIColor systemBlueColor].CGColor;
|
|
if (@available(iOS 19.0, *)) {
|
|
button.layer.cornerRadius = 32.0;
|
|
}
|
|
else {
|
|
button.layer.cornerRadius = 8.0;
|
|
}
|
|
}
|
|
else {
|
|
button.backgroundColor = [UIColor clearColor];
|
|
button.layer.borderWidth = 0.0;
|
|
button.layer.borderColor = [UIColor clearColor].CGColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated
|
|
{
|
|
[super viewDidAppear:animated];
|
|
[self becomeFirstResponder];
|
|
}
|
|
|
|
- (void)dismissSelf
|
|
{
|
|
[self.presentingViewController dismissViewControllerAnimated:true completion:nil];
|
|
}
|
|
@end
|