git.y1.nz

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.