From a372b2ec0f8bb7e6dfa48e3027a53a12f38ccd2e Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Sat, 31 Aug 2024 21:06:31 +0300 Subject: [PATCH] Printer emulation in iOS --- iOS/GBCheckableAlertController.h | 5 ++ iOS/GBCheckableAlertController.m | 32 +++++++ iOS/GBMenuViewController.m | 52 +++++++++--- iOS/GBPrinterFeedController.h | 7 ++ iOS/GBPrinterFeedController.m | 109 ++++++++++++++++++++++++ iOS/GBViewController.h | 2 + iOS/GBViewController.m | 139 +++++++++++++++++++++++++++++++ iOS/LinkCableTemplate@2x.png | Bin 0 -> 561 bytes iOS/LinkCableTemplate@3x.png | Bin 0 -> 851 bytes iOS/PrinterTemplate@2x.png | Bin 0 -> 405 bytes iOS/PrinterTemplate@3x.png | Bin 0 -> 582 bytes 11 files changed, 335 insertions(+), 11 deletions(-) create mode 100644 iOS/GBCheckableAlertController.h create mode 100644 iOS/GBCheckableAlertController.m create mode 100644 iOS/GBPrinterFeedController.h create mode 100644 iOS/GBPrinterFeedController.m create mode 100644 iOS/LinkCableTemplate@2x.png create mode 100644 iOS/LinkCableTemplate@3x.png create mode 100644 iOS/PrinterTemplate@2x.png create mode 100644 iOS/PrinterTemplate@3x.png 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 0000000000000000000000000000000000000000..2e9552fa0cf4b4338127b86f54c002b1bfc3408b GIT binary patch literal 561 zcmV-10?z%3P)1w!AnhO`T`b}vMF~QeF1o0TOQBFZ^hX$%EQNLyijc+G#UCKLIQTc*x>$56 z6x51-4m~pH#pasphrZvKJD%i{@4d?l?kaM|Gm@zSa;ujS*lfOTLj@+YXh2kNQjcA&3&wkYiZ%8 zi+QSryG9Uhv~VY;5mt3@YkVLGvDWil8WTIz!Ic>si{8@#){O_e(=mJ6f)ysVGbHAS zfUR7$G3&OQ84eF_^*~HD9a!PcGf)ujsH7vS#5{NgzS8`CVGC^Je0Kpy#0(Rp%8pZ@ z!@B|2b((CC5rQzLtOGnWHhHOKld-YMH67e4ZwSJP*7MysAHZ_y6hRnh;aaBQm2|9& z3+;;;Wc0yj&L}z`z;5To2|_nBY{fJ}nCJKDDKqXILHMMs5z6cv6TA8uKLp-!yT+nl zSYI@5iHM0A+pvWz?znHfZ1mTu`&c7I|B2c^i100000NkvXXu0mjfXA}fz literal 0 HcmV?d00001 diff --git a/iOS/LinkCableTemplate@3x.png b/iOS/LinkCableTemplate@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..edcef7957eb2dc9e987d5e872cb8af17c8a438ec GIT binary patch literal 851 zcmV-Z1FZasP)16$A`vA4A{>nJ|sqQXh`MA#%hl z(x*LPemO?^UFdNF*55IxHl@Ka@~d0?G|(+FqmZkJjif@3$wlrfWWQWwN+G-CA`ca^ zS1xi#A&2E6mlSeVE;6i;hjNiE3i)oY$SqBrDdxOfWK3T zRAdcT8Z%CXOlEJ^U}cYbnaoI50s6=UwGx@LIhFD=Ny2neN3|s8IPV_Mu^&it$|lqF z*}oI|xh>h?{f-5@&pbbITfy3E6UPq503pwK}FW0U89-)FhrrG3_G!Gs#-x-&5 zt5#D-Ct;F2=RiePx3x_0n8l39e4x~`ry(;^yRHi28uN-8spZa!#+;LitmHr;pYn~d zN*m3*C}vb{OEa&KXL6B2t<-5Lhw)EXF7m6oU6P9&R>&Q>$lh`o|8yy2N-nZrA@}7X z#}qOt7rCmCGx>TQ=Wk>(T(V3>z7DkF}OM=Hk_ zm_;TW5<^;!A=AihN5l@TDLeW5Ba?^Ob{!C#xYjZZNJREGBq1}b(`HR6 zHS&!VQ5vo8D=(1NCxW9y2+{rZ&!%+9qT>hYE>cnz9kk; daHU(+;t$JDW!8DOD>488002ovPDHLkV1f(9gDe05 literal 0 HcmV?d00001 diff --git a/iOS/PrinterTemplate@2x.png b/iOS/PrinterTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1e75ac33ebb78327ba58167906a495818f683b GIT binary patch literal 405 zcmV;G0c!q-@25Jle!7?-AG$n`8L=LEi#DN<6nFun^EA9@ADjW&Q$hfLY5A#ZIzK#CxIE)Tr#W=zb?48<}K+iV3l3T%^&%0IDQjSS;1p#$u_)HXh4i=m%`WRHNICwm0!JlP{)=gA%c zJ5Tlq*m?5*5I8-y%$FUfbpoB@kZrnxueB?9{g-SYFg{pT0%?!HxHVW|dhA<=dp5WI z0L)lSn^E&~u%n)s7LVyLZ};{+WiavYVCW9nkI58VXw3v#E)>3N?Yef|Yb<842J)S3jmjZa0rwZ!IhXf1R0q#6fVG+BeW7QRqBc$bOEVKm*mlu1dN}bL#z1B zHXrZ3__v(^6nnxHD;$w9*AXjB@eC33uw;Y$UQ2kkIDN!oI0+KO=-chiP826Wf*pUC z8i-7oIefI(f2;)`3ll3sgBNCCLX1Uj`vOunPa_6A_jT&-ivjs*#5K=7 z%R6l`pti|NEXz(@(yU@u4rD73qM{-x5CWniEfAKX;zA%SL`6;@ghWMJAgo12ULY(* zMP48*MMYj9imihXd+ZUSb6r$iwk$JgG z?Ntv1bP*y(jIi(-s{n{xy467c5=dhlLk7|s#}I+^#xW!y&2bC?NOv4V0MZ@D5P)>Y zF$5spaSQ=ScN{|i(w%uZPFlp_9uUsxNg7^e2GgQpM5)YMoW? ztyI%BfwDSa1I$cKSS3(aDb0XUX+{;s@ zZKF8jxsRN+(nDT=@|1b66{bJlblJr?rf0Lw5BB}gt(1B1itN9zx}Gwr>yGsI4Ie&u U2vxx?3jhEB07*qoM6N<$f+4W=ng9R* literal 0 HcmV?d00001