From 1f5f2f5ebb54144c72b1944cc2bfbb178817c022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 24 Sep 2026 13:21:36 -0600 Subject: [PATCH 1/3] macOS: Target 11.0, the oldest current libc++ supports With a deployment target of 10.13 (10.14 for arm64 libretro), every file that includes a libc++ header warned "The selected platform is no longer supported by libc++." Apple Silicon Macs start at 11 anyway. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ff03e675da..fc87b0ce15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,11 +510,8 @@ if(NOT MSVC) if(IOS) set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0") elseif(APPLE AND NOT CMAKE_CROSSCOMPILING) - if(LIBRETRO AND ARM64) - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14") - else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13") - endif() + # The oldest libc++ in current Xcode supports ("The selected platform is no longer supported" below). + set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") add_compile_options("$<$:-stdlib=libc++>" -U__STRICT_ANSI__) set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++") elseif(NOT ANDROID) From 48b380a27e68dc1121e7494b27e91110c6c5df75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 24 Sep 2026 13:29:03 -0600 Subject: [PATCH 2/3] macOS: Fix menu bar items - Recent files always booted the first one (no item had its tag set), and the list was never refreshed. Rebuild it when opened, with the path on each item. - The Graphics and Debug menus were never updated when opened: they were matched by title, looked up under different translation keys than they were created with. Compare the menus themselves. - The Fullscreen item toggled bFullScreen twice, doing nothing. - The status counter items checked their tag instead of the flag. - Remove Restart Graphics from the Debug menu (it shared tag 12 with Show Debug Statistics). - Drop @available checks for versions below the deployment target. Co-Authored-By: Claude Opus 5.5 (1M context) --- SDL/CocoaBarItems.mm | 87 +++++++++++++++++++----------------------- SDL/MacCameraHelper.mm | 38 +++++++++--------- 2 files changed, 57 insertions(+), 68 deletions(-) diff --git a/SDL/CocoaBarItems.mm b/SDL/CocoaBarItems.mm index 0306d2b915..8b6479b4d6 100644 --- a/SDL/CocoaBarItems.mm +++ b/SDL/CocoaBarItems.mm @@ -43,6 +43,10 @@ variableName.state = [self controlStateForBool: ConfigurationValueName]; +(instancetype)sharedInstance; -(void)setupAppBarItems; @property (assign) NSMenu *fileMenu; +@property (assign) NSMenu *emulationMenu; +@property (assign) NSMenu *debugMenu; +@property (assign) NSMenu *graphicsMenu; +@property (assign) NSMenu *recentsMenu; @end void initializeOSXExtras() { @@ -74,11 +78,6 @@ void OSXOpenURL(const char *url) { return @(T_cstr(cat, key)); } --(NSString *)localizedMenuString: (const char *)key { - std::string processed = UnescapeMenuString(T_cstr(I18NCat::DESKTOPUI, key), nullptr); - return @(processed.c_str()); -} - -(void)setupAppBarItems { NSMenuItem *fileMenuItem = [[NSMenuItem alloc] init]; @@ -88,14 +87,17 @@ void OSXOpenURL(const char *url) { NSMenuItem *emulationMenuItem = [[NSMenuItem alloc] init]; emulationMenuItem.submenu = [self makeEmulationMenu]; emulationMenuItem.submenu.delegate = self; + self.emulationMenu = emulationMenuItem.submenu; NSMenuItem *debugMenuItem = [[NSMenuItem alloc] init]; debugMenuItem.submenu = [self makeDebugMenu]; debugMenuItem.submenu.delegate = self; + self.debugMenu = debugMenuItem.submenu; NSMenuItem *graphicsMenuItem = [[NSMenuItem alloc] init]; graphicsMenuItem.submenu = [self makeGraphicsMenu]; graphicsMenuItem.submenu.delegate = self; + self.graphicsMenu = graphicsMenuItem.submenu; NSMenuItem *helpMenuItem = [[NSMenuItem alloc] init]; helpMenuItem.submenu = [self makeHelpMenu]; @@ -138,28 +140,23 @@ void OSXOpenURL(const char *url) { window.styleMask &= ~NSWindowStyleMaskResizable; [[window standardWindowButton:NSWindowMiniaturizeButton] setEnabled:NO]; - if (@available(macOS 10.15, *)) { - window.backgroundColor = [NSColor colorWithName:nil dynamicProvider:^NSColor * _Nonnull(NSAppearance * _Nonnull appearance) { - // check for dark/light mode (dark mode is OS X 10.14+ only) - /* no I can't use switch statements here it's an NSString pointer */ - if (appearance.name == NSAppearanceNameDarkAqua || - appearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark || - appearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua || - appearance.name == NSAppearanceNameVibrantDark) - return [NSColor colorWithRed:0.19 green:0.19 blue:0.19 alpha:1]; - - // macOS pre 10.14 is always light mode - return [NSColor whiteColor]; - }]; - } else { - window.backgroundColor = [NSColor whiteColor]; - } + window.backgroundColor = [NSColor colorWithName:nil dynamicProvider:^NSColor * _Nonnull(NSAppearance * _Nonnull appearance) { + /* no I can't use switch statements here it's an NSString pointer */ + if (appearance.name == NSAppearanceNameDarkAqua || + appearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark || + appearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua || + appearance.name == NSAppearanceNameVibrantDark) + return [NSColor colorWithRed:0.19 green:0.19 blue:0.19 alpha:1]; + return [NSColor whiteColor]; + }]; [[[NSWindowController alloc] initWithWindow:window] showWindow:nil]; } - (void)menuNeedsUpdate:(NSMenu *)menu { - if ([menu.title isEqualToString: [self localizedMenuString:"Emulation"]]) { + if (menu == self.recentsMenu) { + [self rebuildRecentsMenu]; + } else if (menu == self.emulationMenu) { menu.autoenablesItems = NO; // Enable/disable the various items. for (NSMenuItem *item in menu.itemArray) { @@ -178,7 +175,7 @@ void OSXOpenURL(const char *url) { break; } } - } else if ([menu.title isEqualToString: [self localizedMenuString:"Graphics"]]) { + } else if (menu == self.graphicsMenu) { for (NSMenuItem *item in menu.itemArray) { switch (item.tag) { case 1: @@ -206,7 +203,7 @@ void OSXOpenURL(const char *url) { break; } } - } else if ([menu.title isEqualToString: [self localizedMenuString:"Debug"]]) { + } else if (menu == self.debugMenu) { menu.autoenablesItems = NO; GlobalUIState state = GetUIState(); for (NSMenuItem *item in menu.itemArray) { @@ -413,10 +410,6 @@ void OSXOpenURL(const char *url) { copyBaseAddr.target = self; copyBaseAddr.tag = 11; - NSMenuItem *restartGraphicsAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Restart Graphics") action:@selector(restartGraphics) keyEquivalent:@""]; - restartGraphicsAction.target = self; - restartGraphicsAction.tag = 12; - MENU_ITEM(showDebugStatsAction, DESKTOPUI_LOCALIZED("Show Debug Statistics"), @selector(toggleShowDebugStats:), ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS), 12) [parent addItem:loadSymbolMapAction]; @@ -433,7 +426,6 @@ void OSXOpenURL(const char *url) { [parent addItem:takeScreenshotAction]; [parent addItem:saveFrameDumpAction]; [parent addItem:showDebugStatsAction]; - [parent addItem:restartGraphicsAction]; [parent addItem:[NSMenuItem separatorItem]]; [parent addItem:copyBaseAddr]; @@ -475,10 +467,6 @@ void OSXOpenURL(const char *url) { [NSPasteboard.generalPasteboard setString:stringToCopy forType:NSPasteboardTypeString]; } --(void)restartGraphics { - System_PostUIMessage(UIMessage::RESTART_GRAPHICS); -} - -(NSURL *)presentOpenPanelWithAllowedFileTypes: (NSArray *)allowedFileTypes { NSOpenPanel *openPanel = [[NSOpenPanel alloc] init]; openPanel.allowedFileTypes = allowedFileTypes; @@ -556,7 +544,7 @@ TOGGLE_METHOD_INVERSE(BreakOnLoad, g_Config.bAutoRun) TOGGLE_METHOD(IgnoreIllegalRWs, g_Config.bIgnoreBadMemAccess) TOGGLE_METHOD(AutoFrameSkip, g_Config.bAutoFrameSkip, g_Config.UpdateAfterSettingAutoFrameSkip()) TOGGLE_METHOD(SoftwareRendering, g_Config.bSoftwareRendering) -TOGGLE_METHOD(FullScreen, g_Config.bFullScreen, g_Config.bFullScreen = !g_Config.bFullScreen, System_ApplyFullscreenState()); +TOGGLE_METHOD(FullScreen, g_Config.bFullScreen, System_ApplyFullscreenState()); // TOGGLE_METHOD(VSync, g_Config.bVSync) #undef TOGGLE_METHOD @@ -572,7 +560,7 @@ TOGGLE_METHOD(FullScreen, g_Config.bFullScreen, g_Config.bFullScreen = !g_Config -(void)setToggleShowCounterItem: (NSMenuItem *)item { [self addOrRemoveInteger:(int)(item.tag - 100) to:&g_Config.iShowStatusFlags]; - item.state = [self controlStateForBool:g_Config.iShowStatusFlags & item.tag]; + item.state = [self controlStateForBool:g_Config.iShowStatusFlags & (int)(item.tag - 100)]; } -(void)addOrRemoveInteger: (int)integer to: (int *)r { @@ -630,25 +618,30 @@ TOGGLE_METHOD(FullScreen, g_Config.bFullScreen, g_Config.bFullScreen = !g_Config std::shared_ptr mainmenuLocalization = GetI18NCategory(I18NCat::MAINMENU); #define MAINMENU_LOCALIZED(key) @(mainmenuLocalization->T_cstr(key)) - std::vector recentFiles = g_recentFiles.GetRecentFiles(); NSMenuItem *openRecent = [[NSMenuItem alloc] initWithTitle:MAINMENU_LOCALIZED("Recent") action:nil keyEquivalent:@""]; NSMenu *recentsMenu = [[NSMenu alloc] init]; - if (recentFiles.empty()) - openRecent.enabled = NO; - - for (const auto &file : recentFiles) { - std::string filename = Path(file).GetFilename(); - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@(filename.c_str()) action:@selector(openRecentItem:) keyEquivalent:@""]; - item.target = self; - [recentsMenu addItem:item]; - } - + recentsMenu.delegate = self; + self.recentsMenu = recentsMenu; + [self rebuildRecentsMenu]; + openRecent.submenu = recentsMenu; [self.fileMenu addItem:openRecent]; } +-(void)rebuildRecentsMenu { + [self.recentsMenu removeAllItems]; + for (const auto &file : g_recentFiles.GetRecentFiles()) { + std::string filename = Path(file).GetFilename(); + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@(filename.c_str()) action:@selector(openRecentItem:) keyEquivalent:@""]; + item.target = self; + item.representedObject = @(file.c_str()); + [self.recentsMenu addItem:item]; + } +} + -(void)openRecentItem: (NSMenuItem *)item { - System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, g_recentFiles.GetRecentFiles()[item.tag]); + NSString *path = item.representedObject; + System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, path.UTF8String); } -(void)openSystemFileBrowser { diff --git a/SDL/MacCameraHelper.mm b/SDL/MacCameraHelper.mm index d2a7e89df9..fa4e6a0872 100644 --- a/SDL/MacCameraHelper.mm +++ b/SDL/MacCameraHelper.mm @@ -57,29 +57,25 @@ void RunOnMainQueue(dispatch_block_t block) { } - (BOOL)ensurePermission { - if (@available(macOS 10.14, *)) { - AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; - if (status == AVAuthorizationStatusAuthorized) { - return YES; - } - - if (status == AVAuthorizationStatusNotDetermined) { - dispatch_semaphore_t sema = dispatch_semaphore_create(0); - [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { - (void)granted; - dispatch_semaphore_signal(sema); - }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); - status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; - } - - if (status != AVAuthorizationStatusAuthorized) { - ERROR_LOG(Log::HLE, "Camera permission denied on macOS"); - return NO; - } + AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + if (status == AVAuthorizationStatusAuthorized) { return YES; } - // macOS < 10.14 does not require runtime permission checks. + + if (status == AVAuthorizationStatusNotDetermined) { + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { + (void)granted; + dispatch_semaphore_signal(sema); + }]; + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + } + + if (status != AVAuthorizationStatusAuthorized) { + ERROR_LOG(Log::HLE, "Camera permission denied on macOS"); + return NO; + } return YES; } From ecf9efcf84bb8f6f2c16cfb94bc1586d9d89fd95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 24 Sep 2026 13:54:13 -0600 Subject: [PATCH 3/3] macOS: Add menu items from the Windows menu Stop, Switch UMD, savestate slots, Load/Save State (Cmd+L/Cmd+S) and state files, shortcuts to the settings screens (plus Settings... with Cmd+, in the app menu), Rendering Resolution, Texture Filtering, Enable Sound, Enable Cheats and the website links. Menu items now carry their action and how to tell whether they're checked or enabled as blocks, checked each time a menu is shown, instead of tags matched up in menuNeedsUpdate. The Graphics menu becomes Game Settings, as on Windows. Co-Authored-By: Claude Opus 5.5 (1M context) --- SDL/CocoaBarItems.mm | 914 +++++++++++++++++++------------------------ 1 file changed, 407 insertions(+), 507 deletions(-) diff --git a/SDL/CocoaBarItems.mm b/SDL/CocoaBarItems.mm index 8b6479b4d6..b886626e28 100644 --- a/SDL/CocoaBarItems.mm +++ b/SDL/CocoaBarItems.mm @@ -10,19 +10,27 @@ #include "Core/Util/DarwinFileSystemServices.h" #include "UI/PSPNSApplicationDelegate.h" +#include "UI/PauseScreen.h" #include "Core/Debugger/SymbolMap.h" #include "Core/MemMap.h" #include "Core/System.h" #include "Core/Core.h" +#include "Core/SaveState.h" +#include "Core/RetroAchievements.h" +#include "Core/ELF/ParamSFO.h" +#include "Core/HLE/sceNet.h" +#include "Core/HLE/sceUmd.h" #include "GPU/GPUCommon.h" #include "Common/File/Path.h" #include "Common/System/System.h" #include "Common/System/Request.h" #include "Common/System/NativeApp.h" +#include "Common/System/OSD.h" #include "Common/Data/Text/I18n.h" #include "Common/StringUtils.h" #include "Core/Config.h" +#include "Core/ConfigValues.h" #include "Core/Util/RecentFiles.h" #ifdef __cplusplus @@ -31,22 +39,27 @@ extern "C" { extern bool g_TakeScreenshot; -#define MENU_ITEM(variableName, localizedTitleName, SEL, ConfigurationValueName, Tag) \ -NSMenuItem *variableName = [[NSMenuItem alloc] initWithTitle:localizedTitleName action:SEL keyEquivalent:@""]; \ -variableName.target = self; \ -variableName.tag = Tag; \ -variableName.state = [self controlStateForBool: ConfigurationValueName]; +typedef BOOL (^MenuCondition)(void); + +// A menu item that carries its own action, and optionally how to tell whether it's checked, +// enabled or what it's called right now. These are checked each time the menu is shown. +@interface PPSSPPMenuItem : NSMenuItem +@property (copy) dispatch_block_t onSelect; +@property (copy) MenuCondition checkedWhen; +@property (copy) MenuCondition enabledWhen; +@property (copy) NSString *(^currentTitle)(void); +@end + +@implementation PPSSPPMenuItem +@end // NSMenuItem requires the use of an objective-c selector (aka the devil's greatest trick) // So we have to make this class -@interface BarItemsManager : NSObject +@interface BarItemsManager : NSObject +(instancetype)sharedInstance; -(void)setupAppBarItems; -@property (assign) NSMenu *fileMenu; -@property (assign) NSMenu *emulationMenu; -@property (assign) NSMenu *debugMenu; -@property (assign) NSMenu *graphicsMenu; -@property (assign) NSMenu *recentsMenu; +@property (strong) NSMenu *recentsMenu; +@property (strong) NSMenu *saveStateSlotMenu; @end void initializeOSXExtras() { @@ -64,6 +77,27 @@ void OSXOpenURL(const char *url) { [NSWorkspace.sharedWorkspace openURL:nsURL]; } +static NSString *MenuString(I18NCat cat, const char *key) { + // Falls back to the key itself: not every menu string is in the language files. + return @(UnescapeMenuString(T_cstr(cat, key, key), nullptr).c_str()); +} + +static NSString *DesktopUI(const char *key) { + return MenuString(I18NCat::DESKTOPUI, key); +} + +static BOOL InGame() { + return GetUIState() == UISTATE_INGAME; +} + +static void SaveStateActionFinished(SaveState::Status status, std::string_view message, std::string_view metadata) { + ShowMessageAfterSaveStateAction(status, message, metadata); +} + +static bool CanUseSaveStates(bool isSaveAction) { + return !Achievements::WarnUserIfHardcoreModeActive(isSaveAction) && !NetworkWarnUserIfOnlineAndCantSavestate(); +} + @implementation BarItemsManager + (instancetype)sharedInstance { static BarItemsManager *stub; @@ -74,59 +108,120 @@ void OSXOpenURL(const char *url) { return stub; } --(NSString *)localizedString: (const char *)key category: (I18NCat)cat { - return @(T_cstr(cat, key)); +// Menu construction helpers. + +-(NSMenu *)newMenu: (NSString *)title { + NSMenu *menu = [[NSMenu alloc] initWithTitle:title]; + menu.delegate = self; + return menu; +} + +-(NSMenu *)addSubmenu: (NSString *)title to: (NSMenu *)menu { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""]; + item.submenu = [self newMenu:title]; + [menu addItem:item]; + return item.submenu; +} + +-(PPSSPPMenuItem *)addItem: (NSString *)title to: (NSMenu *)menu action: (dispatch_block_t)action { + PPSSPPMenuItem *item = [[PPSSPPMenuItem alloc] initWithTitle:title action:@selector(runItem:) keyEquivalent:@""]; + item.target = self; + item.onSelect = action; + [menu addItem:item]; + return item; +} + +-(PPSSPPMenuItem *)addToggle: (NSString *)title to: (NSMenu *)menu value: (bool *)value then: (dispatch_block_t)after { + PPSSPPMenuItem *item = [self addItem:title to:menu action:^{ + *value = !*value; + if (after) { + after(); + } + }]; + item.checkedWhen = ^BOOL { return *value; }; + return item; +} + +-(PPSSPPMenuItem *)addChoice: (NSString *)title to: (NSMenu *)menu value: (int *)value is: (int)choice then: (dispatch_block_t)after { + PPSSPPMenuItem *item = [self addItem:title to:menu action:^{ + *value = choice; + if (after) { + after(); + } + }]; + item.checkedWhen = ^BOOL { return *value == choice; }; + return item; +} + +-(void)addSeparatorTo: (NSMenu *)menu { + [menu addItem:[NSMenuItem separatorItem]]; +} + +-(void)runItem: (PPSSPPMenuItem *)item { + if (item.onSelect) { + item.onSelect(); + } +} + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if (![menuItem isKindOfClass:[PPSSPPMenuItem class]]) { + return YES; + } + PPSSPPMenuItem *item = (PPSSPPMenuItem *)menuItem; + if (item.checkedWhen) { + item.state = item.checkedWhen() ? NSControlStateValueOn : NSControlStateValueOff; + } + if (item.currentTitle) { + item.title = item.currentTitle(); + } + return item.enabledWhen ? item.enabledWhen() : YES; +} + +- (void)menuNeedsUpdate:(NSMenu *)menu { + if (menu == self.recentsMenu) { + [self rebuildRecentsMenu]; + } else if (menu == self.saveStateSlotMenu) { + [self rebuildSaveStateSlotMenu]; + } } -(void)setupAppBarItems { - - NSMenuItem *fileMenuItem = [[NSMenuItem alloc] init]; - fileMenuItem.submenu = [self makeFileSubmenu]; - fileMenuItem.submenu.delegate = self; + NSMenu *mainMenu = NSApplication.sharedApplication.menu; + for (NSMenu *menu in @[[self makeFileMenu], [self makeEmulationMenu], [self makeDebugMenu], [self makeGameSettingsMenu], [self makeHelpMenu]]) { + NSMenuItem *item = [[NSMenuItem alloc] init]; + item.submenu = menu; + [mainMenu addItem:item]; + } - NSMenuItem *emulationMenuItem = [[NSMenuItem alloc] init]; - emulationMenuItem.submenu = [self makeEmulationMenu]; - emulationMenuItem.submenu.delegate = self; - self.emulationMenu = emulationMenuItem.submenu; - - NSMenuItem *debugMenuItem = [[NSMenuItem alloc] init]; - debugMenuItem.submenu = [self makeDebugMenu]; - debugMenuItem.submenu.delegate = self; - self.debugMenu = debugMenuItem.submenu; - - NSMenuItem *graphicsMenuItem = [[NSMenuItem alloc] init]; - graphicsMenuItem.submenu = [self makeGraphicsMenu]; - graphicsMenuItem.submenu.delegate = self; - self.graphicsMenu = graphicsMenuItem.submenu; - - NSMenuItem *helpMenuItem = [[NSMenuItem alloc] init]; - helpMenuItem.submenu = [self makeHelpMenu]; - - [NSApplication.sharedApplication.menu addItem:fileMenuItem]; - [NSApplication.sharedApplication.menu addItem:emulationMenuItem]; - [NSApplication.sharedApplication.menu addItem:debugMenuItem]; - [NSApplication.sharedApplication.menu addItem:graphicsMenuItem]; - [NSApplication.sharedApplication.menu addItem:helpMenuItem]; - NSString *windowMenuItemTitle = @"Window"; // Don't translate, we lookup this. // Rearrange 'Window' to be behind 'Help' - for (NSMenuItem *item in NSApplication.sharedApplication.menu.itemArray) { + for (NSMenuItem *item in mainMenu.itemArray) { if ([item.title isEqualToString:windowMenuItemTitle]) { - [NSApplication.sharedApplication.menu removeItem:item]; + [mainMenu removeItem:item]; // 'Help' is the last item in the bar - // so we can just use `NSApplication.sharedApplication.menu.numberOfItems - 1` + // so we can just use `mainMenu.numberOfItems - 1` // as it's index - [NSApplication.sharedApplication.menu insertItem:item atIndex:NSApplication.sharedApplication.menu.numberOfItems - 1]; + [mainMenu insertItem:item atIndex:mainMenu.numberOfItems - 1]; break; } } - NSArray *firstSubmenu = NSApp.menu.itemArray.firstObject.submenu.itemArray; - for (NSMenuItem *item in firstSubmenu) { + NSMenu *appMenu = mainMenu.itemArray.firstObject.submenu; + for (NSMenuItem *item in appMenu.itemArray) { // about item, set action if ([item.title hasPrefix:@"About "]) { item.target = self; item.action = @selector(presentAboutMenu); + + // Settings go right after About, as in every other Mac app. + NSInteger index = [appMenu indexOfItem:item] + 1; + [appMenu insertItem:[NSMenuItem separatorItem] atIndex:index]; + PPSSPPMenuItem *settings = [self addItem:[MenuString(I18NCat::DIALOG, "Settings") stringByAppendingString:@"…"] to:appMenu action:^{ + System_PostUIMessage(UIMessage::SHOW_SETTINGS); + }]; + settings.keyEquivalent = @","; + [appMenu removeItem:settings]; + [appMenu insertItem:settings atIndex:index + 1]; break; } } @@ -139,7 +234,7 @@ void OSXOpenURL(const char *url) { window.titlebarAppearsTransparent = YES; window.styleMask &= ~NSWindowStyleMaskResizable; [[window standardWindowButton:NSWindowMiniaturizeButton] setEnabled:NO]; - + window.backgroundColor = [NSColor colorWithName:nil dynamicProvider:^NSColor * _Nonnull(NSAppearance * _Nonnull appearance) { /* no I can't use switch statements here it's an NSString pointer */ if (appearance.name == NSAppearanceNameDarkAqua || @@ -149,325 +244,307 @@ void OSXOpenURL(const char *url) { return [NSColor colorWithRed:0.19 green:0.19 blue:0.19 alpha:1]; return [NSColor whiteColor]; }]; - + [[[NSWindowController alloc] initWithWindow:window] showWindow:nil]; } -- (void)menuNeedsUpdate:(NSMenu *)menu { - if (menu == self.recentsMenu) { - [self rebuildRecentsMenu]; - } else if (menu == self.emulationMenu) { - menu.autoenablesItems = NO; - // Enable/disable the various items. - for (NSMenuItem *item in menu.itemArray) { - switch (item.tag) { - case 1: - case 2: - case 3: - { - GlobalUIState state = GetUIState(); - item.enabled = state == UISTATE_INGAME ? YES : NO; - // printf("Setting enabled state to %d\n", (int)item.enabled); - break; - } - default: - printf("Unknown tag %d\n", (int)(long)item.tag); - break; - } +-(NSMenu *)makeFileMenu { + NSMenu *menu = [self newMenu:DesktopUI("File")]; + + PPSSPPMenuItem *load = [self addItem:DesktopUI("Load") to:menu action:^{ + DarwinFileSystemServices::presentDirectoryPanel([](bool succ, Path thePathChosen) { + if (succ) + System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, thePathChosen.c_str()); + }, /* allowFiles = */ true, /* allowDirectories = */ true); + }]; + load.keyEquivalent = @"o"; + + NSMenuItem *openRecent = [[NSMenuItem alloc] initWithTitle:MenuString(I18NCat::MAINMENU, "Recent") action:nil keyEquivalent:@""]; + self.recentsMenu = [self newMenu:openRecent.title]; + openRecent.submenu = self.recentsMenu; + [menu addItem:openRecent]; + [self rebuildRecentsMenu]; + + [self addSeparatorTo:menu]; + [self addItem:DesktopUI("Open Memory Stick") to:menu action:^{ + [NSWorkspace.sharedWorkspace openURL:[NSURL fileURLWithPath:@(g_Config.memStickDirectory.c_str())]]; + }]; + + [self addSeparatorTo:menu]; + [self addItem:DesktopUI("Load XMB (VSH)") to:menu action:^{ + System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, (g_Config.nandRootDirectory / "flash0/vsh/module/vshmain.prx").ToString()); + }]; + + [self addSeparatorTo:menu]; + MenuCondition inGame = ^BOOL { return InGame(); }; + + NSMenuItem *slotItem = [[NSMenuItem alloc] initWithTitle:DesktopUI("Savestate Slot") action:nil keyEquivalent:@""]; + self.saveStateSlotMenu = [self newMenu:slotItem.title]; + slotItem.submenu = self.saveStateSlotMenu; + [menu addItem:slotItem]; + [self rebuildSaveStateSlotMenu]; + + PPSSPPMenuItem *loadState = [self addItem:DesktopUI("Load State") to:menu action:^{ + if (CanUseSaveStates(false)) { + SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &SaveStateActionFinished); } - } else if (menu == self.graphicsMenu) { - for (NSMenuItem *item in menu.itemArray) { - switch (item.tag) { - case 1: - item.state = [self controlStateForBool:g_Config.bSoftwareRendering]; - break; - case 2: - item.state = [self controlStateForBool:g_Config.bVSync]; - break; - case 3: - item.state = [self controlStateForBool:g_Config.bFullScreen]; - break; - case 4: - item.state = [self controlStateForBool:g_Config.bAutoFrameSkip]; - break; - case (int)ShowStatusFlags::FPS_COUNTER + 100: - item.state = [self controlStateForBool:g_Config.iShowStatusFlags & (int)ShowStatusFlags::FPS_COUNTER]; - break; - case (int)ShowStatusFlags::SPEED_COUNTER + 100: - item.state = [self controlStateForBool:g_Config.iShowStatusFlags & (int)ShowStatusFlags::SPEED_COUNTER]; - break; - case (int)ShowStatusFlags::BATTERY_PERCENT + 100: - item.state = [self controlStateForBool:g_Config.iShowStatusFlags & (int)ShowStatusFlags::BATTERY_PERCENT]; - break; - default: - break; - } + }]; + loadState.keyEquivalent = @"l"; + loadState.enabledWhen = inGame; + + PPSSPPMenuItem *saveState = [self addItem:DesktopUI("Save State") to:menu action:^{ + if (CanUseSaveStates(true)) { + SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &SaveStateActionFinished); } - } else if (menu == self.debugMenu) { - menu.autoenablesItems = NO; - GlobalUIState state = GetUIState(); - for (NSMenuItem *item in menu.itemArray) { - switch ([item tag]) { - case 2: - item.state = [self controlStateForBool:!g_Config.bAutoRun]; - break; - case 3: - item.state = [self controlStateForBool:g_Config.bIgnoreBadMemAccess]; - break; - case 12: - item.state = [self controlStateForBool:(DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS]; - break; - default: - item.enabled = state == UISTATE_INGAME ? YES : NO; - break; - } + }]; + saveState.keyEquivalent = @"s"; + saveState.enabledWhen = inGame; + + [self addItem:DesktopUI("Load State File...") to:menu action:^{ + if (!CanUseSaveStates(false)) { + return; } - } -} + NSURL *url = [BarItemsManager presentOpenPanelWithAllowedFileTypes:@[@"ppst"]]; + if (url) { + SaveState::Load(Path(url.fileSystemRepresentation), -1, &SaveStateActionFinished); + } + }].enabledWhen = inGame; --(NSMenu *)makeHelpMenu { - NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Help"]; - NSMenuItem *githubItem = [[NSMenuItem alloc] initWithTitle:@"Report an issue" action:@selector(reportAnIssue) keyEquivalent:@""]; - githubItem.target = self; - [menu addItem:githubItem]; - - NSMenuItem *discordItem = [[NSMenuItem alloc] initWithTitle:@"Join the Discord" action:@selector(joinTheDiscord) keyEquivalent:@""]; - discordItem.target = self; - [menu addItem:discordItem]; - return menu; -} - --(void)reportAnIssue { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/hrydgard/ppsspp/issues/new/choose"]]; -} - --(void)joinTheDiscord { - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://discord.gg/5NJB6dD"]]; -} - --(NSMenu *)makeFileSubmenu { - std::shared_ptr desktopUILocalization = GetI18NCategory(I18NCat::DESKTOPUI); -#define DESKTOPUI_LOCALIZED(key) @(UnescapeMenuString(desktopUILocalization->T_cstr(key), nil).c_str()) - - NSMenu *menu = [[NSMenu alloc] initWithTitle:DESKTOPUI_LOCALIZED("File")]; - NSMenuItem *openWithSystemFolderBrowserItem = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Load") action:@selector(openSystemFileBrowser) keyEquivalent:@"o"]; - openWithSystemFolderBrowserItem.keyEquivalentModifierMask = NSEventModifierFlagCommand; - openWithSystemFolderBrowserItem.enabled = YES; - openWithSystemFolderBrowserItem.target = self; - [menu addItem:openWithSystemFolderBrowserItem]; - self.fileMenu = menu; - [self addOpenRecentlyItem]; - - [self.fileMenu addItem:[NSMenuItem separatorItem]]; - - NSMenuItem *openMemstickFolderItem = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Open Memory Stick") action:@selector(openMemstickFolder) keyEquivalent:@""]; - openMemstickFolderItem.target = self; - [self.fileMenu addItem:openMemstickFolderItem]; - - [self.fileMenu addItem:[NSMenuItem separatorItem]]; - - NSMenuItem *loadVSH = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Load VSH") action:@selector(loadVSH) keyEquivalent:@""]; - loadVSH.enabled = YES; - loadVSH.target = self; - [self.fileMenu addItem:loadVSH]; + [self addItem:DesktopUI("Save State File...") to:menu action:^{ + if (!CanUseSaveStates(true)) { + return; + } + NSURL *url = [BarItemsManager presentSavePanelWithDefaultFilename:@"state.ppst"]; + if (url) { + SaveState::Save(Path(url.fileSystemRepresentation), -1, &SaveStateActionFinished); + } + }].enabledWhen = inGame; return menu; } --(NSMenu *)makeGraphicsMenu { - std::shared_ptr mainSettingsLocalization = GetI18NCategory(I18NCat::MAINSETTINGS); - std::shared_ptr graphicsLocalization = GetI18NCategory(I18NCat::GRAPHICS); - std::shared_ptr desktopUILocalization = GetI18NCategory(I18NCat::DESKTOPUI); - - NSMenu *parent = [[NSMenu alloc] initWithTitle:@(mainSettingsLocalization->T_cstr("Graphics"))]; - NSMenu *backendsMenu = [[NSMenu alloc] init]; -#define GRAPHICS_LOCALIZED(key) @(graphicsLocalization->T_cstr(key)) -#define DESKTOPUI_LOCALIZED(key) @(UnescapeMenuString(desktopUILocalization->T_cstr(key), nil).c_str()) - - NSMenuItem *gpuBackendItem = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Backend") action:nil keyEquivalent:@""]; - - std::vector allowed = [self allowedGPUBackends]; - for (int i = 0; i < allowed.size(); i++) { - NSMenuItem *backendMenuItem = [[NSMenuItem alloc] initWithTitle:@(GPUBackendToString(allowed[i]).c_str()) action: @selector(setCurrentGPUBackend:) keyEquivalent: @""]; - backendMenuItem.tag = i; - backendMenuItem.target = self; - backendMenuItem.state = [self controlStateForBool: g_Config.iGPUBackend == (int)allowed[i]]; - [backendsMenu addItem:backendMenuItem]; +-(void)rebuildRecentsMenu { + [self.recentsMenu removeAllItems]; + for (const auto &file : g_recentFiles.GetRecentFiles()) { + std::string path = file; + [self addItem:@(Path(file).GetFilename().c_str()) to:self.recentsMenu action:^{ + System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, path); + }]; } +} - gpuBackendItem.submenu = backendsMenu; - [parent addItem:gpuBackendItem]; - - [parent addItem:[NSMenuItem separatorItem]]; - - MENU_ITEM(softwareRendering, GRAPHICS_LOCALIZED("Software Rendering"), @selector(toggleSoftwareRendering:), g_Config.bSoftwareRendering, 1) - [parent addItem:softwareRendering]; - - /* - MENU_ITEM(vsyncItem, GRAPHICS_LOCALIZED("VSync"), @selector(toggleVSync:), g_Config.bVSync, 2) - [parent addItem:vsyncItem]; - */ - - MENU_ITEM(fullScreenItem, DESKTOPUI_LOCALIZED("Fullscreen"), @selector(toggleFullScreen:), g_Config.bFullScreen, 3) - [parent addItem:fullScreenItem]; - - [parent addItem:[NSMenuItem separatorItem]]; - - MENU_ITEM(autoFrameSkip, GRAPHICS_LOCALIZED("Auto FrameSkip"), @selector(toggleAutoFrameSkip:), g_Config.bAutoFrameSkip, 4) - [parent addItem:autoFrameSkip]; - - [parent addItem:[NSMenuItem separatorItem]]; - - MENU_ITEM(fpsCounterItem, DESKTOPUI_LOCALIZED("Show FPS Counter"), @selector(setToggleShowCounterItem:), g_Config.iShowStatusFlags & (int)ShowStatusFlags::FPS_COUNTER, 5) - fpsCounterItem.tag = (int)ShowStatusFlags::FPS_COUNTER + 100; - - MENU_ITEM(speedCounterItem, GRAPHICS_LOCALIZED("Show Speed"), @selector(setToggleShowCounterItem:), g_Config.iShowStatusFlags & (int)ShowStatusFlags::SPEED_COUNTER, 6) - speedCounterItem.tag = (int)ShowStatusFlags::SPEED_COUNTER + 100; // because of menuNeedsUpdate: - - MENU_ITEM(batteryPercentItem, GRAPHICS_LOCALIZED("Show battery %"), @selector(setToggleShowCounterItem:), g_Config.iShowStatusFlags & (int)ShowStatusFlags::BATTERY_PERCENT, 7) - batteryPercentItem.tag = (int)ShowStatusFlags::BATTERY_PERCENT + 100; - - [parent addItem:[NSMenuItem separatorItem]]; - [parent addItem:fpsCounterItem]; - [parent addItem:speedCounterItem]; - [parent addItem:batteryPercentItem]; - -#undef GRAPHICS_LOCALIZED - return parent; +-(void)rebuildSaveStateSlotMenu { + // The slot count is a setting, so this can change. + [self.saveStateSlotMenu removeAllItems]; + auto di = GetI18NCategory(I18NCat::DIALOG); + for (int i = 0; i < g_Config.iSaveStateSlotCount; ++i) { + std::string label = ApplySafeSubstitutions(di->T("Slot %1"), i + 1); + PPSSPPMenuItem *item = [self addItem:@(label.c_str()) to:self.saveStateSlotMenu action:^{ + if (CanUseSaveStates(true)) { + g_Config.iCurrentStateSlot = i; + } + }]; + item.checkedWhen = ^BOOL { return g_Config.iCurrentStateSlot == i; }; + } } -(NSMenu *)makeEmulationMenu { - std::shared_ptr desktopUILocalization = GetI18NCategory(I18NCat::DESKTOPUI); -#define DESKTOPUI_LOCALIZED(key) @(UnescapeMenuString(desktopUILocalization->T_cstr(key), nil).c_str()) + NSMenu *menu = [self newMenu:DesktopUI("Emulation")]; + MenuCondition inGame = ^BOOL { return InGame(); }; - NSMenu *parent = [[NSMenu alloc] initWithTitle:DESKTOPUI_LOCALIZED("Emulation")]; + [self addItem:DesktopUI("Pause") to:menu action:^{ + System_PostUIMessage(UIMessage::REQUEST_GAME_PAUSE); + }].enabledWhen = inGame; + [self addItem:DesktopUI("Stop") to:menu action:^{ + System_PostUIMessage(UIMessage::REQUEST_GAME_STOP); + }].enabledWhen = inGame; + [self addItem:DesktopUI("Reset") to:menu action:^{ + System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); + Core_Resume(); + }].enabledWhen = inGame; + [self addItem:DesktopUI("Switch UMD") to:menu action:^{ + System_BrowseForFile(NON_EPHEMERAL_TOKEN, T(I18NCat::MAINMENU, "Switch UMD"), BrowseFileType::BOOTABLE, [](std::string_view value, int) { + // The callback runs on the emu thread. + __UmdReplace(Path(value)); + }); + }].enabledWhen = inGame; - NSMenuItem *pauseAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Pause") action:@selector(pauseAction:) keyEquivalent:@""]; - pauseAction.target = self; - pauseAction.tag = 1; - NSMenuItem *resetAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Reset") action:@selector(resetAction:) keyEquivalent:@""]; - resetAction.target = self; - resetAction.tag = 2; - - [parent addItem:pauseAction]; - [parent addItem:resetAction]; - - return parent; + return menu; } -(NSMenu *)makeDebugMenu { - std::shared_ptr sysInfoLocalization = GetI18NCategory(I18NCat::SYSINFO); - std::shared_ptr desktopUILocalization = GetI18NCategory(I18NCat::DESKTOPUI); -#define DESKTOPUI_LOCALIZED(key) @(UnescapeMenuString(desktopUILocalization->T_cstr(key), nil).c_str()) + NSMenu *menu = [self newMenu:DesktopUI("Debugging")]; + MenuCondition inGame = ^BOOL { return InGame(); }; - NSMenu *parent = [[NSMenu alloc] initWithTitle:DESKTOPUI_LOCALIZED("Debugging")]; + PPSSPPMenuItem *breakItem = [self addItem:DesktopUI("Break") to:menu action:^{ + if (Core_IsStepping()) { + Core_Resume(); + } else { + Core_Break(BreakReason::DebugBreak, 0); + } + }]; + breakItem.enabledWhen = inGame; + breakItem.currentTitle = ^NSString * { + return Core_IsStepping() ? MenuString(I18NCat::DEVELOPER, "Resume") : DesktopUI("Break"); + }; + [self addSeparatorTo:menu]; - NSMenuItem *breakAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Break") action:@selector(breakAction:) keyEquivalent:@""]; - breakAction.tag = 1; - breakAction.target = self; + [self addItem:DesktopUI("Break on Load") to:menu action:^{ + g_Config.bAutoRun = !g_Config.bAutoRun; + }].checkedWhen = ^BOOL { return !g_Config.bAutoRun; }; + [self addToggle:DesktopUI("Ignore Illegal Reads/Writes") to:menu value:&g_Config.bIgnoreBadMemAccess then:nil]; + [self addSeparatorTo:menu]; - MENU_ITEM(breakOnLoadAction, DESKTOPUI_LOCALIZED("Break on Load"), @selector(toggleBreakOnLoad:), g_Config.bAutoRun, 2) - MENU_ITEM(ignoreIllegalRWAction, DESKTOPUI_LOCALIZED("Ignore Illegal Reads/Writes"), @selector(toggleIgnoreIllegalRWs:), g_Config.bIgnoreBadMemAccess, 3) + [self addItem:DesktopUI("Load Map File...") to:menu action:^{ + NSURL *url = [BarItemsManager presentOpenPanelWithAllowedFileTypes:@[@"map"]]; + if (url) + g_symbolMap->LoadSymbolMap(Path(url.fileSystemRepresentation)); + }].enabledWhen = inGame; + [self addItem:DesktopUI("Save Map file...") to:menu action:^{ + NSURL *url = [BarItemsManager presentSavePanelWithDefaultFilename:@"Symbols.map"]; + if (url) + g_symbolMap->SaveSymbolMap(Path(url.fileSystemRepresentation)); + }].enabledWhen = inGame; + [self addSeparatorTo:menu]; - [parent addItem:breakAction]; - [parent addItem:[NSMenuItem separatorItem]]; + [self addItem:DesktopUI("Load .sym File...") to:menu action:^{ + NSURL *url = [BarItemsManager presentOpenPanelWithAllowedFileTypes:@[@"sym"]]; + if (url) + g_symbolMap->LoadNocashSym(Path(url.fileSystemRepresentation)); + }].enabledWhen = inGame; + [self addItem:DesktopUI("Save .sym File...") to:menu action:^{ + NSURL *url = [BarItemsManager presentSavePanelWithDefaultFilename:@"Symbols.sym"]; + if (url) + g_symbolMap->SaveNocashSym(Path(url.fileSystemRepresentation)); + }].enabledWhen = inGame; + [self addSeparatorTo:menu]; - [parent addItem:breakOnLoadAction]; - [parent addItem:ignoreIllegalRWAction]; - [parent addItem:[NSMenuItem separatorItem]]; + [self addItem:DesktopUI("Reset Symbol Table") to:menu action:^{ + g_symbolMap->Clear(); + }].enabledWhen = inGame; + [self addSeparatorTo:menu]; - // Tags 1, 2, and 3 are taken above. + [self addItem:DesktopUI("Take Screenshot") to:menu action:^{ + g_TakeScreenshot = true; + }].enabledWhen = inGame; + [self addItem:DesktopUI("Save frame dump") to:menu action:^{ + System_PostUIMessage(UIMessage::SAVE_FRAME_DUMP); + }].enabledWhen = inGame; + [self addItem:DesktopUI("Show Debug Statistics") to:menu action:^{ + const bool on = (DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS; + g_Config.iDebugOverlay = (int)(on ? DebugOverlay::OFF : DebugOverlay::DEBUG_STATS); + System_PostUIMessage(UIMessage::REQUEST_CLEAR_JIT); + }].checkedWhen = ^BOOL { return (DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS; }; + [self addSeparatorTo:menu]; - NSMenuItem *loadSymbolMapAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Load Map File...") action:@selector(loadMapFile) keyEquivalent:@""]; - loadSymbolMapAction.target = self; - loadSymbolMapAction.tag = 4; + [self addItem:DesktopUI("Copy PSP memory base address") to:menu action:^{ + NSString *stringToCopy = [NSString stringWithFormat: @"%016llx", (uint64_t)(uintptr_t)Memory::base]; + [NSPasteboard.generalPasteboard declareTypes:@[NSPasteboardTypeString] owner:nil]; + [NSPasteboard.generalPasteboard setString:stringToCopy forType:NSPasteboardTypeString]; + }].enabledWhen = inGame; - NSMenuItem *saveMapFileAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Save Map file...") action:@selector(saveMapFile) keyEquivalent:@""]; - saveMapFileAction.target = self; - saveMapFileAction.tag = 5; - - NSMenuItem *loadSymFileAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Load .sym File...") action:@selector(loadSymbolsFile) keyEquivalent:@""]; - loadSymFileAction.target = self; - loadSymFileAction.tag = 6; - - NSMenuItem *saveSymFileAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Save .sym File...") action:@selector(saveSymbolsfile) keyEquivalent:@""]; - saveSymFileAction.target = self; - saveSymFileAction.tag = 7; - - NSMenuItem *resetSymbolTableAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Reset Symbol Table") action:@selector(resetSymbolTable) keyEquivalent:@""]; - resetSymbolTableAction.target = self; - resetSymbolTableAction.tag = 8; - - NSMenuItem *takeScreenshotAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Take Screenshot") action:@selector(takeScreenshot) keyEquivalent:@""]; - takeScreenshotAction.target = self; - takeScreenshotAction.tag = 9; - - NSMenuItem *saveFrameDumpAction = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Save frame dump") action:@selector(saveFrameDump) keyEquivalent:@""]; - saveFrameDumpAction.target = self; - saveFrameDumpAction.tag = 10; - - NSMenuItem *copyBaseAddr = [[NSMenuItem alloc] initWithTitle:DESKTOPUI_LOCALIZED("Copy PSP memory base address") action:@selector(copyAddr) keyEquivalent:@""]; - copyBaseAddr.target = self; - copyBaseAddr.tag = 11; - - MENU_ITEM(showDebugStatsAction, DESKTOPUI_LOCALIZED("Show Debug Statistics"), @selector(toggleShowDebugStats:), ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS), 12) - - [parent addItem:loadSymbolMapAction]; - [parent addItem:saveMapFileAction]; - [parent addItem:[NSMenuItem separatorItem]]; - - [parent addItem:loadSymFileAction]; - [parent addItem:saveSymFileAction]; - [parent addItem:[NSMenuItem separatorItem]]; - - [parent addItem:resetSymbolTableAction]; - [parent addItem:[NSMenuItem separatorItem]]; - - [parent addItem:takeScreenshotAction]; - [parent addItem:saveFrameDumpAction]; - [parent addItem:showDebugStatsAction]; - - [parent addItem:[NSMenuItem separatorItem]]; - [parent addItem:copyBaseAddr]; - return parent; + return menu; } --(void)breakAction: (NSMenuItem *)item { - std::shared_ptr desktopUILocalization = GetI18NCategory(I18NCat::DESKTOPUI); -#define DESKTOPUI_LOCALIZED(key) @(UnescapeMenuString(desktopUILocalization->T_cstr(key), nil).c_str()) - std::shared_ptr developerUILocalization = GetI18NCategory(I18NCat::DEVELOPER); -#define DEVELOPERUI_LOCALIZED(key) @(developerUILocalization->T_cstr(key)) - if (Core_IsStepping()) { - Core_Resume(); - item.title = DESKTOPUI_LOCALIZED("Break"); - } else { - Core_Break(BreakReason::DebugBreak, 0); - item.title = DEVELOPERUI_LOCALIZED("Resume"); +-(NSMenu *)makeGameSettingsMenu { + NSMenu *menu = [self newMenu:DesktopUI("Game Settings")]; + auto graphics = [](const char *key) { return MenuString(I18NCat::GRAPHICS, key); }; + + [self addItem:DesktopUI("More Settings...") to:menu action:^{ + System_PostUIMessage(UIMessage::SHOW_SETTINGS); + }]; + [self addItem:DesktopUI("Control Mapping...") to:menu action:^{ + System_PostUIMessage(UIMessage::SHOW_CONTROL_MAPPING); + }]; + [self addItem:DesktopUI("Display Layout && Effects") to:menu action:^{ + System_PostUIMessage(UIMessage::SHOW_DISPLAY_LAYOUT_EDITOR); + }]; + [self addItem:DesktopUI("Language...") to:menu action:^{ + System_PostUIMessage(UIMessage::SHOW_LANGUAGE_SCREEN); + }]; + [self addSeparatorTo:menu]; + + NSMenu *backends = [self addSubmenu:DesktopUI("Backend") to:menu]; + for (GPUBackend backend : { GPUBackend::OPENGL, GPUBackend::VULKAN }) { + if (!g_Config.IsBackendEnabled(backend)) { + continue; + } + [self addItem:@(GPUBackendToString(backend).c_str()) to:backends action:^{ + if (g_Config.iGPUBackend != (int)backend) { + g_Config.iGPUBackend = (int)backend; + // This does a clean shutdown, so the config will be saved automatically. + System_RestartApp(""); + } + }].checkedWhen = ^BOOL { return g_Config.iGPUBackend == (int)backend; }; } -} + [self addToggle:graphics("Software Rendering") to:menu value:&g_Config.bSoftwareRendering then:nil]; + [self addToggle:DesktopUI("Fullscreen") to:menu value:&g_Config.bFullScreen then:^{ + System_ApplyFullscreenState(); + }]; + [self addSeparatorTo:menu]; --(void)pauseAction: (NSMenuItem *)item { - System_PostUIMessage(UIMessage::REQUEST_GAME_PAUSE); -} - --(void)resetAction: (NSMenuItem *)item { - System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); - Core_Resume(); -} - --(void)chatAction: (NSMenuItem *)item { - if (GetUIState() == UISTATE_INGAME) { - System_PostUIMessage(UIMessage::SHOW_CHAT_SCREEN); + NSMenu *resolution = [self addSubmenu:DesktopUI("Rendering Resolution") to:menu]; + dispatch_block_t resized = ^{ + System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED); + }; + [self addChoice:DesktopUI("Auto") to:resolution value:&g_Config.iInternalResolution is:0 then:resized]; + for (int i = 1; i <= 10; i++) { + [self addChoice:[NSString stringWithFormat:@"%dx", i] to:resolution value:&g_Config.iInternalResolution is:i then:resized]; } + + NSMenu *filtering = [self addSubmenu:DesktopUI("Texture Filtering") to:menu]; + [self addChoice:DesktopUI("Auto") to:filtering value:&g_Config.iTexFiltering is:TEX_FILTER_AUTO then:nil]; + [self addChoice:DesktopUI("Nearest") to:filtering value:&g_Config.iTexFiltering is:TEX_FILTER_FORCE_NEAREST then:nil]; + [self addChoice:DesktopUI("Linear") to:filtering value:&g_Config.iTexFiltering is:TEX_FILTER_FORCE_LINEAR then:nil]; + [self addChoice:DesktopUI("Auto Max Quality") to:filtering value:&g_Config.iTexFiltering is:TEX_FILTER_AUTO_MAX_QUALITY then:nil]; + [self addSeparatorTo:filtering]; + [self addToggle:DesktopUI("Smart 2D texture filtering") to:filtering value:&g_Config.bSmart2DTexFiltering then:nil]; + + [self addToggle:graphics("Auto FrameSkip") to:menu value:&g_Config.bAutoFrameSkip then:^{ + g_Config.UpdateAfterSettingAutoFrameSkip(); + }]; + [self addSeparatorTo:menu]; + + for (auto [flag, title] : { std::pair{ ShowStatusFlags::FPS_COUNTER, DesktopUI("Show FPS Counter") }, + std::pair{ ShowStatusFlags::SPEED_COUNTER, graphics("Show Speed") }, + std::pair{ ShowStatusFlags::BATTERY_PERCENT, graphics("Show Battery %") } }) { + const int mask = (int)flag; + [self addItem:title to:menu action:^{ + g_Config.iShowStatusFlags ^= mask; + }].checkedWhen = ^BOOL { return (g_Config.iShowStatusFlags & mask) != 0; }; + } + [self addSeparatorTo:menu]; + + [self addToggle:DesktopUI("Enable Sound") to:menu value:&g_Config.bEnableSound then:nil]; + [self addToggle:DesktopUI("Enable Cheats") to:menu value:&g_Config.bEnableCheats then:^{ + g_OSD.ShowOnOff(T(I18NCat::GRAPHICS, "Cheats"), g_Config.bEnableCheats); + }]; + + return menu; } --(void)copyAddr { - NSString *stringToCopy = [NSString stringWithFormat: @"%016llx", (uint64_t)(uintptr_t)Memory::base]; - [NSPasteboard.generalPasteboard declareTypes:@[NSPasteboardTypeString] owner:nil]; - [NSPasteboard.generalPasteboard setString:stringToCopy forType:NSPasteboardTypeString]; +-(NSMenu *)makeHelpMenu { + NSMenu *menu = [self newMenu:DesktopUI("Help")]; + auto addLink = ^(NSString *title, const char *url) { + std::string link = url; + [self addItem:title to:menu action:^{ + System_LaunchUrl(LaunchUrlType::BROWSER_URL, link); + }]; + }; + + addLink(DesktopUI("www.ppsspp.org"), "https://www.ppsspp.org/"); + addLink(DesktopUI("PPSSPP Forums"), "https://forums.ppsspp.org/"); + if (!System_GetPropertyBool(SYSPROP_APP_GOLD)) { + addLink(DesktopUI("Buy PPSSPP Gold"), "https://www.ppsspp.org/buygold"); + } + [self addSeparatorTo:menu]; + addLink(DesktopUI("GitHub"), "https://github.com/hrydgard/ppsspp/"); + addLink(@"Report an issue", "https://github.com/hrydgard/ppsspp/issues/new/choose"); + addLink(DesktopUI("Discord"), "https://discord.gg/5NJB6dD"); + return menu; } --(NSURL *)presentOpenPanelWithAllowedFileTypes: (NSArray *)allowedFileTypes { ++(NSURL *)presentOpenPanelWithAllowedFileTypes: (NSArray *)allowedFileTypes { NSOpenPanel *openPanel = [[NSOpenPanel alloc] init]; openPanel.allowedFileTypes = allowedFileTypes; if ([openPanel runModal] == NSModalResponseOK) { @@ -479,7 +556,7 @@ void OSXOpenURL(const char *url) { return nil; } --(NSURL *)presentSavePanelWithDefaultFilename: (NSString *)filename { ++(NSURL *)presentSavePanelWithDefaultFilename: (NSString *)filename { NSSavePanel *savePanel = [[NSSavePanel alloc] init]; savePanel.nameFieldStringValue = filename; if ([savePanel runModal] == NSModalResponseOK && savePanel.URL) { @@ -488,183 +565,6 @@ void OSXOpenURL(const char *url) { return nil; } --(void)saveFrameDump { - System_PostUIMessage(UIMessage::SAVE_FRAME_DUMP); -} - --(void)takeScreenshot { - g_TakeScreenshot = true; -} - --(void)resetSymbolTable { - g_symbolMap->Clear(); -} - --(void)saveSymbolsfile { - NSURL *url = [self presentSavePanelWithDefaultFilename:@"Symbols.sym"]; - if (url) - g_symbolMap->SaveNocashSym(Path(url.fileSystemRepresentation)); -} - --(void)loadSymbolsFile { - NSURL *url = [self presentOpenPanelWithAllowedFileTypes:@[@"sym"]]; - if (url) - g_symbolMap->LoadNocashSym(Path(url.fileSystemRepresentation)); -} - --(void)loadMapFile { - /* Using NSOpenPanel to filter by `allowedFileTypes` */ - NSURL *url = [self presentOpenPanelWithAllowedFileTypes:@[@"map"]]; - if (url) - g_symbolMap->LoadSymbolMap(Path(url.fileSystemRepresentation)); -} - --(void)saveMapFile { - NSURL *symbolsMapFileURL = [self presentSavePanelWithDefaultFilename:@"Symbols.map"]; - if (symbolsMapFileURL) - g_symbolMap->SaveSymbolMap(Path(symbolsMapFileURL.fileSystemRepresentation)); -} - -#define TOGGLE_METHOD(name, ConfigValueName, ...) \ --(void)toggle##name: (NSMenuItem *)item { \ -ConfigValueName = !ConfigValueName; \ -__VA_ARGS__; /* for any additional updates */ \ -item.state = [self controlStateForBool: ConfigValueName]; \ -} - -#define TOGGLE_METHOD_INVERSE(name, ConfigValueName, ...) \ --(void)toggle##name: (NSMenuItem *)item { \ -ConfigValueName = !ConfigValueName; \ -__VA_ARGS__; /* for any additional updates */ \ -item.state = [self controlStateForBool: !ConfigValueName]; \ -} - -TOGGLE_METHOD(Sound, g_Config.bEnableSound) -TOGGLE_METHOD_INVERSE(BreakOnLoad, g_Config.bAutoRun) -TOGGLE_METHOD(IgnoreIllegalRWs, g_Config.bIgnoreBadMemAccess) -TOGGLE_METHOD(AutoFrameSkip, g_Config.bAutoFrameSkip, g_Config.UpdateAfterSettingAutoFrameSkip()) -TOGGLE_METHOD(SoftwareRendering, g_Config.bSoftwareRendering) -TOGGLE_METHOD(FullScreen, g_Config.bFullScreen, System_ApplyFullscreenState()); -// TOGGLE_METHOD(VSync, g_Config.bVSync) -#undef TOGGLE_METHOD - --(void)toggleShowDebugStats: (NSMenuItem *)item { \ - if ((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS) { - g_Config.iDebugOverlay = (int)DebugOverlay::OFF; - } else { - g_Config.iDebugOverlay = (int)DebugOverlay::DEBUG_STATS; - } - System_PostUIMessage(UIMessage::REQUEST_CLEAR_JIT); - item.state = [self controlStateForBool: (DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS]; \ -} - --(void)setToggleShowCounterItem: (NSMenuItem *)item { - [self addOrRemoveInteger:(int)(item.tag - 100) to:&g_Config.iShowStatusFlags]; - item.state = [self controlStateForBool:g_Config.iShowStatusFlags & (int)(item.tag - 100)]; -} - --(void)addOrRemoveInteger: (int)integer to: (int *)r { - if (integer & *r) { - *r -= integer; - } else { - *r |= integer; - } -} - --(void)setCurrentGPUBackend: (NSMenuItem *)sender { - std::vector allowed = [self allowedGPUBackends]; - if (allowed.size() == 1) { - printf("only one item, bailing"); - return; - } - - g_Config.iGPUBackend = (int)(allowed[sender.tag]); - sender.state = NSControlStateValueOn; - - for (NSMenuItem *item in sender.menu.itemArray) { - // deselect the previously selected item - if (item.state == NSControlStateValueOn && item.tag != sender.tag) { - item.state = NSControlStateValueOff; - break; - } - } - - // TODO: Use same command line params as the previous startup? - // Note that this does a clean shutdown, so the config will be saved automatically. - System_RestartApp(""); -} - --(NSControlStateValue) controlStateForBool: (BOOL)boolValue { - return boolValue ? NSControlStateValueOn : NSControlStateValueOff; -} - --(std::vector)allowedGPUBackends { - std::vector allBackends = { - GPUBackend::OPENGL, GPUBackend::VULKAN, - }; - - std::vector allowed; - - for (GPUBackend backend : allBackends) { - if (g_Config.IsBackendEnabled(backend)) { - allowed.push_back(backend); - } - } - - return allowed; -} - --(void)addOpenRecentlyItem { - std::shared_ptr mainmenuLocalization = GetI18NCategory(I18NCat::MAINMENU); -#define MAINMENU_LOCALIZED(key) @(mainmenuLocalization->T_cstr(key)) - - NSMenuItem *openRecent = [[NSMenuItem alloc] initWithTitle:MAINMENU_LOCALIZED("Recent") action:nil keyEquivalent:@""]; - NSMenu *recentsMenu = [[NSMenu alloc] init]; - recentsMenu.delegate = self; - self.recentsMenu = recentsMenu; - [self rebuildRecentsMenu]; - - openRecent.submenu = recentsMenu; - [self.fileMenu addItem:openRecent]; -} - --(void)rebuildRecentsMenu { - [self.recentsMenu removeAllItems]; - for (const auto &file : g_recentFiles.GetRecentFiles()) { - std::string filename = Path(file).GetFilename(); - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@(filename.c_str()) action:@selector(openRecentItem:) keyEquivalent:@""]; - item.target = self; - item.representedObject = @(file.c_str()); - [self.recentsMenu addItem:item]; - } -} - --(void)openRecentItem: (NSMenuItem *)item { - NSString *path = item.representedObject; - System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, path.UTF8String); -} - --(void)openSystemFileBrowser { - DarwinDirectoryPanelCallback panelCallback = [] (bool succ, Path thePathChosen) { - if (succ) - System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, thePathChosen.c_str()); - }; - DarwinFileSystemServices::presentDirectoryPanel(panelCallback, /* allowFiles = */ true, /* allowDirectorites = */ true); -} - --(void)openMemstickFolder { - NSURL *memstickURL = [NSURL fileURLWithPath:@(g_Config.memStickDirectory.c_str())]; - [NSWorkspace.sharedWorkspace openURL:memstickURL]; -} - --(void)loadVSH { - System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, (g_Config.nandRootDirectory / "flash0/vsh/module/vshmain.prx").ToString()); -} - -- (void)dealloc { - [NSNotificationCenter.defaultCenter removeObserver:self]; -} - @end #ifdef __cplusplus