pcsx2/common/CocoaTools.mm
chaoticgd aedc51e151
Some checks failed
🐧 Linux Builds / AppImage (push) Has been cancelled
🐧 Linux Builds / Flatpak (push) Has been cancelled
🍎 MacOS Builds / Defaults (push) Has been cancelled
🖥️ Windows Builds / Lint VS Project Files (push) Has been cancelled
🖥️ Windows Builds / CMake (push) Has been cancelled
🖥️ Windows Builds / SSE4 (push) Has been cancelled
🖥️ Windows Builds / AVX2 (push) Has been cancelled
Qt: Set QStyleHints colorScheme property properly
2025-10-23 20:17:23 -04:00

271 lines
8.2 KiB
Plaintext

// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#if ! __has_feature(objc_arc)
#error "Compile this with -fobjc-arc"
#endif
#include "CocoaTools.h"
#include "Console.h"
#include "HostSys.h"
#include "WindowInfo.h"
#include <dlfcn.h>
#include <mutex>
#include <vector>
#include <Cocoa/Cocoa.h>
#include <QuartzCore/QuartzCore.h>
// MARK: - Metal Layers
static NSString*_Nonnull NSStringFromStringView(std::string_view sv)
{
return [[NSString alloc] initWithBytes:sv.data() length:sv.size() encoding:NSUTF8StringEncoding];
}
bool CocoaTools::CreateMetalLayer(WindowInfo* wi)
{
if (![NSThread isMainThread])
{
bool ret;
dispatch_sync(dispatch_get_main_queue(), [&ret, wi]{ ret = CreateMetalLayer(wi); });
return ret;
}
CAMetalLayer* layer = [CAMetalLayer layer];
if (!layer)
{
Console.Error("Failed to create Metal layer.");
return false;
}
NSView* view = (__bridge NSView*)wi->window_handle;
[view setWantsLayer:YES];
[view setLayer:layer];
[layer setContentsScale:[[[view window] screen] backingScaleFactor]];
// Store the layer pointer, that way MoltenVK doesn't call [NSView layer] outside the main thread.
wi->surface_handle = (__bridge_retained void*)layer;
return true;
}
void CocoaTools::DestroyMetalLayer(WindowInfo* wi)
{
if (![NSThread isMainThread])
{
dispatch_sync_f(dispatch_get_main_queue(), wi, [](void* ctx){ DestroyMetalLayer(static_cast<WindowInfo*>(ctx)); });
return;
}
NSView* view = (__bridge NSView*)wi->window_handle;
CAMetalLayer* layer = (__bridge_transfer CAMetalLayer*)wi->surface_handle;
if (!layer)
return;
wi->surface_handle = nullptr;
[view setLayer:nil];
[view setWantsLayer:NO];
}
std::optional<float> CocoaTools::GetViewRefreshRate(const WindowInfo& wi)
{
if (![NSThread isMainThread])
{
std::optional<float> ret;
dispatch_sync(dispatch_get_main_queue(), [&ret, wi]{ ret = GetViewRefreshRate(wi); });
return ret;
}
std::optional<float> ret;
NSView* const view = (__bridge NSView*)wi.window_handle;
const u32 did = [[[[[view window] screen] deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
if (CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did))
{
ret = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
}
return ret;
}
// MARK: - Help menu
void CocoaTools::MarkHelpMenu(void* menu)
{
[NSApp setHelpMenu:(__bridge NSMenu*)menu];
}
// MARK: - Sound playback
bool Common::PlaySoundAsync(const char* path)
{
NSString* nspath = [[NSString alloc] initWithUTF8String:path];
NSSound* sound = [[NSSound alloc] initWithContentsOfFile:nspath byReference:YES];
return [sound play];
}
// MARK: - Updater
std::optional<std::string> CocoaTools::GetBundlePath()
{
std::optional<std::string> ret;
@autoreleasepool {
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (url)
ret = std::string([url fileSystemRepresentation]);
}
return ret;
}
std::optional<std::string> CocoaTools::GetNonTranslocatedBundlePath()
{
// See https://objective-see.com/blog/blog_0x15.html
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (!url)
return std::nullopt;
if (void* handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY))
{
auto IsTranslocatedURL = reinterpret_cast<Boolean(*)(CFURLRef path, bool* isTranslocated, CFErrorRef*__nullable error)>(dlsym(handle, "SecTranslocateIsTranslocatedURL"));
auto CreateOriginalPathForURL = reinterpret_cast<CFURLRef __nullable(*)(CFURLRef translocatedPath, CFErrorRef*__nullable error)>(dlsym(handle, "SecTranslocateCreateOriginalPathForURL"));
bool is_translocated = false;
if (IsTranslocatedURL)
IsTranslocatedURL((__bridge CFURLRef)url, &is_translocated, nullptr);
if (is_translocated)
{
if (CFURLRef actual = CreateOriginalPathForURL((__bridge CFURLRef)url, nullptr))
url = (__bridge_transfer NSURL*)actual;
}
dlclose(handle);
}
return std::string([url fileSystemRepresentation]);
}
std::optional<std::string> CocoaTools::MoveToTrash(std::string_view file)
{
NSURL* url = [NSURL fileURLWithPath:NSStringFromStringView(file)];
NSURL* new_url;
if (![[NSFileManager defaultManager] trashItemAtURL:url resultingItemURL:&new_url error:nil])
return std::nullopt;
return std::string([new_url fileSystemRepresentation]);
}
bool CocoaTools::DelayedLaunch(std::string_view file)
{
@autoreleasepool {
NSTask* task = [NSTask new];
[task setExecutableURL:[NSURL fileURLWithPath:@"/bin/sh"]];
[task setEnvironment:@{
@"WAITPID": [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]],
@"LAUNCHAPP": NSStringFromStringView(file),
}];
[task setArguments:@[@"-c", @"while /bin/ps -p $WAITPID > /dev/null; do /bin/sleep 0.1; done; exec /usr/bin/open \"$LAUNCHAPP\";"]];
return [task launchAndReturnError:nil];
}
}
// MARK: - Directory Services
bool CocoaTools::ShowInFinder(std::string_view file)
{
return [[NSWorkspace sharedWorkspace] selectFile:NSStringFromStringView(file)
inFileViewerRootedAtPath:@""];
}
std::optional<std::string> CocoaTools::GetResourcePath()
{ @autoreleasepool {
if (NSBundle* bundle = [NSBundle mainBundle])
{
NSString* rsrc = [bundle resourcePath];
NSString* root = [bundle bundlePath];
if ([rsrc isEqualToString:root])
rsrc = [rsrc stringByAppendingString:@"/resources"];
return [rsrc UTF8String];
}
return std::nullopt;
}}
// MARK: - GSRunner
void* CocoaTools::CreateWindow(std::string_view title, u32 width, u32 height)
{
if (!NSApp)
{
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
}
constexpr NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
NSScreen* mainScreen = [NSScreen mainScreen];
// Center the window on the screen, because why not
NSRect screenFrame = [mainScreen frame];
NSRect viewFrame = screenFrame;
viewFrame.size = NSMakeSize(width, height);
viewFrame.origin.x += (screenFrame.size.width - viewFrame.size.width) / 2;
viewFrame.origin.y += (screenFrame.size.height - viewFrame.size.height) / 2;
NSWindow* window = [[NSWindow alloc]
initWithContentRect:viewFrame
styleMask:style
backing:NSBackingStoreBuffered
defer:NO];
[window setTitle:NSStringFromStringView(title)];
[window makeKeyAndOrderFront:window];
return (__bridge_retained void*)window;
}
void CocoaTools::DestroyWindow(void* window)
{
(void)(__bridge_transfer NSWindow*)window;
}
void CocoaTools::GetWindowInfoFromWindow(WindowInfo* wi, void* cf_window)
{
if (cf_window)
{
NSWindow* window = (__bridge NSWindow*)cf_window;
float scale = [window backingScaleFactor];
NSView* view = [window contentView];
NSRect dims = [view frame];
wi->type = WindowInfo::Type::MacOS;
wi->window_handle = (__bridge void*)view;
wi->surface_width = dims.size.width * scale;
wi->surface_height = dims.size.height * scale;
wi->surface_scale = scale;
}
else
{
wi->type = WindowInfo::Type::Surfaceless;
}
}
static constexpr short STOP_EVENT_LOOP = 0x100;
void CocoaTools::RunCocoaEventLoop(bool forever)
{
NSDate* end = forever ? [NSDate distantFuture] : [NSDate distantPast];
[NSApplication sharedApplication]; // Ensure NSApp is initialized
while (true)
{ @autoreleasepool {
NSEvent* ev = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:end
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (!ev || ([ev type] == NSEventTypeApplicationDefined && [ev subtype] == STOP_EVENT_LOOP))
break;
[NSApp sendEvent:ev];
}}
}
void CocoaTools::StopMainThreadEventLoop()
{ @autoreleasepool {
NSEvent* ev = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:{}
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:STOP_EVENT_LOOP
data1:0
data2:0];
[NSApp postEvent:ev atStart:NO];
}}