// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "cocoa_nogui_platform.h" #include "cocoa_key_names.h" #include "nogui_host.h" #include "core/host.h" #include "util/cocoa_tools.h" #include "util/imgui_manager.h" #include "common/log.h" #include "common/scoped_guard.h" #include "common/string_util.h" #include "common/threading.h" Log_SetChannel(CocoaNoGUIPlatform); constexpr NSWindowStyleMask WINDOWED_STYLE = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; @implementation CocoaNoGUIView - (BOOL)acceptsFirstResponder { return YES; } - (BOOL)canBecomeKeyView { return YES; } - (void)mouseDown:(NSEvent *)event { NoGUIHost::ProcessPlatformMouseButtonEvent(0, true); } - (void)rightMouseDown:(NSEvent *)event { NoGUIHost::ProcessPlatformMouseButtonEvent(1, true); } - (void)otherMouseDown:(NSEvent *)event { NoGUIHost::ProcessPlatformMouseButtonEvent(static_cast(event.buttonNumber), true); } - (void)mouseUp:(NSEvent *)event { NoGUIHost::ProcessPlatformMouseButtonEvent(0, false); } - (void)rightMouseUp:(NSEvent *)event { NoGUIHost::ProcessPlatformMouseButtonEvent(1, false); } - (void)otherMouseUp:(NSEvent *)event { NoGUIHost::ProcessPlatformMouseButtonEvent(static_cast(event.buttonNumber), false); } - (void)mouseMoved:(NSEvent *)event { // Flip for lower-left origin. const NSView* contentView = self; const NSPoint pt = [contentView convertPointToBacking:[event locationInWindow]]; const NSSize size = [contentView convertSizeToBacking:contentView.frame.size]; const float local_x = pt.x; const float local_y = size.height - pt.y; NoGUIHost::ProcessPlatformMouseMoveEvent(local_x, local_y); } - (void)keyDown:(NSEvent *)event { [super keyDown:event]; if (ImGuiManager::WantsTextInput() && event.characters && event.characters.length > 0) { ImGuiManager::AddTextInput([event.characters UTF8String]); } if (!event.isARepeat) NoGUIHost::ProcessPlatformKeyEvent(static_cast(event.keyCode), true); } - (void)keyUp:(NSEvent *)event { [super keyUp:event]; NoGUIHost::ProcessPlatformKeyEvent(static_cast(event.keyCode), false); } - (void)windowDidEndLiveResize:(NSNotification *)notif { const NSSize size = [self convertSizeToBacking:self.frame.size]; NoGUIHost::ProcessPlatformWindowResize(static_cast(size.width), static_cast(size.height), 1.0f); } @end CocoaNoGUIPlatform::CocoaNoGUIPlatform() = default; CocoaNoGUIPlatform::~CocoaNoGUIPlatform() { if (m_window) { [m_window release]; m_window = nil; } } bool CocoaNoGUIPlatform::Initialize() { [NSApplication sharedApplication]; // Needed for keyboard in put. const ProcessSerialNumber psn = {0, kCurrentProcess}; TransformProcessType(&psn, kProcessTransformToForegroundApplication); return true; } void CocoaNoGUIPlatform::ReportError(const std::string_view& title, const std::string_view& message) { if (![NSThread isMainThread]) { dispatch_sync(dispatch_get_main_queue(), [this, &title, &message]() { ReportError(title, message); }); return; } @autoreleasepool { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText: CocoaTools::StringViewToNSString(title)]; [alert setInformativeText: CocoaTools::StringViewToNSString(message)]; [alert runModal]; } } bool CocoaNoGUIPlatform::ConfirmMessage(const std::string_view& title, const std::string_view& message) { if (![NSThread isMainThread]) { bool result = false; dispatch_sync(dispatch_get_main_queue(), [this, &title, &message, &result]() { result = ConfirmMessage(title, message); }); return result; } @autoreleasepool { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText: CocoaTools::StringViewToNSString(title)]; [alert setInformativeText: CocoaTools::StringViewToNSString(message)]; [alert addButtonWithTitle:@"Yes"]; [alert addButtonWithTitle:@"No"]; return ([alert runModal] == 0); } } void CocoaNoGUIPlatform::SetDefaultConfig(SettingsInterface& si) { // noop } bool CocoaNoGUIPlatform::CreatePlatformWindow(std::string title) { @autoreleasepool { s32 window_x, window_y, window_width, window_height; const bool has_window_geom = NoGUIHost::GetSavedPlatformWindowGeometry(&window_x, &window_y, &window_width, &window_height); if (!has_window_geom) { window_width = DEFAULT_WINDOW_WIDTH; window_height = DEFAULT_WINDOW_HEIGHT; } m_window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0.0f, 0.0f, static_cast(window_width), static_cast(window_height)) styleMask:WINDOWED_STYLE backing:NSBackingStoreBuffered defer:YES]; CocoaNoGUIView* view = [[[CocoaNoGUIView alloc] init] autorelease]; [m_window setDelegate:view]; [m_window setContentView:view]; if (!has_window_geom) [m_window center]; else [m_window setFrameOrigin:NSMakePoint(static_cast(window_x), static_cast(window_y))]; [m_window setTitle: [NSString stringWithUTF8String:title.c_str()]]; [m_window setAcceptsMouseMovedEvents:YES]; [m_window setReleasedWhenClosed:NO]; [m_window setIsVisible:TRUE]; [m_window makeKeyAndOrderFront:nil]; } if (m_fullscreen.load(std::memory_order_acquire)) SetFullscreen(true); return true; } bool CocoaNoGUIPlatform::HasPlatformWindow() const { return (m_window != NULL); } void CocoaNoGUIPlatform::DestroyPlatformWindow() { if (m_window == nil) return; const CGPoint frame_origin = m_window.frame.origin; const CGSize content_size = m_window.contentView.frame.size; if (!m_fullscreen.load(std::memory_order_acquire)) { NoGUIHost::SavePlatformWindowGeometry(static_cast(frame_origin.x), static_cast(frame_origin.y), static_cast(content_size.width), static_cast(content_size.height)); } [m_window close]; [m_window release]; m_window = nil; } std::optional CocoaNoGUIPlatform::GetPlatformWindowInfo() { if (m_window == nil) return std::nullopt; NSView* contentView = [m_window contentView]; const NSSize size = [contentView convertSizeToBacking:contentView.frame.size]; WindowInfo wi; wi.surface_width = static_cast(size.width); wi.surface_height = static_cast(size.height); wi.surface_scale = m_window_scale; wi.type = WindowInfo::Type::MacOS; wi.window_handle = static_cast(m_window.contentView); return wi; } void CocoaNoGUIPlatform::SetPlatformWindowTitle(std::string title) { dispatch_async(dispatch_get_main_queue(), [this, title = std::move(title)]() { if (!m_window) return; @autoreleasepool { [m_window setTitle: [NSString stringWithUTF8String:title.c_str()]]; } }); } std::optional CocoaNoGUIPlatform::ConvertHostKeyboardStringToCode(const std::string_view& str) { std::optional converted(CocoaKeyNames::GetKeyCodeForName(str)); return converted.has_value() ? std::optional(static_cast(converted.value())) : std::nullopt; } std::optional CocoaNoGUIPlatform::ConvertHostKeyboardCodeToString(u32 code) { const char* converted = CocoaKeyNames::GetKeyName(static_cast(code)); return converted ? std::optional(converted) : std::nullopt; } void CocoaNoGUIPlatform::RunMessageLoop() { [NSApp run]; } void CocoaNoGUIPlatform::ExecuteInMessageLoop(std::function func) { dispatch_async(dispatch_get_main_queue(), [func = std::move(func)]() { func(); }); } void CocoaNoGUIPlatform::QuitMessageLoop() { [NSApp stop:nil]; } void CocoaNoGUIPlatform::SetFullscreen(bool enabled) { Log_ErrorPrint("SetFullscreen() not implemented."); } bool CocoaNoGUIPlatform::RequestRenderWindowSize(s32 new_window_width, s32 new_window_height) { dispatch_async(dispatch_get_main_queue(), [this, new_window_width, new_window_height]() { if (!m_window) return; @autoreleasepool { [m_window setContentSize:NSMakeSize(static_cast(new_window_width), static_cast(new_window_height))]; } }); return true; } bool CocoaNoGUIPlatform::OpenURL(const std::string_view& url) { Log_ErrorPrint("OpenURL() not implemented."); return false; } bool CocoaNoGUIPlatform::CopyTextToClipboard(const std::string_view& text) { Log_ErrorPrint("CopyTextToClipboard() not implemented."); return false; } std::unique_ptr NoGUIPlatform::CreateCocoaPlatform() { std::unique_ptr ret(new CocoaNoGUIPlatform()); if (!ret->Initialize()) return {}; return ret; }