diff --git a/iOS/GBCheckableAlertController.h b/iOS/GBCheckableAlertController.h new file mode 100644 index 0000000..7423565 --- /dev/null +++ b/iOS/GBCheckableAlertController.h @@ -0,0 +1,5 @@ +#import + +@interface GBCheckableAlertController : UIAlertController +@property UIAlertAction *selectedAction; +@end diff --git a/iOS/GBCheckableAlertController.m b/iOS/GBCheckableAlertController.m new file mode 100644 index 0000000..d1d2b98 --- /dev/null +++ b/iOS/GBCheckableAlertController.m @@ -0,0 +1,32 @@ +#import "GBCheckableAlertController.h" + +@implementation GBCheckableAlertController +{ + bool _addedChecks; +} + +- (void)viewWillAppear:(BOOL)animated +{ + if (!_addedChecks && _selectedAction) { + _addedChecks = true; + NSMutableSet *set = [NSMutableSet setWithObject:self.view]; + while (set.count) { + UIView *view = [set anyObject]; + [set removeObject:view]; + if ([view.debugDescription containsString:_selectedAction.debugDescription]) { + UIImageView *checkImage = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"checkmark"]]; + CGRect bounds = view.bounds; + CGRect frame = checkImage.frame; + frame.origin.x = bounds.size.width - frame.size.width - 12; + frame.origin.y = round((bounds.size.height - frame.size.height) / 2); + checkImage.frame = frame; + [view addSubview:checkImage]; + break; + } + [set addObjectsFromArray:view.subviews]; + } + } + [super viewWillAppear:animated]; +} + +@end diff --git a/iOS/GBMenuViewController.m b/iOS/GBMenuViewController.m index 80effab..02e3fa1 100644 --- a/iOS/GBMenuViewController.m +++ b/iOS/GBMenuViewController.m @@ -54,27 +54,30 @@ static NSString *const tips[] = { NSString *label; NSString *image; NSString *selector; + bool requireRunning; } buttons[] = { - {@"Reset", @"arrow.2.circlepath", SelectorString(reset)}, + {@"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)}, + {@"States", @"square.stack", SelectorString(openStates), true}, + {@"Cheats", @"wand.and.stars", nil}, // TODO {@"Settings", @"gear", SelectorString(openSettings)}, {@"About", @"info.circle", SelectorString(showAbout)}, }; - double width = self.view.frame.size.width / 3; + double width = self.view.frame.size.width / 4; double height = 88; - for (unsigned i = 0; i < 6; i++) { - unsigned x = i % 3; - unsigned y = i / 3; + 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(width * x, height * y, width, height); + button.frame = CGRectMake(round(width * x), height * y, round(width), height); button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; [self.view addSubview:button]; @@ -82,9 +85,12 @@ static NSString *const tips[] = { button.enabled = false; continue; } + if (buttons[i].selector == nil) { + button.enabled = false; + continue; + } SEL selector = NSSelectorFromString(buttons[i].selector); - if ((selector == @selector(reset) || selector == @selector(openStates)) - && ![GBROMManager sharedManager].currentROM) { + if (buttons[i].requireRunning && ![GBROMManager sharedManager].currentROM) { button.enabled = false; continue; } @@ -149,8 +155,32 @@ static NSString *const tips[] = { - (void)viewDidLayoutSubviews { - if (self.view.bounds.size.height < 88 * 2) { - [self.view.heightAnchor constraintEqualToConstant:self.view.bounds.size.height + 88 * 2].active = true; + 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]; diff --git a/iOS/GBPrinterFeedController.h b/iOS/GBPrinterFeedController.h new file mode 100644 index 0000000..fda34e5 --- /dev/null +++ b/iOS/GBPrinterFeedController.h @@ -0,0 +1,7 @@ +#import + +@interface GBPrinterFeedController : UINavigationController +- (instancetype)initWithImage:(UIImage *)image; +- (void)emptyPrinterFeed; +@end + diff --git a/iOS/GBPrinterFeedController.m b/iOS/GBPrinterFeedController.m new file mode 100644 index 0000000..155edfd --- /dev/null +++ b/iOS/GBPrinterFeedController.m @@ -0,0 +1,109 @@ +#import "GBPrinterFeedController.h" +#import "GBViewController.h" + +@implementation GBPrinterFeedController +{ + UIViewController *_scrollViewController; + UIImage *_image; +} + +- (instancetype)initWithImage:(UIImage *)image +{ + _image = image; + _scrollViewController = [[UIViewController alloc] init]; + _scrollViewController.title = @"Printer Feed"; + _scrollViewController.view.backgroundColor = [UIColor systemBackgroundColor]; + + UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:_scrollViewController.view.bounds]; + + scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + scrollView.scrollEnabled = true; + scrollView.pagingEnabled = false; + scrollView.showsVerticalScrollIndicator = true; + scrollView.showsHorizontalScrollIndicator = false; + [_scrollViewController.view addSubview:scrollView]; + + CGSize size = image.size; + while (size.width < 320) { + size.width *= 2; + size.height *= 2; + } + UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; + imageView.contentMode = UIViewContentModeScaleToFill; + imageView.frame = (CGRect){{0, 0}, size}; + imageView.layer.magnificationFilter = kCAFilterNearest; + + scrollView.contentSize = size; + [scrollView addSubview:imageView]; + + CGSize contentSize = size; + self.preferredContentSize = contentSize; + + self = [self initWithRootViewController:_scrollViewController]; + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:self + action:@selector(dismissFromParent)]; + [self.visibleViewController.navigationItem setLeftBarButtonItem:close]; + [_scrollViewController setToolbarItems:@[ + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction + target:self + action:@selector(presentShareSheet)], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash + target:self + action:@selector(emptyFeed)], + + ] animated:false]; + [self setToolbarHidden:false animated:false]; + return self; +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { + CGRect frame = self.view.frame; + frame.origin.x = ([UIScreen mainScreen].bounds.size.width - 320) / 2; + frame.size.width = 320; + self.view.frame = frame; + + UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds + byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight + cornerRadii:CGSizeMake(12.0, 12.0)]; + + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + maskLayer.frame = self.view.bounds; + maskLayer.path = maskPath.CGPath; + + self.view.layer.mask = maskLayer; + } +} + +- (void)presentShareSheet +{ + NSURL *url = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"Game Boy Printer Image.png"]]; + [UIImagePNGRepresentation(_image) writeToURL:url atomically:false]; + [self presentViewController:[[UIActivityViewController alloc] initWithActivityItems:@[url] + applicationActivities:nil] + animated:true + completion:nil]; +} + +- (void)emptyFeed +{ + [(GBViewController *)UIApplication.sharedApplication.delegate emptyPrinterFeed]; +} + +- (void)dismissFromParent +{ + [self.presentingViewController dismissViewControllerAnimated:true completion:nil]; +} + +- (UIModalPresentationStyle)modalPresentationStyle +{ + return UIModalPresentationFormSheet; +} +@end diff --git a/iOS/GBViewController.h b/iOS/GBViewController.h index e7d9e5f..bd168cd 100644 --- a/iOS/GBViewController.h +++ b/iOS/GBViewController.h @@ -22,6 +22,8 @@ typedef enum { - (void)openStates; - (void)openSettings; - (void)showAbout; +- (void)openConnectMenu; +- (void)emptyPrinterFeed; - (void)saveStateToFile:(NSString *)file; - (bool)loadStateFromFile:(NSString *)file; - (bool)handleOpenURLs:(NSArray *)urls openInPlace:(bool)inPlace; diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m index eef58cc..dab9f5c 100644 --- a/iOS/GBViewController.m +++ b/iOS/GBViewController.m @@ -12,6 +12,8 @@ #import "GBAboutController.h" #import "GBSettingsViewController.h" #import "GBStatesViewController.h" +#import "GBCheckableAlertController.h" +#import "GBPrinterFeedController.h" #import "GCExtendedGamepad+AllElements.h" #import "GBZipReader.h" #import @@ -67,6 +69,11 @@ UIWindow *_mirrorWindow; GBView *_mirrorView; + + bool _printerConnected; + UIButton *_printerButton; + UIActivityIndicatorView *_printerSpinner; + NSMutableData *_currentPrinterImageData; } static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type) @@ -81,6 +88,21 @@ static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type) [self vblankWithType:type]; } + +static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, + uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; +} + +static void printDone(GB_gameboy_t *gb) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self printDone]; +} + + static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) { static NSString *buffer = @""; @@ -350,7 +372,33 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) object:nil]; } + _printerButton = [[UIButton alloc] init]; + _printerSpinner = [[UIActivityIndicatorView alloc] init]; + [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation]; + if (@available(iOS 13.0, *)) { + [_printerButton setImage:[UIImage systemImageNamed:@"printer" + withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]] + forState:UIControlStateNormal]; + _printerButton.backgroundColor = [UIColor systemBackgroundColor]; + } + else { + UIImage *rotateImage = [[UIImage imageNamed:@"PrinterTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [_printerButton setImage:rotateImage + forState:UIControlStateNormal]; + _printerButton.backgroundColor = [UIColor whiteColor]; + } + + _printerButton.layer.cornerRadius = 6; + _printerButton.alpha = 0; + [_printerButton addTarget:self + action:@selector(showPrinterFeed) + forControlEvents:UIControlEventTouchUpInside]; + + + [_backgroundView addSubview:_printerButton]; + [_backgroundView addSubview:_printerSpinner]; + [self updateMirrorWindow]; @@ -835,6 +883,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) 32, 32); } + _printerButton.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32, + 32, + 32); + + _printerSpinner.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - 32 - 8, + 32, + 32); + } - (UIInterfaceOrientationMask)supportedInterfaceOrientations @@ -1584,4 +1642,85 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response }); } +- (void)openConnectMenu +{ + UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad? + UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet; + GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect which accessory?" + message:nil + preferredStyle:style]; + [menu addAction:[UIAlertAction actionWithTitle:@"None" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + _printerConnected = false; + _currentPrinterImageData = nil; + [UIView animateWithDuration:0.25 animations:^{ + _printerButton.alpha = 0; + }]; + [_printerSpinner stopAnimating]; + GB_disconnect_serial(&_gb); + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Game Boy Printer" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + _printerConnected = true; + GB_connect_printer(&_gb, printImage, printDone); + }]]; + menu.selectedAction = menu.actions[_printerConnected]; + [menu addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:menu animated:true completion:nil]; +} + +- (void)printImage:(uint32_t *)imageBytes height:(unsigned) height + topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin + exposure:(unsigned) exposure +{ + uint32_t paddedImage[160 * (topMargin + height + bottomMargin)]; + memset(paddedImage, 0xFF, sizeof(paddedImage)); + memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0])); + if (!_currentPrinterImageData) { + _currentPrinterImageData = [[NSMutableData alloc] init]; + } + [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.25 animations:^{ + _printerButton.alpha = 1; + }]; + [_printerSpinner startAnimating]; + }); + +} + +- (void)printDone +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [_printerSpinner stopAnimating]; + }); +} + +- (void)showPrinterFeed +{ + UIImage *image = [self imageFromData:_currentPrinterImageData + width:160 + height:_currentPrinterImageData.length / 160 / sizeof(uint32_t)]; + + [self presentViewController:[[GBPrinterFeedController alloc] initWithImage:image] + animated:true + completion:nil]; + +} + +- (void)emptyPrinterFeed +{ + _currentPrinterImageData = nil; + [UIView animateWithDuration:0.25 animations:^{ + _printerButton.alpha = 0; + }]; + [_printerSpinner stopAnimating]; + [self dismissViewController]; +} + @end diff --git a/iOS/LinkCableTemplate@2x.png b/iOS/LinkCableTemplate@2x.png new file mode 100644 index 0000000..2e9552f Binary files /dev/null and b/iOS/LinkCableTemplate@2x.png differ diff --git a/iOS/LinkCableTemplate@3x.png b/iOS/LinkCableTemplate@3x.png new file mode 100644 index 0000000..edcef79 Binary files /dev/null and b/iOS/LinkCableTemplate@3x.png differ diff --git a/iOS/PrinterTemplate@2x.png b/iOS/PrinterTemplate@2x.png new file mode 100644 index 0000000..fd1e75a Binary files /dev/null and b/iOS/PrinterTemplate@2x.png differ diff --git a/iOS/PrinterTemplate@3x.png b/iOS/PrinterTemplate@3x.png new file mode 100644 index 0000000..3d8c6c7 Binary files /dev/null and b/iOS/PrinterTemplate@3x.png differ