Files
ppsspp/SDL/CocoaBarItems.mm
T
Henrik RydgårdandClaude Opus 5.5 ecf9efcf84 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) <[email protected]>
2026-09-24 14:03:15 -06:00

573 lines
22 KiB
Plaintext

//
// CocoaBarItems.mm
// PPSSPP
//
// Created by Serena on 06/02/2023.
//
#import <Cocoa/Cocoa.h>
#import "PPSSPPAboutViewController.h"
#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
extern "C" {
#endif
extern bool g_TakeScreenshot;
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 <NSMenuDelegate, NSMenuItemValidation>
+(instancetype)sharedInstance;
-(void)setupAppBarItems;
@property (strong) NSMenu *recentsMenu;
@property (strong) NSMenu *saveStateSlotMenu;
@end
void initializeOSXExtras() {
[NSApplication.sharedApplication setDelegate:[PSPNSApplicationDelegate sharedAppDelegate]];
[[BarItemsManager sharedInstance] setupAppBarItems];
}
void OSXShowInFinder(const char *path) {
NSURL *url = [NSURL fileURLWithPath:@(path)];
[NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:@[url]];
}
void OSXOpenURL(const char *url) {
NSURL *nsURL = [NSURL URLWithString:@(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;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
stub = [BarItemsManager new];
});
return stub;
}
// 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 {
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];
}
NSString *windowMenuItemTitle = @"Window"; // Don't translate, we lookup this.
// Rearrange 'Window' to be behind 'Help'
for (NSMenuItem *item in mainMenu.itemArray) {
if ([item.title isEqualToString:windowMenuItemTitle]) {
[mainMenu removeItem:item];
// 'Help' is the last item in the bar
// so we can just use `mainMenu.numberOfItems - 1`
// as it's index
[mainMenu insertItem:item atIndex:mainMenu.numberOfItems - 1];
break;
}
}
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;
}
}
}
-(void)presentAboutMenu {
NSWindow *window = [NSWindow windowWithContentViewController:[PPSSPPAboutViewController new]];
window.title = @"PPSSPP";
window.titleVisibility = NSWindowTitleHidden;
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 ||
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];
}
-(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);
}
}];
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);
}
}];
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;
[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;
}
-(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);
}];
}
}
-(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 {
NSMenu *menu = [self newMenu:DesktopUI("Emulation")];
MenuCondition inGame = ^BOOL { return InGame(); };
[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;
return menu;
}
-(NSMenu *)makeDebugMenu {
NSMenu *menu = [self newMenu:DesktopUI("Debugging")];
MenuCondition inGame = ^BOOL { return InGame(); };
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];
[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];
[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];
[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];
[self addItem:DesktopUI("Reset Symbol Table") to:menu action:^{
g_symbolMap->Clear();
}].enabledWhen = inGame;
[self addSeparatorTo:menu];
[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];
[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;
return menu;
}
-(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];
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;
}
-(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<NSString *> *)allowedFileTypes {
NSOpenPanel *openPanel = [[NSOpenPanel alloc] init];
openPanel.allowedFileTypes = allowedFileTypes;
if ([openPanel runModal] == NSModalResponseOK) {
NSURL *urlWeWant = openPanel.URLs.firstObject;
if (urlWeWant) {
return urlWeWant;
}
}
return nil;
}
+(NSURL *)presentSavePanelWithDefaultFilename: (NSString *)filename {
NSSavePanel *savePanel = [[NSSavePanel alloc] init];
savePanel.nameFieldStringValue = filename;
if ([savePanel runModal] == NSModalResponseOK && savePanel.URL) {
return savePanel.URL;
}
return nil;
}
@end
#ifdef __cplusplus
}
#endif