// Copyright 2023 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinNoGUI/Platform.h" #include "Common/MsgHandler.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" #include "Core/State.h" #include "VideoCommon/Present.h" #include "VideoCommon/RenderBase.h" #include #include #include #include #include #include #include #include #include @interface Application : NSApplication @property Platform* platform; - (void)shutdown; - (void)togglePause; - (void)saveScreenShot; - (void)loadLastSaved; - (void)undoLoadState; - (void)undoSaveState; - (void)loadState:(id)sender; - (void)saveState:(id)sender; @end @implementation Application - (void)shutdown; { [self platform]->RequestShutdown(); [self stop:nil]; } - (void)togglePause { if (Core::GetState() == Core::State::Running) Core::SetState(Core::State::Paused); else Core::SetState(Core::State::Running); } - (void)saveScreenShot { Core::SaveScreenShot(); } - (void)loadLastSaved { State::LoadLastSaved(); } - (void)undoLoadState { State::UndoLoadState(); } - (void)undoSaveState { State::UndoSaveState(); } - (void)loadState:(id)sender { State::Load([sender tag]); } - (void)saveState:(id)sender { State::Save([sender tag]); } @end @interface AppDelegate : NSObject @property(readonly) Platform* platform; - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender; - (id)initWithPlatform:(Platform*)platform; @end @implementation AppDelegate - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender { return YES; } - (id)initWithPlatform:(Platform*)platform { self = [super init]; if (self) { _platform = platform; } return self; } @end @interface WindowDelegate : NSObject - (void)windowDidResize:(NSNotification*)notification; @end @implementation WindowDelegate - (void)windowDidResize:(NSNotification*)notification { if (g_presenter) g_presenter->ResizeSurface(); } @end namespace { class PlatformMacOS : public Platform { public: ~PlatformMacOS() override; bool Init() override; void SetTitle(const std::string& title) override; void MainLoop() override; WindowSystemInfo GetWindowSystemInfo() const override; private: void ProcessEvents(); void UpdateWindowPosition(); void HandleSaveStates(NSUInteger key, NSUInteger flags); void SetupMenu(); NSRect m_window_rect; NSWindow* m_window; NSMenu* menuBar; AppDelegate* m_app_delegate; WindowDelegate* m_window_delegate; int m_window_x = Config::Get(Config::MAIN_RENDER_WINDOW_XPOS); int m_window_y = Config::Get(Config::MAIN_RENDER_WINDOW_YPOS); unsigned int m_window_width = Config::Get(Config::MAIN_RENDER_WINDOW_WIDTH); unsigned int m_window_height = Config::Get(Config::MAIN_RENDER_WINDOW_HEIGHT); bool m_window_fullscreen = Config::Get(Config::MAIN_FULLSCREEN); }; PlatformMacOS::~PlatformMacOS() { [m_window close]; } bool PlatformMacOS::Init() { [Application sharedApplication]; m_app_delegate = [[AppDelegate alloc] initWithPlatform:this]; [NSApp setDelegate:m_app_delegate]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setPlatform:this]; [Application.sharedApplication finishLaunching]; unsigned long styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable; m_window_rect = CGRectMake(m_window_x, m_window_y, m_window_width, m_window_height); m_window = [NSWindow alloc]; m_window = [m_window initWithContentRect:m_window_rect styleMask:styleMask backing:NSBackingStoreBuffered defer:NO]; m_window_delegate = [[WindowDelegate alloc] init]; [m_window setDelegate:m_window_delegate]; NSNotificationCenter* c = [NSNotificationCenter defaultCenter]; [c addObserver:NSApp selector:@selector(shutdown) name:NSWindowWillCloseNotification object:m_window]; if (m_window == nil) { NSLog(@"Window is %@\n", m_window); return false; } if (Config::Get(Config::MAIN_SHOW_CURSOR) == Config::ShowCursor::Never) [NSCursor hide]; if (Config::Get(Config::MAIN_FULLSCREEN)) { m_window_fullscreen = true; [m_window toggleFullScreen:m_window]; } [m_window makeKeyAndOrderFront:NSApp]; [m_window makeMainWindow]; [NSApp activateIgnoringOtherApps:YES]; [m_window setTitle:@"Dolphin-emu-nogui"]; SetupMenu(); return true; } void PlatformMacOS::SetTitle(const std::string& title) { @autoreleasepool { NSWindow* window = m_window; NSString* str = [NSString stringWithUTF8String:title.c_str()]; dispatch_async(dispatch_get_main_queue(), ^{ [window setTitle:str]; }); } } void PlatformMacOS::MainLoop() { while (IsRunning()) { UpdateRunningFlag(); Core::HostDispatchJobs(); ProcessEvents(); UpdateWindowPosition(); } } WindowSystemInfo PlatformMacOS::GetWindowSystemInfo() const { @autoreleasepool { WindowSystemInfo wsi; wsi.type = WindowSystemType::MacOS; wsi.render_window = (void*)CFBridgingRetain([m_window contentView]); wsi.render_surface = wsi.render_window; return wsi; } } void PlatformMacOS::ProcessEvents() { @autoreleasepool { NSDate* expiration = [NSDate dateWithTimeIntervalSinceNow:1]; NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:YES]; [NSApp sendEvent:event]; // Need to update if m_window becomes fullscreen m_window_fullscreen = [m_window styleMask] & NSWindowStyleMaskFullScreen; if ([m_window isMainWindow]) { m_window_focus = true; if (Config::Get(Config::MAIN_SHOW_CURSOR) == Config::ShowCursor::Never && Core::GetState() != Core::State::Paused) [NSCursor unhide]; } else { m_window_focus = false; if (Config::Get(Config::MAIN_SHOW_CURSOR) == Config::ShowCursor::Never) [NSCursor hide]; } } } void PlatformMacOS::UpdateWindowPosition() { if (m_window_fullscreen) return; NSRect win = [m_window frame]; m_window_x = win.origin.x; m_window_y = win.origin.y; m_window_width = win.size.width; m_window_height = win.size.height; } void PlatformMacOS::SetupMenu() { @autoreleasepool { menuBar = [NSMenu new]; NSMenu* appMenu = [NSMenu new]; NSMenu* stateMenu = [[NSMenu alloc] initWithTitle:@"States"]; NSMenu* loadStateMenu = [[NSMenu alloc] initWithTitle:@"Load"]; NSMenu* saveStateMenu = [[NSMenu alloc] initWithTitle:@"Save"]; NSMenu* miscMenu = [[NSMenu alloc] initWithTitle:@"Misc"]; NSMenuItem* appMenuItem = [NSMenuItem new]; NSMenuItem* miscMenuItem = [NSMenuItem new]; NSMenuItem* stateMenuItem = [NSMenuItem new]; NSMenuItem* loadStateItem = [[NSMenuItem alloc] initWithTitle:@"Load" action:nil keyEquivalent:@""]; NSMenuItem* saveStateItem = [[NSMenuItem alloc] initWithTitle:@"Save" action:nil keyEquivalent:@""]; [menuBar addItem:appMenuItem]; [menuBar addItem:stateMenuItem]; [menuBar addItem:miscMenuItem]; // Quit NSString* quitTitle = [@"Quit " stringByAppendingString:@"dolphin-emu-nogui"]; NSMenuItem* quitMenuItem = [[NSMenuItem alloc] initWithTitle:quitTitle action:@selector(shutdown) keyEquivalent:@"q"]; // Fullscreen NSString* fullScreenItemTitle = @"Toggle Fullscreen"; NSMenuItem* fullScreenItem = [[NSMenuItem alloc] initWithTitle:fullScreenItemTitle action:@selector(toggleFullScreen:) keyEquivalent:@"f"]; [fullScreenItem setKeyEquivalentModifierMask:NSEventModifierFlagFunction]; // Screenshot NSString* ScreenShotTitle = @"Take Screenshot"; unichar c = NSF9FunctionKey; NSString* f9 = [NSString stringWithCharacters:&c length:1]; NSMenuItem* ScreenShotItem = [[NSMenuItem alloc] initWithTitle:ScreenShotTitle action:@selector(saveScreenShot) keyEquivalent:f9]; [ScreenShotItem setKeyEquivalentModifierMask:NSEventModifierFlagFunction]; // Pause game NSString* pauseTitle = @"Toggle pause"; c = NSF10FunctionKey; NSString* f10 = [NSString stringWithCharacters:&c length:1]; NSMenuItem* pauseItem = [[NSMenuItem alloc] initWithTitle:pauseTitle action:@selector(togglePause) keyEquivalent:f10]; [pauseItem setKeyEquivalentModifierMask:NSEventModifierFlagFunction]; // Load last save NSString* loadLastTitle = @"Load Last Saved"; c = NSF11FunctionKey; NSString* f11 = [NSString stringWithCharacters:&c length:1]; NSMenuItem* loadLastItem = [[NSMenuItem alloc] initWithTitle:loadLastTitle action:@selector(loadLastSaved) keyEquivalent:f11]; [loadLastItem setKeyEquivalentModifierMask:NSEventModifierFlagFunction]; // Undo Load State NSString* undoLoadTitle = @"Undo Load"; c = NSF12FunctionKey; NSString* f12 = [NSString stringWithCharacters:&c length:1]; NSMenuItem* undoLoadItem = [[NSMenuItem alloc] initWithTitle:undoLoadTitle action:@selector(undoLoadState) keyEquivalent:f12]; [undoLoadItem setKeyEquivalentModifierMask:NSEventModifierFlagShift]; // Undo Save State NSString* undoSaveTitle = @"Undo Save"; NSMenuItem* undoSaveItem = [[NSMenuItem alloc] initWithTitle:undoSaveTitle action:@selector(undoSaveState) keyEquivalent:f12]; [undoSaveItem setKeyEquivalentModifierMask:NSEventModifierFlagFunction]; // Load and Save States for (unichar i = NSF1FunctionKey; i <= NSF8FunctionKey; i++) { NSInteger stateNum = i - NSF1FunctionKey + 1; NSString* lstateTitle = [NSString stringWithFormat:@"Load State %ld", (long)stateNum]; c = i; NSString* t = [NSString stringWithCharacters:&c length:1]; NSMenuItem* lstateItem = [[NSMenuItem alloc] initWithTitle:lstateTitle action:@selector(loadState:) keyEquivalent:t]; [lstateItem setTag:stateNum]; [lstateItem setKeyEquivalentModifierMask:NSEventModifierFlagFunction]; [loadStateMenu addItem:lstateItem]; NSString* sstateTitle = [NSString stringWithFormat:@"Save State %ld", (long)stateNum]; c = i; NSMenuItem* sstateItem = [[NSMenuItem alloc] initWithTitle:sstateTitle action:@selector(saveState:) keyEquivalent:t]; [sstateItem setKeyEquivalentModifierMask:NSEventModifierFlagShift]; [sstateItem setTag:stateNum]; [saveStateMenu addItem:sstateItem]; } // App Main menu [appMenu addItem:quitMenuItem]; // State Menu [loadStateItem setSubmenu:loadStateMenu]; [saveStateItem setSubmenu:saveStateMenu]; [stateMenu addItem:loadLastItem]; [stateMenu addItem:undoLoadItem]; [stateMenu addItem:undoSaveItem]; [stateMenu addItem:loadStateItem]; [stateMenu addItem:saveStateItem]; // Misc Menu [miscMenu addItem:fullScreenItem]; [miscMenu addItem:ScreenShotItem]; [miscMenu addItem:pauseItem]; [appMenuItem setSubmenu:appMenu]; [stateMenuItem setSubmenu:stateMenu]; [miscMenuItem setSubmenu:miscMenu]; [NSApp setMainMenu:menuBar]; } } } // namespace std::unique_ptr Platform::CreateMacOSPlatform() { return std::make_unique(); }