mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-05-29 00:21:34 +08:00
550 lines
17 KiB
Plaintext
550 lines
17 KiB
Plaintext
#import "ios/CameraHelper.h"
|
|
#import "ios/ViewControllerCommon.h"
|
|
#import "ios/Controls.h"
|
|
#import "ios/IAPManager.h"
|
|
#import <Photos/Photos.h>
|
|
#import <objc/runtime.h>
|
|
#include "Common/System/Request.h"
|
|
#include "Common/Input/InputState.h"
|
|
#include "Common/System/NativeApp.h"
|
|
#include "Common/System/Display.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 () {
|
|
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;
|
|
}
|
|
|
|
// Strange idiom for generating unique IDs (within the process, at least).
|
|
static const void *kPickerFilenameKey = &kPickerFilenameKey;
|
|
static const void *kPickerRequestIdKey = &kPickerRequestIdKey;
|
|
|
|
static void SetPickerContext(id picker, NSString *filename, int requestId) {
|
|
objc_setAssociatedObject(picker, kPickerFilenameKey, filename, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
|
objc_setAssociatedObject(picker, kPickerRequestIdKey, @(requestId), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
static NSString *GetPickerFilename(id picker) {
|
|
return (NSString *)objc_getAssociatedObject(picker, kPickerFilenameKey);
|
|
}
|
|
|
|
static int GetPickerRequestId(id picker) {
|
|
NSNumber *requestIdNumber = (NSNumber *)objc_getAssociatedObject(picker, kPickerRequestIdKey);
|
|
return requestIdNumber ? requestIdNumber.intValue : -1;
|
|
}
|
|
|
|
- (void)savePickedImage:(UIImage *)image toFilename:(NSString *)targetFilename requestId:(int)requestId {
|
|
NSData *jpegData = UIImageJPEGRepresentation(image, 0.9);
|
|
if (jpegData) {
|
|
[jpegData writeToFile:targetFilename atomically:YES];
|
|
NSLog(@"Saved JPEG image to %@", targetFilename);
|
|
g_requestManager.PostSystemSuccess(requestId, "", 1);
|
|
} else {
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
}
|
|
}
|
|
|
|
- (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 {
|
|
NSLog(@"Picking photo to save to %@ (id: %d)", saveFilename, requestId);
|
|
NSString *targetFilename = [saveFilename copy];
|
|
|
|
if (@available(iOS 14.0, *)) {
|
|
PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary];
|
|
config.selectionLimit = 1;
|
|
config.filter = [PHPickerFilter imagesFilter];
|
|
|
|
PHPickerViewController *picker = [[PHPickerViewController alloc] initWithConfiguration:config];
|
|
picker.delegate = self;
|
|
SetPickerContext(picker, targetFilename, requestId);
|
|
[self presentViewController:picker animated:YES completion:nil];
|
|
return;
|
|
}
|
|
|
|
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
|
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
|
// Images only keeps the picker from doing extra video-related setup.
|
|
picker.mediaTypes = @[@"public.image"];
|
|
picker.delegate = self;
|
|
SetPickerContext(picker, targetFilename, requestId);
|
|
[self presentViewController:picker animated:YES completion:nil];
|
|
}
|
|
|
|
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14.0)) {
|
|
NSString *targetFilename = GetPickerFilename(picker);
|
|
int requestId = GetPickerRequestId(picker);
|
|
|
|
[picker dismissViewControllerAnimated:YES completion:nil];
|
|
|
|
if (results.count == 0) {
|
|
NSLog(@"User cancelled photo picker");
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
[self hideKeyboard];
|
|
return;
|
|
}
|
|
|
|
NSItemProvider *itemProvider = results.firstObject.itemProvider;
|
|
if (![itemProvider canLoadObjectOfClass:[UIImage class]]) {
|
|
NSLog(@"Photo picker result does not provide UIImage");
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
[self hideKeyboard];
|
|
return;
|
|
}
|
|
|
|
[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^(UIImage *image, NSError *error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error || !image) {
|
|
NSLog(@"Failed loading picked image: %@", error);
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
} else {
|
|
[self savePickedImage:image toFilename:targetFilename requestId:requestId];
|
|
}
|
|
[self hideKeyboard];
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)imagePickerController:(UIImagePickerController *)picker
|
|
didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info {
|
|
NSString *targetFilename = GetPickerFilename(picker);
|
|
int requestId = GetPickerRequestId(picker);
|
|
|
|
UIImage *image = info[UIImagePickerControllerOriginalImage];
|
|
[self savePickedImage:image toFilename:targetFilename requestId:requestId];
|
|
|
|
[picker dismissViewControllerAnimated:YES completion:nil];
|
|
|
|
[self hideKeyboard];
|
|
}
|
|
|
|
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
|
NSLog(@"User cancelled image picker");
|
|
int requestId = GetPickerRequestId(picker);
|
|
|
|
[picker dismissViewControllerAnimated:YES completion:nil];
|
|
|
|
// You can also call your custom callback or use the requestId here
|
|
g_requestManager.PostSystemFailure(requestId);
|
|
[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];
|
|
}
|
|
|
|
- (void)updateResolutionWithView:(UIView *)view {
|
|
// 1. Get the scale from the window scene (safest for Metal)
|
|
CGFloat scale = 1.0;
|
|
if (view.window.windowScene) {
|
|
scale = view.window.windowScene.screen.nativeScale;
|
|
} else {
|
|
scale = [UITraitCollection currentTraitCollection].displayScale;
|
|
}
|
|
|
|
// 2. Get the ACTUAL bounds of the view (already corrected for orientation)
|
|
CGSize size = view.bounds.size;
|
|
|
|
// Your existing logic
|
|
float dpi = (IS_IPAD() ? 200.0f : 150.0f) * scale;
|
|
const float dpi_scale_x = 240.0f / dpi;
|
|
const float dpi_scale_y = 240.0f / dpi;
|
|
|
|
g_display.Recalculate(size.width * scale, size.height * scale,
|
|
dpi_scale_x, dpi_scale_y,
|
|
UIScaleFactorToMultiplier(g_Config.iUIScaleFactor));
|
|
|
|
[view setContentScaleFactor:scale];
|
|
|
|
// PSP native resize
|
|
PSP_CoreParameter().pixelWidth = g_display.pixel_xres;
|
|
PSP_CoreParameter().pixelHeight = g_display.pixel_yres;
|
|
|
|
NativeResized();
|
|
|
|
NSLog(@"Updated display resolution: (%d, %d) @%.1fx",
|
|
g_display.pixel_xres, g_display.pixel_yres, (float)scale);
|
|
}
|
|
|
|
@end
|