Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/ios/ViewControllerCommon.mm
5657 views
#import "ios/CameraHelper.h"
#import "ios/ViewControllerCommon.h"
#import "ios/Controls.h"
#import "ios/IAPManager.h"
#include "Common/System/Request.h"
#include "Common/Input/InputState.h"
#include "Common/System/NativeApp.h"
#include "Common/Log.h"
#include "Core/HLE/sceUsbCam.h"
#include "Core/HLE/sceUsbGps.h"
#include "Core/System.h"
#include "Core/Config.h"

@interface PPSSPPBaseViewController () {
	int imageRequestId;
	NSString *imageFilename;
	CameraHelper *cameraHelper;
	LocationHelper *locationHelper;
	ICadeTracker g_iCadeTracker;
	TouchTracker g_touchTracker;
}

@property (strong, nonatomic) NSOperationQueue *accelerometerQueue;
@property (nonatomic) GCController *gameController __attribute__((weak_import));
@property (strong, nonatomic) CMMotionManager *motionManager;

@end

@implementation PPSSPPBaseViewController {
	UIScreenEdgePanGestureRecognizer *mBackGestureRecognizer;
}

- (id)init {
	self = [super init];
	if (self) {
		sharedViewController = self;

		g_iCadeTracker.InitKeyMap();

		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidConnect:) name:GCControllerDidConnectNotification object:nil];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidDisconnect:) name:GCControllerDidDisconnectNotification object:nil];

		// Observe orientation changes
		[[NSNotificationCenter defaultCenter] addObserver:self
												selector:@selector(onOrientationChanged)
												name:UIDeviceOrientationDidChangeNotification
												object:nil];
	}
	self.accelerometerQueue = [[NSOperationQueue alloc] init];
	self.accelerometerQueue.name = @"AccelerometerQueue";
	self.accelerometerQueue.maxConcurrentOperationCount = 1;

	return self;
}

- (void)shutdown {
	self.gameController = nil;
	[[NSNotificationCenter defaultCenter] removeObserver:self];

	_dbg_assert_(sharedViewController != nil);
	sharedViewController = nil;
}

- (BOOL)prefersHomeIndicatorAutoHidden {
	if (g_Config.iAppSwitchMode == (int)AppSwitchMode::DOUBLE_SWIPE_INDICATOR) {
		return NO;
	} else {
		return YES;
	}
}

- (void)didBecomeActive {
	if (self.motionManager.accelerometerAvailable) {
		self.motionManager.accelerometerUpdateInterval = 1.0 / 60.0;
		INFO_LOG(Log::G3D, "Starting accelerometer updates.");

		[self.motionManager startAccelerometerUpdatesToQueue:self.accelerometerQueue
							withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
			if (error) {
				NSLog(@"Accelerometer error: %@", error);
				return;
			}
			ProcessAccelerometerData(accelerometerData);
		}];
	} else {
		INFO_LOG(Log::G3D, "No accelerometer available, not starting updates.");
	}
}

- (void)willResignActive {
	// Stop accelerometer updates
	if (self.motionManager.accelerometerActive) {
		INFO_LOG(Log::G3D, "Stopping accelerometer updates");
		[self.motionManager stopAccelerometerUpdates];
	}
}

- (void)appWillTerminate:(NSNotification *)notification {
	[self shutdown];
}

- (void)viewDidAppear:(BOOL)animated {
	[super viewDidAppear:animated];
	INFO_LOG(Log::G3D, "viewDidAppear");
	[self hideKeyboard];
	[self updateGesture];

	// This needs to be called really late during startup, unfortunately.
#if PPSSPP_PLATFORM(IOS_APP_STORE)
	[IAPManager sharedIAPManager];  // Kick off the IAPManager early.
	NSLog(@"Metal viewDidAppear. updating icon");
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
		[[IAPManager sharedIAPManager] updateIcon:false];
		[self hideKeyboard];
	});
#endif  // IOS_APP_STORE
}

// Enables tapping for edge area.
-(UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
	if (GetUIState() == UISTATE_INGAME) {
		// In-game, we need all the control we can get. Though, we could possibly
		// allow the top edge?
		INFO_LOG(Log::System, "Defer system gestures on all edges");
		return UIRectEdgeAll;
	} else {
		INFO_LOG(Log::System, "Allow system gestures on the bottom");
		// Allow task switching gestures to take precedence, without causing
		// scroll events in the UI. Otherwise, we get "ghost" scrolls when switching tasks.
		return UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeRight;
	}
}

- (void)controllerDidConnect:(NSNotification *)note
{
	if (![[GCController controllers] containsObject:self.gameController]) self.gameController = nil;

	if (self.gameController != nil) return; // already have a connected controller

	[self setupController:(GCController *)note.object];
}

