Printer emulation in iOS

This commit is contained in:
Lior Halphon 2024-08-31 21:06:31 +03:00
parent 7127e3e068
commit a372b2ec0f
11 changed files with 335 additions and 11 deletions

View File

@ -0,0 +1,5 @@
#import <UIKit/UIKit.h>
@interface GBCheckableAlertController : UIAlertController
@property UIAlertAction *selectedAction;
@end

View File

@ -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

View File

@ -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];

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
@interface GBPrinterFeedController : UINavigationController
- (instancetype)initWithImage:(UIImage *)image;
- (void)emptyPrinterFeed;
@end

View File

@ -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

View File

@ -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 <NSURL *> *)urls openInPlace:(bool)inPlace;

View File

@ -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 <sys/stat.h>
@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 851 B

BIN
iOS/PrinterTemplate@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

BIN
iOS/PrinterTemplate@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B