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