- (void)controllerDidDisconnect:(NSNotification *)note
{
	if (self.gameController == note.object) {
		ShutdownController(self.gameController);
		self.gameController = nil;

		if ([[GCController controllers] count] > 0) {
			[self setupController:[[GCController controllers] firstObject]];
		}
	}
}

- (void)setupController:(GCController *)controller {
	self.gameController = controller;
	if (!InitController(controller)) {
		self.gameController = nil;
	}
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	g_touchTracker.Began(touches, self.view);
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	g_touchTracker.Moved(touches, self.view);
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	g_touchTracker.Ended(touches, self.view);
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
	g_touchTracker.Cancelled(touches, self.view);
}

- (void)pickPhoto:(NSString *)saveFilename requestId:(int)requestId {
	imageRequestId = requestId;
	imageFilename = saveFilename;
	NSLog(@"Picking photo to save to %@ (id: %d)", saveFilename, requestId);

	UIImagePickerController *picker = [[UIImagePickerController alloc] init];
	picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
	picker.delegate = self;
	[self presentViewController:picker animated:YES completion:nil];
}

- (void)imagePickerController:(UIImagePickerController *)picker
		didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info {

	UIImage *image = info[UIImagePickerControllerOriginalImage];

	// Convert to JPEG with 90% quality
	NSData *jpegData = UIImageJPEGRepresentation(image, 0.9);
	if (jpegData) {
		// Do something with the JPEG data (e.g., save to file)
		[jpegData writeToFile:imageFilename atomically:YES];
		NSLog(@"Saved JPEG image to %@", imageFilename);
		g_requestManager.PostSystemSuccess(imageRequestId, "", 1);
	} else {
		g_requestManager.PostSystemFailure(imageRequestId);
	}

	[picker dismissViewControllerAnimated:YES completion:nil];
	[self hideKeyboard];
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
	NSLog(@"User cancelled image picker");

	[picker dismissViewControllerAnimated:YES completion:nil];

	// You can also call your custom callback or use the requestId here
	g_requestManager.PostSystemFailure(imageRequestId);
	[self hideKeyboard];
}

- (void)handleSwipeFrom:(UIScreenEdgePanGestureRecognizer *)recognizer {
	if (recognizer.state == UIGestureRecognizerStateEnded) {
		KeyInput key;
		key.flags = KeyInputFlags::DOWN | KeyInputFlags::UP;
		key.keyCode = NKCODE_BACK;
		key.deviceId = DEVICE_ID_TOUCH;
		NativeKey(key);
		INFO_LOG(Log::System, "Detected back swipe");
	}
}

- (void)updateGesture {
	INFO_LOG(Log::System, "Updating swipe gesture.");

	if (mBackGestureRecognizer) {
		INFO_LOG(Log::System, "Removing swipe gesture.");
		[[self view] removeGestureRecognizer:mBackGestureRecognizer];
		mBackGestureRecognizer = nil;
	}

	if (GetUIState() != UISTATE_INGAME) {
		INFO_LOG(Log::System, "Adding swipe gesture.");
		mBackGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFrom:) ];
		[mBackGestureRecognizer setEdges:UIRectEdgeLeft];
		[[self view] addGestureRecognizer:mBackGestureRecognizer];
	}
}

