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