Merge pull request #22037 from hrydgard/hide-status-bar-ios

Add setting to hide/show the status bar on iOS
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-08-07 07:24:25 +02:00
commit 5f5130025c
20 files changed
+84 -255

No files matched your search

-1
View File
@@ -976,7 +976,6 @@ elseif(IOS AND NOT LIBRETRO)
ios/iCade/iCadeReaderView.m
ios/main.mm
ios/CameraHelper.mm
ios/AudioEngine.mm
ios/LocationHelper.mm
ios/Controls.mm
Core/Util/DarwinFileSystemServices.mm
+1 -1
View File
@@ -2539,7 +2539,7 @@ static u32 sceKernelLoadModuleDNAS(const char *name, u32 flags)
}
// Pretty sure this is a badly brute-forced function name...
static SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr)
SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr)
{
if (flags != 0) {
WARN_LOG_REPORT(Log::Loader, "sceKernelLoadModuleBufferUsbWlan: unsupported flags: %08x", flags);
+8
View File
@@ -1165,6 +1165,14 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) {
switchMode->OnChoice.Add([](EventParams &e) {
System_Notify(SystemNotification::APP_SWITCH_MODE_CHANGED);
});
{
// Note: On iPhone, iOS hides the status bar in landscape no matter what this is set to.
DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation());
systemSettings->Add(new CheckBox(&config.bImmersiveMode, sy->T("Hide status bar")))->OnClick.Add([](EventParams &e) {
System_Notify(SystemNotification::IMMERSIVE_MODE_CHANGE);
});
}
#endif
#if PPSSPP_PLATFORM(ANDROID)
+1
View File
@@ -1424,6 +1424,7 @@ Default tab = Default tab
Dynarec/JIT (recommended) = Dynarec/JIT (recommended)
Final processed image = Final processed image
Hide navigation bar = Hide navigation bar
Hide status bar = Hide status bar
I/O timing method = I/O timing method
JIT using IR = JIT using IR
Loaded plugin: %1 = Loaded plugin: %1
-4
View File
@@ -4,15 +4,11 @@
#include <string_view>
@protocol PPSSPPViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) UIScreen *screen;
@property (nonatomic, strong) NSDictionary *launchOptions;
- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv;
- (void)processFilePath:(NSString *)path;
// Exports the game library to a caller app via a URL scheme callback.
-2
View File
@@ -248,8 +248,6 @@ static std::vector<std::string> GatherGameLibrary() {
default:
return UIInterfaceOrientationMaskAll;
}
return UIInterfaceOrientationMaskAll; // or at least include Portrait
}
@end
-13
View File
@@ -1,13 +0,0 @@
//
// AudioEngine.h
// PPSSPP
//
// Created by rock88 on 15/03/2013.
// Copyright (c) 2013 Homebrew. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface AudioEngine : NSObject
@end
-149
View File
@@ -1,149 +0,0 @@
//
// AudioEngine.mm
// PPSSPP
//
// Created by rock88 on 15/03/2013.
// Copyright (c) 2013 Homebrew. All rights reserved.
//
#import "AudioEngine.h"
#import <OpenAL/al.h>
#import <OpenAL/alc.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <string>
static volatile BOOL done = 0;
#define SAMPLE_SIZE 44100
static short stream[SAMPLE_SIZE];
void NativeMix(short *audio, int numSamples, int sampleRateHz, void *userdata);
@interface AudioEngine ()
@property (nonatomic,assign) ALCdevice *alcDevice;
@property (nonatomic,assign) ALCcontext *alContext;
@property (nonatomic,assign) ALuint buffer;
@property (nonatomic,assign) ALuint source;
@end
@implementation AudioEngine
@synthesize alcDevice,alContext,buffer,source;
- (id)init
{
self = [super init];
if (self)
{
[self audioInit];
[self audioLoop];
}
return self;
}
- (void)dealloc
{
[self audioShutdown];
[super dealloc];
}
- (void)checkALError
{
ALenum ErrCode;
std::string Err = "OpenAL error: ";
if ((ErrCode = alGetError()) != AL_NO_ERROR)
{
Err += (char *)alGetString(ErrCode);
printf("%s\n",Err.c_str());
}
}
- (void)audioInit
{
done = 0;
alcDevice = alcOpenDevice(NULL);
if (alcDevice)
{
NSLog(@"OpenAL device opened: %s",alcGetString(alcDevice, ALC_DEVICE_SPECIFIER));
}
else
{
NSLog(@"WARNING: could not open OpenAL device");
return;
}
alContext = alcCreateContext(alcDevice, NULL);
if (alContext)
{
alcMakeContextCurrent(alContext);
}
else
{
NSLog(@"ERROR: no OpenAL context");
return;
}
alGenSources(1, &source);
alGenBuffers(1, &buffer);
}
- (void)audioShutdown
{
done = 1;
alcMakeContextCurrent(NULL);
if (alContext)
{
alcDestroyContext(alContext);
alContext = NULL;
}
if (alcDevice)
{
alcCloseDevice(alcDevice);
alcDevice = NULL;
}
}
- (bool)playing
{
ALenum state;
alGetSourcei(source, AL_SOURCE_STATE, &state);
return (state == AL_PLAYING);
}
- (void)audioLoop
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
const int sampleRateHz = 44100;
while (!done) {
if (![self playing]) {
NativeMix(stream, SAMPLE_SIZE / 2, sampleRateHz, 0);
frames_ready = SAMPLE_SIZE / 2;
} else {
frames_ready = 0;
}
if (frames_ready > 0) {
const size_t bytes_ready = frames_ready * sizeof(short) * 2;
alSourcei(source, AL_BUFFER, 0);
alBufferData(buffer, AL_FORMAT_STEREO16, stream, bytes_ready, sampleRateHz);
alSourcei(source, AL_BUFFER, buffer);
alSourcePlay(source);
// TODO: Maybe this could get behind?
usleep((1000000 * frames_ready) / sampleRateHz);
} else {
usleep(100);
}
pthread_yield_np();
}
});
}
@end
+5
View File
@@ -53,6 +53,10 @@ NSString *getSelectedCamera() {
case AVAuthorizationStatusAuthorized: {
return 0;
}
default: {
NSLog(@"unknown camera authorization status %ld, treating as denied", (long)status);
return 1;
}
}
}
@@ -145,6 +149,7 @@ NSString *getSelectedCamera() {
CGContextDrawImage(outContext, outRect, inImage);
CGImageRelease(inImage);
CGImageRef outImage = CGBitmapContextCreateImage(outContext);
CGContextRelease(outContext);
CGColorSpaceRelease(colorSpace);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
View File
Whitespace-only changes.
+10 -5
View File
@@ -130,19 +130,24 @@
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
case SKPaymentTransactionStateRestored:
NSLog(transaction.transactionState == SKPaymentTransactionStatePurchased ? @"IAP Purchase" : @"IAP Restore");
NSLog(@"%@", transaction.transactionState == SKPaymentTransactionStatePurchased ? @"IAP Purchase" : @"IAP Restore");
// Perform the unlock (updaing the variable and switching the icon).
[self unlockGold];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
g_requestManager.PostSystemSuccess(_pendingRequestID, "", 0);
_pendingRequestID = 0;
// The queue also delivers transactions we didn't ask for, like unfinished ones at startup.
if (_pendingRequestID != 0) {
g_requestManager.PostSystemSuccess(_pendingRequestID, "", 0);
_pendingRequestID = 0;
}
break;
case SKPaymentTransactionStateFailed:
NSLog(@"[IAPManager] Purchase failed (requestID: %d): %@", _pendingRequestID, transaction.error.localizedDescription);
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
// Optionally post failure callback here
g_requestManager.PostSystemFailure(_pendingRequestID);
_pendingRequestID = 0;
if (_pendingRequestID != 0) {
g_requestManager.PostSystemFailure(_pendingRequestID);
_pendingRequestID = 0;
}
break;
default:
break;
+5 -2
View File
@@ -7,8 +7,11 @@
-(id) init {
NSLog(@"LocationHelper::init");
locationManager = [[CLLocationManager alloc] init];
[locationManager setDelegate:self];
self = [super init];
if (self) {
locationManager = [[CLLocationManager alloc] init];
[locationManager setDelegate:self];
}
return self;
}
+1 -1
View File
@@ -81,7 +81,7 @@
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
} else if (GSEVENT_TYPE_KEYDOWN) {
} else if (eventType == GSEVENT_TYPE_KEYDOWN) {
struct KeyInput key;
key.flags = KeyInputFlags::DOWN;
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
+2 -24
View File
@@ -5,7 +5,6 @@
// Modified by xSacha
// Reworked by hrydgard
#import "AppDelegate.h"
#import "ViewController.h"
#import "iOSCoreAudio.h"
@@ -56,7 +55,6 @@ PPSSPPBaseViewController *sharedViewController;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) GLKView *glView;
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, assign) NSTimeInterval lastTimestamp;
@property (nonatomic, strong) EAGLContext* context;
@@ -134,10 +132,6 @@ PPSSPPBaseViewController *sharedViewController;
}
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
self.lastTimestamp = 0;
UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen];
self.view.frame = [screen bounds];
self.view.multipleTouchEnabled = YES;
graphicsContext = new OpenGLGraphicsContext();
@@ -173,14 +167,12 @@ PPSSPPBaseViewController *sharedViewController;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Resume display link unless explicitly paused
INFO_LOG(Log::G3D, "viewWillAppear - resuming display link");
INFO_LOG(Log::G3D, "viewWillAppear");
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// stop rendering while not visible
INFO_LOG(Log::G3D, "viewWillDisappear - pausing display link");
INFO_LOG(Log::G3D, "viewWillDisappear");
}
- (void)dealloc {
@@ -205,16 +197,6 @@ PPSSPPBaseViewController *sharedViewController;
}
- (void)displayLinkFired:(CADisplayLink *)dl {
// compute delta time
NSTimeInterval timestamp = dl.timestamp;
NSTimeInterval delta = 0;
if (self.lastTimestamp > 0) {
delta = timestamp - self.lastTimestamp;
} else {
delta = dl.duration; // fallback
}
self.lastTimestamp = timestamp;
// Ensure context is current before drawing
[EAGLContext setCurrentContext:self.glContext];
@@ -286,10 +268,6 @@ PPSSPPBaseViewController *sharedViewController;
[(GLKView*)self.glView bindDrawable];
}
- (UIView *)getView {
return [self view];
}
// Can't consolidate this yet.
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
+1 -1
View File
@@ -20,12 +20,12 @@
- (void)shareText:(NSString *)text;
- (void)shutdown;
- (void)bindDefaultFBO;
- (UIView *)getView;
- (void)startLocation;
- (void)stopLocation;
- (void)startVideo:(int)width height:(int)height;
- (void)stopVideo;
- (void)appSwitchModeChanged;
- (void)immersiveModeChanged;
- (void)setupController:(GCController *)controller;
// Forwarded from the AppDelegate
+32 -20
View File
@@ -76,14 +76,17 @@ static int GetPickerRequestId(id picker) {
selector:@selector(onOrientationChanged)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
self.accelerometerQueue = [[NSOperationQueue alloc] init];
self.accelerometerQueue.name = @"AccelerometerQueue";
self.accelerometerQueue.maxConcurrentOperationCount = 1;
self.accelerometerQueue = [[NSOperationQueue alloc] init];
self.accelerometerQueue.name = @"AccelerometerQueue";
self.accelerometerQueue.maxConcurrentOperationCount = 1;
}
return self;
}
// Only means anything for the GL backend, which overrides this.
- (void)bindDefaultFBO {}
- (void)shutdown {
self.gameController = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
@@ -481,26 +484,24 @@ extern float g_safeInsetBottom;
#endif
#pragma mark - Status Bar Control
// iOS calls this to determine whether to hide the status bar
- (BOOL)prefersStatusBarHidden {
UIInterfaceOrientation orientation;
// The immersive mode setting is per-orientation, so we need to know which way we're facing.
// Can't just use g_display for this, since it lags behind during rotation.
// Note: Using viewIfLoaded, since this can get called before the view exists, and we don't want to force it into existence.
- (DeviceOrientation)currentDeviceOrientation {
if (@available(iOS 13.0, *)) {
UIWindowScene *scene = self.view.window.windowScene;
UIWindowScene *scene = self.viewIfLoaded.window.windowScene;
if (scene != nil) {
orientation = scene.interfaceOrientation;
} else {
orientation = UIApplication.sharedApplication.statusBarOrientation;
return UIInterfaceOrientationIsPortrait(scene.interfaceOrientation) ? DeviceOrientation::Portrait : DeviceOrientation::Landscape;
}
} else {
orientation = UIApplication.sharedApplication.statusBarOrientation;
}
CGSize size = self.viewIfLoaded.bounds.size;
return size.height > size.width ? DeviceOrientation::Portrait : DeviceOrientation::Landscape;
}
BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation);
bool userWantsStatusBar = true; // g_Config.bShowStatusBar;
// return isLandscape || !userWantsStatusBar;
return false;
// iOS calls this to determine whether to hide the status bar.
// Note that on iPhone, iOS hides it in landscape regardless of what we return here.
- (BOOL)prefersStatusBarHidden {
return g_Config.GetDisplayLayoutConfig([self currentDeviceOrientation]).bImmersiveMode ? YES : NO;
}
// Optional: choose light/dark text for the status bar
@@ -508,11 +509,22 @@ extern float g_safeInsetBottom;
return UIStatusBarStyleLightContent;
}
// This should also be called when the user preference changes.
- (void)onOrientationChanged {
[self setNeedsStatusBarAppearanceUpdate];
}
// Called from the C++ side when the user toggles the immersive mode setting.
- (void)immersiveModeChanged {
[self setNeedsStatusBarAppearanceUpdate];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
// The immersive mode setting is per-orientation, so the status bar may need to change along with the rotation.
[self setNeedsStatusBarAppearanceUpdate];
}
- (void)updateResolutionWithView:(UIView *)view {
// 1. Get the scale from the window scene (safest for Metal)
CGFloat scale = 1.0;
+3 -12
View File
@@ -1,4 +1,3 @@
#import "AppDelegate.h"
#import "ViewControllerMetal.h"
#import "iOSCoreAudio.h"
@@ -193,8 +192,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met
INFO_LOG(Log::System, "Metal viewDidLoad");
UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen];
self.view.frame = [screen bounds];
self.view.multipleTouchEnabled = YES;
// self.view.insetsLayoutMarginsFromSafeArea = NO;
// self.view.clipsToBounds = YES;
@@ -206,7 +203,9 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met
g_Config.iGPUBackend = (int)GPUBackend::OPENGL;
SetGPUBackend(GPUBackend::OPENGL);
delete graphicsContext;
// TODO: What to do here?
graphicsContext = nullptr; // The render loop and shutdown check for this.
// TODO: What to do here? We've switched the config over to GL, but we're still the Metal view controller,
// so we won't render anything until the app gets restarted.
}
[self updateResolutionWithView:self.view];
@@ -218,10 +217,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met
INFO_LOG(Log::G3D, "Detected size: %dx%d", g_display.pixel_xres, g_display.pixel_yres);
}
- (UIView *)getView {
return [self view];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
INFO_LOG(Log::G3D, "viewWillAppear");
@@ -245,10 +240,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met
INFO_LOG(Log::G3D, "viewDidDisappear");
}
- (void)bindDefaultFBO {
// Do nothing
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
+2 -4
View File
@@ -21,7 +21,5 @@
void iOSCoreAudioInit();
void iOSCoreAudioShutdown();
// Ignore mute switch when connected to external display.
// Also, obey other settings.
void iOSCoreAudioUpdateSession();
void iOSCoreAudioSetDisplayConnected(bool connected);
// Applies the audio settings (mix with others, respect silent mode) to the session.
void iOSCoreAudioUpdateSession();
-15
View File
@@ -29,18 +29,9 @@
#define SAMPLE_RATE 44100
static AudioComponentInstance audioInstance = nil;
static bool g_displayConnected = false;
void iOSCoreAudioUpdateSession() {
NSError *error = nil;
if (g_displayConnected) {
INFO_LOG(Log::Audio, "Display connected, setting Playback mode");
// Special handling when a display is connected. Always exclusive.
// Let's revisit this later.
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
return;
}
INFO_LOG(Log::Audio, "RespectSilentMode: %d MixWithOthers: %d", g_Config.bAudioRespectSilentMode, g_Config.bAudioMixWithOthers);
// Hacky hack to force iOS to re-evaluate.
@@ -48,7 +39,6 @@ void iOSCoreAudioUpdateSession() {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAudioProcessing error:&error];
// Here, we apply the settings.
const bool mixWithOthers = g_Config.bAudioMixWithOthers;
if (g_Config.bAudioMixWithOthers) {
if (g_Config.bAudioRespectSilentMode) {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&error];
@@ -69,11 +59,6 @@ void iOSCoreAudioUpdateSession() {
}
}
void iOSCoreAudioSetDisplayConnected(bool connected) {
g_displayConnected = connected;
iOSCoreAudioUpdateSession();
}
void NativeMix(short *audio, int numSamples, int sampleRateHz, void *userdata);
OSStatus iOSCoreAudioCallback(void *inRefCon,
+13 -1
View File
@@ -432,6 +432,13 @@ void System_Notify(SystemNotification notification) {
}
});
break;
case SystemNotification::IMMERSIVE_MODE_CHANGE:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
[sharedViewController immersiveModeChanged];
}
});
break;
case SystemNotification::UI_STATE_CHANGED:
dispatch_async(dispatch_get_main_queue(), ^{
if (sharedViewController) {
@@ -452,6 +459,7 @@ void System_Notify(SystemNotification notification) {
[UIViewController attemptRotationToDeviceOrientation];
}
});
break;
default:
break;
}
@@ -596,7 +604,11 @@ void System_AskForPermission(SystemPermission permission) {}
void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {
std::string strUrl(url);
NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithCString:strUrl.c_str() encoding:NSStringEncodingConversionAllowLossy]];
NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithUTF8String:strUrl.c_str()]];
if (!nsUrl) {
ERROR_LOG(Log::System, "Failed to parse URL: %s", strUrl.c_str());
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] openURL:nsUrl options:@{} completionHandler:nil];
});