- (void)uiStateChanged {
	[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
	[self hideKeyboard];
	[self updateGesture];
}

- (void)startVideo:(int)width height:(int)height {
	[cameraHelper startVideo:width h:height];
}

- (void)stopVideo {
	[cameraHelper stopVideo];
}

- (void)PushCameraImageIOS:(long long)len buffer:(unsigned char*)data {
	Camera::pushCameraImage(len, data);
}

- (void)startLocation {
	[locationHelper startLocationUpdates];
}

- (void)stopLocation {
	[locationHelper stopLocationUpdates];
}

- (void)SetGpsDataIOS:(CLLocation *)newLocation {
	GPS::setGpsData((long long)newLocation.timestamp.timeIntervalSince1970,
					newLocation.horizontalAccuracy/5.0,
					newLocation.coordinate.latitude, newLocation.coordinate.longitude,
					newLocation.altitude,
					MAX(newLocation.speed * 3.6, 0.0), /* m/s to km/h */
					0 /* bearing */);
}

- (void)viewDidLoad {
	[super viewDidLoad];

	cameraHelper = [[CameraHelper alloc] init];
	[cameraHelper setDelegate:self];

	locationHelper = [[LocationHelper alloc] init];
	[locationHelper setDelegate:self];

	self.motionManager = [[CMMotionManager alloc] init];
}

extern float g_safeInsetLeft;
extern float g_safeInsetRight;
extern float g_safeInsetTop;
extern float g_safeInsetBottom;

- (void)viewSafeAreaInsetsDidChange {
	[super viewSafeAreaInsetsDidChange];

	// Converts points to pixels.
	CGFloat scale = UIScreen.mainScreen.scale;

	float xInsetSum = self.view.safeAreaInsets.left + self.view.safeAreaInsets.right;
	float yInsetSum = self.view.safeAreaInsets.top + self.view.safeAreaInsets.bottom;

	// Only use the larger set of insets. The other set, we'll handle through layouts.
	// Also, we'll treat the bottom inset differently: We'll add some space at the end of everything
	// that scrolls so that when you're not at the bottom, we use the full vertical area,
	// just looks better.
	if (xInsetSum > yInsetSum) {
		// Landscape mode insets are larger, use those.
		g_safeInsetLeft = self.view.safeAreaInsets.left * scale;
		g_safeInsetRight = self.view.safeAreaInsets.right * scale;
		g_safeInsetTop = 0.0f;
		g_safeInsetBottom = 0.0f;
	} else {
		// Portrait mode insets are larger, use those.
		g_safeInsetLeft = 0.0f;
		g_safeInsetRight = 0.0f;
		g_safeInsetTop = self.view.safeAreaInsets.top * scale;
		g_safeInsetBottom = self.view.safeAreaInsets.bottom * scale;
	}
}

- (void)shareText:(NSString *)text {
	NSArray *items = @[text];
	UIActivityViewController * viewController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil];
	dispatch_async(dispatch_get_main_queue(), ^{
		[self presentViewController:viewController animated:YES completion:nil];
	});
}

- (void)appSwitchModeChanged {
	[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}

// The below is inspired by https://stackoverflow.com/questions/7253477/how-to-display-the-iphone-ipad-keyboard-over-a-full-screen-opengl-es-app
// It's a bit limited but good enough.

- (void)deleteBackward {
	KeyInput input{};
	input.deviceId = DEVICE_ID_KEYBOARD;
	input.flags = KeyInputFlags::DOWN | KeyInputFlags::UP;
	input.keyCode = NKCODE_DEL;
	NativeKey(input);
	INFO_LOG(Log::System, "Backspace");
}

- (void)insertText:(NSString *)text {
	std::string str([text UTF8String]);
	INFO_LOG(Log::System, "Chars: %s", str.c_str());
	SendKeyboardChars(str);
}

- (BOOL)hasText {
	return true;
}

- (void)showKeyboard {
	dispatch_async(dispatch_get_main_queue(), ^{
		INFO_LOG(Log::System, "becomeFirstResponder");
		[self becomeFirstResponder];
	});
}

- (void)hideKeyboard {
	dispatch_async(dispatch_get_main_queue(), ^{
		INFO_LOG(Log::System, "resignFirstResponder");
		[self resignFirstResponder];
	});
}

- (BOOL)canBecomeFirstResponder {
	return YES;
}

- (void)buttonDown:(iCadeState)button
{
	g_iCadeTracker.ButtonDown(button);
}

- (void)buttonUp:(iCadeState)button
{
	g_iCadeTracker.ButtonUp(button);
}

// See PPSSPPUIApplication.mm for the other method
#if PPSSPP_PLATFORM(IOS_APP_STORE)

- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
	KeyboardPressesBegan(presses, event);
}

- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
	KeyboardPressesEnded(presses, event);
}

- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
	KeyboardPressesEnded(presses, event);
}

#endif
#pragma mark - Status Bar Control

// iOS calls this to determine whether to hide the status bar
- (BOOL)prefersStatusBarHidden {
	UIInterfaceOrientation orientation;

	if (@available(iOS 13.0, *)) {
		UIWindowScene *scene = self.view.window.windowScene;
		if (scene != nil) {
			orientation = scene.interfaceOrientation;
		} else {
			orientation = UIApplication.sharedApplication.statusBarOrientation;
		}
	} else {
		orientation = UIApplication.sharedApplication.statusBarOrientation;
	}

	BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation);

	bool userWantsStatusBar = true; // g_Config.bShowStatusBar;
	// return isLandscape || !userWantsStatusBar;
	return false;
}

// Optional: choose light/dark text for the status bar
- (UIStatusBarStyle)preferredStatusBarStyle {
	return UIStatusBarStyleLightContent;
}

// This should also be called when the user preference changes.
- (void)onOrientationChanged {
	[self setNeedsStatusBarAppearanceUpdate];
}

@end