* Adding macOS FileSystemWatcher, this should allow files to be monitored and update like the other OSs * Reworked how macOS launches processes to use NSTask which just deals with escaped and unescaped paths better * Made a change to the ScriptsBuilder::RunBuildTool, this was adding the escaped values to the path, in reality it should be up to the underlying OS to make sure things are properly escaped, so removed those as they just end up causing issues. Also instead of appending the args to the fileName we just properly use the Arguments variable on the CreateProcessSettings * No longer use open in order to show files in the finder, we use the proper method selectFile * made a slight cleanup change to the MacPlatform Tick function * Added ToNSString functions just to make that easier * Added a ParseArguments function that will take a string and turn it into an array for NSTask
554 lines
16 KiB
C++
554 lines
16 KiB
C++
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
|
|
|
#if PLATFORM_MAC
|
|
|
|
#include "MacPlatform.h"
|
|
#include "MacWindow.h"
|
|
#include "Engine/Platform/Apple/AppleUtils.h"
|
|
#include "Engine/Core/Log.h"
|
|
#include "Engine/Core/Types/Guid.h"
|
|
#include "Engine/Core/Types/String.h"
|
|
#include "Engine/Core/Collections/HashFunctions.h"
|
|
#include "Engine/Core/Collections/Array.h"
|
|
#include "Engine/Core/Collections/Dictionary.h"
|
|
#include "Engine/Core/Collections/HashFunctions.h"
|
|
#include "Engine/Core/Math/Math.h"
|
|
#include "Engine/Core/Math/Rectangle.h"
|
|
#include "Engine/Core/Math/Color32.h"
|
|
#include "Engine/Platform/CPUInfo.h"
|
|
#include "Engine/Platform/MemoryStats.h"
|
|
#include "Engine/Platform/MessageBox.h"
|
|
#include "Engine/Platform/StringUtils.h"
|
|
#include "Engine/Platform/WindowsManager.h"
|
|
#include "Engine/Platform/Clipboard.h"
|
|
#include "Engine/Platform/IGuiData.h"
|
|
#include "Engine/Platform/Base/PlatformUtils.h"
|
|
#include "Engine/Platform/CreateProcessSettings.h"
|
|
#include "Engine/Utilities/StringConverter.h"
|
|
#include "Engine/Threading/Threading.h"
|
|
#include "Engine/Engine/Engine.h"
|
|
#include "Engine/Engine/CommandLine.h"
|
|
#include "Engine/Input/Input.h"
|
|
#include "Engine/Input/Mouse.h"
|
|
#include "Engine/Input/Keyboard.h"
|
|
#include <unistd.h>
|
|
#include <cstdint>
|
|
#include <stdlib.h>
|
|
#include <sys/sysctl.h>
|
|
#include <sys/time.h>
|
|
#include <mach/mach_time.h>
|
|
#include <mach-o/dyld.h>
|
|
#include <uuid/uuid.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <CoreGraphics/CoreGraphics.h>
|
|
#include <SystemConfiguration/SystemConfiguration.h>
|
|
#include <IOKit/IOKitLib.h>
|
|
#include <Cocoa/Cocoa.h>
|
|
#include <dlfcn.h>
|
|
#if CRASH_LOG_ENABLE
|
|
#include <execinfo.h>
|
|
#endif
|
|
|
|
Guid DeviceId;
|
|
String ComputerName;
|
|
|
|
DialogResult MessageBox::Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
|
{
|
|
if (CommandLine::Options.Headless)
|
|
return DialogResult::None;
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
ASSERT(alert);
|
|
switch (buttons)
|
|
{
|
|
case MessageBoxButtons::AbortRetryIgnore:
|
|
[alert addButtonWithTitle:@"Abort"];
|
|
[alert addButtonWithTitle:@"Retry"];
|
|
[alert addButtonWithTitle:@"Ignore"];
|
|
break;
|
|
case MessageBoxButtons::OK:
|
|
[alert addButtonWithTitle:@"OK"];
|
|
break;
|
|
case MessageBoxButtons::OKCancel:
|
|
[alert addButtonWithTitle:@"OK"];
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
break;
|
|
case MessageBoxButtons::RetryCancel:
|
|
[alert addButtonWithTitle:@"Retry"];
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
break;
|
|
case MessageBoxButtons::YesNo:
|
|
[alert addButtonWithTitle:@"Yes"];
|
|
[alert addButtonWithTitle:@"No"];
|
|
break;
|
|
case MessageBoxButtons::YesNoCancel:
|
|
[alert addButtonWithTitle:@"Yes"];
|
|
[alert addButtonWithTitle:@"No"];
|
|
[alert addButtonWithTitle:@"Cancel"];
|
|
break;
|
|
}
|
|
switch (icon)
|
|
{
|
|
case MessageBoxIcon::Information:
|
|
[alert setAlertStyle:NSAlertStyleCritical];
|
|
break;
|
|
case MessageBoxIcon::Error:
|
|
case MessageBoxIcon::Stop:
|
|
[alert setAlertStyle:NSAlertStyleInformational];
|
|
break;
|
|
case MessageBoxIcon::Warning:
|
|
[alert setAlertStyle:NSAlertStyleWarning];
|
|
break;
|
|
}
|
|
[alert setMessageText:(NSString*)AppleUtils::ToString(caption)];
|
|
[alert setInformativeText:(NSString*)AppleUtils::ToString(text)];
|
|
NSInteger button = [alert runModal];
|
|
DialogResult result = DialogResult::OK;
|
|
switch (buttons)
|
|
{
|
|
case MessageBoxButtons::AbortRetryIgnore:
|
|
if (button == NSAlertFirstButtonReturn)
|
|
result = DialogResult::Abort;
|
|
else if (button == NSAlertSecondButtonReturn)
|
|
result = DialogResult::Retry;
|
|
else
|
|
result = DialogResult::Ignore;
|
|
break;
|
|
case MessageBoxButtons::OK:
|
|
result = DialogResult::OK;
|
|
break;
|
|
case MessageBoxButtons::OKCancel:
|
|
if (button == NSAlertFirstButtonReturn)
|
|
result = DialogResult::OK;
|
|
else
|
|
result = DialogResult::Cancel;
|
|
break;
|
|
case MessageBoxButtons::RetryCancel:
|
|
if (button == NSAlertFirstButtonReturn)
|
|
result = DialogResult::Retry;
|
|
else
|
|
result = DialogResult::Cancel;
|
|
break;
|
|
case MessageBoxButtons::YesNo:
|
|
if (button == NSAlertFirstButtonReturn)
|
|
result = DialogResult::Yes;
|
|
else
|
|
result = DialogResult::No;
|
|
break;
|
|
case MessageBoxButtons::YesNoCancel:
|
|
if (button == NSAlertFirstButtonReturn)
|
|
result = DialogResult::Yes;
|
|
else if (button == NSAlertSecondButtonReturn)
|
|
result = DialogResult::No;
|
|
else
|
|
result = DialogResult::Cancel;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Float2 AppleUtils::PosToCoca(const Float2& pos)
|
|
{
|
|
// MacOS uses y-coordinate starting at the bottom of the screen
|
|
Float2 result = pos;// / ApplePlatform::ScreenScale;
|
|
result.Y *= -1;
|
|
result += GetScreensOrigin();
|
|
return result;
|
|
}
|
|
|
|
Float2 AppleUtils::CocaToPos(const Float2& pos)
|
|
{
|
|
// MacOS uses y-coordinate starting at the bottom of the screen
|
|
Float2 result = pos;// * ApplePlatform::ScreenScale;
|
|
result -= GetScreensOrigin();
|
|
result.Y *= -1;
|
|
return result;// * ApplePlatform::ScreenScale;
|
|
}
|
|
|
|
Float2 AppleUtils::GetScreensOrigin()
|
|
{
|
|
Float2 result = Float2::Zero;
|
|
NSArray* screenArray = [NSScreen screens];
|
|
for (NSUInteger i = 0; i < [screenArray count]; i++)
|
|
{
|
|
NSRect rect = [[screenArray objectAtIndex:i] frame];
|
|
Float2 pos(rect.origin.x, rect.origin.y + rect.size.height);
|
|
pos *= ApplePlatform::ScreenScale;
|
|
if (pos.X < result.X)
|
|
result.X = pos.X;
|
|
if (pos.Y > result.Y)
|
|
result.Y = pos.Y;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void MacClipboard::Clear()
|
|
{
|
|
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
|
[pasteboard clearContents];
|
|
}
|
|
|
|
void MacClipboard::SetText(const StringView& text)
|
|
{
|
|
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
|
[pasteboard clearContents];
|
|
[pasteboard writeObjects:[NSArray arrayWithObject:(NSString*)AppleUtils::ToString(text)]];
|
|
}
|
|
|
|
void MacClipboard::SetRawData(const Span<byte>& data)
|
|
{
|
|
}
|
|
|
|
void MacClipboard::SetFiles(const Array<String>& files)
|
|
{
|
|
}
|
|
|
|
String MacClipboard::GetText()
|
|
{
|
|
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
|
NSArray* classes = [NSArray arrayWithObject:[NSString class]];
|
|
NSDictionary* options = [NSDictionary dictionary];
|
|
if (![pasteboard canReadObjectForClasses:classes options:options])
|
|
return String::Empty;
|
|
NSArray* objects = [pasteboard readObjectsForClasses:classes options:options];
|
|
return AppleUtils::ToString((CFStringRef)[objects objectAtIndex:0]);
|
|
}
|
|
|
|
Array<byte> MacClipboard::GetRawData()
|
|
{
|
|
return Array<byte>();
|
|
}
|
|
|
|
Array<String> MacClipboard::GetFiles()
|
|
{
|
|
return Array<String>();
|
|
}
|
|
|
|
class MacKeyboard : public Keyboard
|
|
{
|
|
public:
|
|
explicit MacKeyboard()
|
|
: Keyboard()
|
|
{
|
|
}
|
|
};
|
|
|
|
class MacMouse : public Mouse
|
|
{
|
|
public:
|
|
explicit MacMouse()
|
|
: Mouse()
|
|
{
|
|
}
|
|
|
|
public:
|
|
|
|
// [Mouse]
|
|
void SetMousePosition(const Float2& newPosition) final override
|
|
{
|
|
MacPlatform::SetMousePosition(newPosition);
|
|
|
|
OnMouseMoved(newPosition);
|
|
}
|
|
};
|
|
|
|
bool MacPlatform::Init()
|
|
{
|
|
if (ApplePlatform::Init())
|
|
return true;
|
|
|
|
// Get device id
|
|
{
|
|
io_registry_entry_t ioRegistryRoot = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/");
|
|
CFStringRef deviceUuid = (CFStringRef)IORegistryEntryCreateCFProperty(ioRegistryRoot, CFSTR(kIOPlatformUUIDKey), kCFAllocatorDefault, 0);
|
|
IOObjectRelease(ioRegistryRoot);
|
|
String uuidStr = AppleUtils::ToString(deviceUuid);
|
|
Guid::Parse(uuidStr, DeviceId);
|
|
CFRelease(deviceUuid);
|
|
}
|
|
|
|
// Get computer name
|
|
{
|
|
CFStringRef computerName = SCDynamicStoreCopyComputerName(nullptr, nullptr);
|
|
ComputerName = AppleUtils::ToString(computerName);
|
|
CFRelease(computerName);
|
|
}
|
|
|
|
// Find the maximum scale of the display to handle high-dpi displays scaling factor
|
|
{
|
|
NSArray* screenArray = [NSScreen screens];
|
|
for (int32 i = 0; i < (int32)[screenArray count]; i++)
|
|
{
|
|
if ([[screenArray objectAtIndex:i] respondsToSelector:@selector(backingScaleFactor)])
|
|
{
|
|
ScreenScale = Math::Max(ScreenScale, (float)[[screenArray objectAtIndex:i] backingScaleFactor]);
|
|
}
|
|
}
|
|
CustomDpiScale *= ScreenScale;
|
|
}
|
|
|
|
// Init application
|
|
[NSApplication sharedApplication];
|
|
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
|
|
// Init main menu
|
|
NSMenu* mainMenu = [[[NSMenu alloc] initWithTitle:@""] autorelease];
|
|
[NSApp setMainMenu:mainMenu];
|
|
// TODO: expose main menu for app (eg. to be used by Game or Editor on macOS-only)
|
|
|
|
Input::Mouse = New<MacMouse>();
|
|
Input::Keyboard = New<MacKeyboard>();
|
|
|
|
return false;
|
|
}
|
|
|
|
void MacPlatform::LogInfo()
|
|
{
|
|
ApplePlatform::LogInfo();
|
|
|
|
char str[250];
|
|
size_t strSize = sizeof(str);
|
|
if (sysctlbyname("kern.osrelease", str, &strSize, nullptr, 0) != 0)
|
|
str[0] = 0;
|
|
String osRelease(str);
|
|
if (sysctlbyname("kern.osproductversion", str, &strSize, nullptr, 0) != 0)
|
|
str[0] = 0;
|
|
String osProductVer(str);
|
|
LOG(Info, "macOS {1} (kernel {0})", osRelease, osProductVer);
|
|
}
|
|
|
|
void MacPlatform::BeforeRun()
|
|
{
|
|
[NSApp finishLaunching];
|
|
}
|
|
|
|
void MacPlatform::Tick()
|
|
{
|
|
// Process system events
|
|
NSEvent* event = nil;
|
|
do
|
|
{
|
|
NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny untilDate: nil inMode: NSDefaultRunLoopMode dequeue: YES];
|
|
if (event)
|
|
{
|
|
[NSApp sendEvent:event];
|
|
}
|
|
|
|
} while(event);
|
|
|
|
ApplePlatform::Tick();
|
|
}
|
|
|
|
int32 MacPlatform::GetDpi()
|
|
{
|
|
CGDirectDisplayID mainDisplay = CGMainDisplayID();
|
|
CGSize size = CGDisplayScreenSize(mainDisplay);
|
|
float wide = (float)CGDisplayPixelsWide(mainDisplay);
|
|
float dpi = (wide * 25.4f) / size.width;
|
|
return Math::Max(dpi, 72.0f);
|
|
}
|
|
|
|
Guid MacPlatform::GetUniqueDeviceId()
|
|
{
|
|
return DeviceId;
|
|
}
|
|
|
|
String MacPlatform::GetComputerName()
|
|
{
|
|
return ComputerName;
|
|
}
|
|
|
|
Float2 MacPlatform::GetMousePosition()
|
|
{
|
|
CGEventRef event = CGEventCreate(nullptr);
|
|
CGPoint cursor = CGEventGetLocation(event);
|
|
CFRelease(event);
|
|
return Float2((float)cursor.x, (float)cursor.y) * MacPlatform::ScreenScale;
|
|
}
|
|
|
|
void MacPlatform::SetMousePosition(const Float2& pos)
|
|
{
|
|
CGPoint cursor;
|
|
cursor.x = (CGFloat)(pos.X / MacPlatform::ScreenScale);
|
|
cursor.y = (CGFloat)(pos.Y / MacPlatform::ScreenScale);
|
|
CGWarpMouseCursorPosition(cursor);
|
|
CGAssociateMouseAndMouseCursorPosition(true);
|
|
}
|
|
|
|
Float2 MacPlatform::GetDesktopSize()
|
|
{
|
|
CGDirectDisplayID mainDisplay = CGMainDisplayID();
|
|
return Float2((float)CGDisplayPixelsWide(mainDisplay) * ScreenScale, (float)CGDisplayPixelsHigh(mainDisplay) * ScreenScale);
|
|
}
|
|
|
|
Rectangle GetDisplayBounds(CGDirectDisplayID display)
|
|
{
|
|
CGRect rect = CGDisplayBounds(display);
|
|
float screnScale = ApplePlatform::ScreenScale;
|
|
return Rectangle(rect.origin.x * screnScale, rect.origin.y * screnScale, rect.size.width * screnScale, rect.size.height * screnScale);
|
|
}
|
|
|
|
Rectangle MacPlatform::GetMonitorBounds(const Float2& screenPos)
|
|
{
|
|
CGPoint point;
|
|
point.x = screenPos.X;
|
|
point.y = screenPos.Y;
|
|
CGDirectDisplayID display;
|
|
uint32_t count = 0;
|
|
CGGetDisplaysWithPoint(point, 1, &display, &count);
|
|
if (count == 1)
|
|
return GetDisplayBounds(display);
|
|
return Rectangle(Float2::Zero, GetDesktopSize());
|
|
}
|
|
|
|
Rectangle MacPlatform::GetVirtualDesktopBounds()
|
|
{
|
|
CGDirectDisplayID displays[16];
|
|
uint32_t count = 0;
|
|
CGGetOnlineDisplayList(ARRAY_COUNT(displays), displays, &count);
|
|
if (count == 0)
|
|
return Rectangle(Float2::Zero, GetDesktopSize());
|
|
Rectangle result = GetDisplayBounds(displays[0]);
|
|
for (uint32_t i = 1; i < count; i++)
|
|
result = Rectangle::Union(result, GetDisplayBounds(displays[i]));
|
|
return result;
|
|
}
|
|
|
|
String MacPlatform::GetMainDirectory()
|
|
{
|
|
String path = StringUtils::GetDirectoryName(GetExecutableFilePath());
|
|
if (path.EndsWith(TEXT("/Contents/MacOS")))
|
|
{
|
|
// If running from executable in a package, go up to the Contents
|
|
path = path.Left(path.Length() - 6);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
Window* MacPlatform::CreateWindow(const CreateWindowSettings& settings)
|
|
{
|
|
return New<MacWindow>(settings);
|
|
}
|
|
|
|
int32 MacPlatform::CreateProcess(CreateProcessSettings& settings)
|
|
{
|
|
LOG(Info, "Command: {0} {1}", settings.FileName, settings.Arguments);
|
|
String cwd;
|
|
if (settings.WorkingDirectory.HasChars())
|
|
{
|
|
LOG(Info, "Working directory: {0}", settings.WorkingDirectory);
|
|
cwd = Platform::GetWorkingDirectory();
|
|
Platform::SetWorkingDirectory(settings.WorkingDirectory);
|
|
}
|
|
const bool captureStdOut = settings.LogOutput || settings.SaveOutput;
|
|
|
|
// Special case if filename points to the app package (use actual executable)
|
|
String exePath = settings.FileName;
|
|
{
|
|
NSString* processPath = (NSString*)AppleUtils::ToString(exePath);
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath: processPath])
|
|
{
|
|
NSString* appName = [[processPath lastPathComponent] stringByDeletingPathExtension];
|
|
processPath = [[NSWorkspace sharedWorkspace] fullPathForApplication:appName];
|
|
}
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath: processPath])
|
|
{
|
|
if([[NSWorkspace sharedWorkspace] isFilePackageAtPath: processPath])
|
|
{
|
|
NSBundle* bundle = [NSBundle bundleWithPath:processPath];
|
|
if (bundle != nil)
|
|
{
|
|
processPath = [bundle executablePath];
|
|
if (processPath != nil)
|
|
exePath = AppleUtils::ToString((CFStringRef)processPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NSTask *task = [[NSTask alloc] init];
|
|
task.launchPath = AppleUtils::ToNSString(exePath);
|
|
task.arguments = AppleUtils::ParseArguments(AppleUtils::ToNSString(settings.Arguments));
|
|
|
|
if (cwd.Length() != 0)
|
|
task.currentDirectoryPath = AppleUtils::ToNSString(cwd);
|
|
|
|
|
|
int32 returnCode = 0;
|
|
|
|
if (settings.WaitForEnd)
|
|
{
|
|
id<NSObject> outputObserver = nil;
|
|
|
|
if (captureStdOut)
|
|
{
|
|
NSPipe *stdoutPipe = [NSPipe pipe];
|
|
[task setStandardOutput:stdoutPipe];
|
|
|
|
outputObserver = [[NSNotificationCenter defaultCenter]
|
|
addObserverForName: NSFileHandleDataAvailableNotification
|
|
object: [stdoutPipe fileHandleForReading]
|
|
queue: nil
|
|
usingBlock:^(NSNotification* notification)
|
|
{
|
|
NSData* data = [stdoutPipe fileHandleForReading].availableData;
|
|
if (data.length)
|
|
{
|
|
String line((char*)data.bytes);
|
|
if (settings.SaveOutput)
|
|
settings.Output.Add(line.Get(), line.Length());
|
|
if (settings.LogOutput)
|
|
Log::Logger::Write(LogType::Info, line);
|
|
[[stdoutPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
|
|
}
|
|
}
|
|
];
|
|
|
|
[[stdoutPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
|
|
}
|
|
|
|
String exception;
|
|
|
|
@try
|
|
{
|
|
[task launch];
|
|
[task waitUntilExit];
|
|
}
|
|
@catch (NSException* e)
|
|
{
|
|
exception = e.reason.UTF8String;
|
|
}
|
|
|
|
if (!exception.IsEmpty())
|
|
{
|
|
LOG(Error, "Failed to run command {0} {1} with error {2}", settings.FileName, settings.Arguments, exception);
|
|
return -1;
|
|
}
|
|
|
|
returnCode = [task terminationStatus];
|
|
}
|
|
else {
|
|
|
|
String exception;
|
|
|
|
@try
|
|
{
|
|
[task launch];
|
|
}
|
|
@catch (NSException* e)
|
|
{
|
|
exception = e.reason.UTF8String;
|
|
}
|
|
|
|
if (!exception.IsEmpty())
|
|
{
|
|
LOG(Error, "Failed to run command {0} {1} with error {2}", settings.FileName, settings.Arguments, exception);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
return returnCode;
|
|
}
|
|
|
|
#endif
|