Files
UnrealEngine/Engine/Source/Developer/DesktopPlatform/Private/Mac/MacNativeFeedbackContext.cpp
2025-05-18 13:04:45 +08:00

510 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MacNativeFeedbackContext.h"
#include "HAL/PlatformTime.h"
#include "Logging/StructuredLog.h"
#include "Mac/MacApplication.h"
#include "Mac/CocoaThread.h"
#include "Misc/OutputDeviceHelper.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/OutputDeviceRedirector.h"
#include "HAL/PlatformApplicationMisc.h"
@implementation FMacNativeFeedbackContextWindowController
-(void)toggleLog
{
if([LogView isHidden])
{
NSRect Frame = [Window frame];
int32 ConsoleHeight = 600;
if(GConfig)
{
GConfig->GetInt(TEXT("DebugMac"), TEXT("ConsoleHeight"), ConsoleHeight, GGameIni);
}
Frame.origin.y -= (ConsoleHeight - Frame.size.height);
Frame.size.height = ConsoleHeight;
[Window setFrame:Frame display:YES animate:YES];
[LogView setHidden:NO];
}
else
{
CGFloat Height = [LogView frame].size.height;
[LogView setHidden:YES];
NSRect Frame = [Window frame];
Frame.size.height -= Height;
Frame.origin.y += Height;
[Window setFrame:Frame display:YES animate:YES];
}
}
-(id)init
{
id obj = [super init];
if(obj)
{
int32 ConsoleWidth = 800;
int32 ConsoleHeight = 600;
int32 ConsolePosX = 0;
int32 ConsolePosY = 0;
bool bHasX = false;
bool bHasY = false;
if(GConfig)
{
GConfig->GetInt(TEXT("DebugMac"), TEXT("ConsoleWidth"), ConsoleWidth, GGameIni);
GConfig->GetInt(TEXT("DebugMac"), TEXT("ConsoleHeight"), ConsoleHeight, GGameIni);
bHasX = GConfig->GetInt(TEXT("DebugMac"), TEXT("ConsoleX"), ConsolePosX, GGameIni);
bHasY = GConfig->GetInt(TEXT("DebugMac"), TEXT("ConsoleY"), ConsolePosY, GGameIni);
}
NSRect WindowRect = NSMakeRect(ConsolePosX, ConsolePosY, ConsoleWidth, ConsoleHeight);
Window = [[NSWindow alloc] initWithContentRect:WindowRect styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskResizable|NSWindowStyleMaskClosable backing:NSBackingStoreBuffered defer:NO];
[Window setTitle:@"Unreal Engine"];
[Window setReleasedWhenClosed:NO];
[Window setMinSize:NSMakeSize(400, 100)];
[Window setRestorable:NO];
[Window disableSnapshotRestoration];
NSView* View = [Window contentView];
[View setAutoresizesSubviews:YES];
[View setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
ShowLogButton = [[NSButton new] autorelease];
NSRect ShowLogRect = [ShowLogButton frame];
{
[ShowLogButton setIdentifier:@"ShowLogButton"];
[ShowLogButton setButtonType:NSButtonTypeMomentaryPushIn];
[ShowLogButton setBezelStyle:NSBezelStyleRounded];
[ShowLogButton setTitle:@"Show Log"];
[ShowLogButton sizeToFit];
ShowLogRect = [ShowLogButton frame];
ShowLogRect.origin.x = WindowRect.size.width - 8 - ShowLogRect.size.width;
ShowLogRect.origin.y = WindowRect.size.height - ShowLogRect.size.height - 8;
[ShowLogButton setFrameOrigin:ShowLogRect.origin];
[ShowLogButton setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin];
[ShowLogButton setTarget:self];
[ShowLogButton setAction:@selector(toggleLog)];
}
CancelButton = [[NSButton new] autorelease];
NSRect CancelRect = [CancelButton frame];
{
[CancelButton setIdentifier:@"CancelButton"];
[CancelButton setTitle:@"Cancel"];
[CancelButton setButtonType:NSButtonTypeMomentaryPushIn];
[CancelButton setBezelStyle:NSBezelStyleRounded];
[CancelButton sizeToFit];
CancelRect = [CancelButton frame];
CancelRect.origin.x = ShowLogRect.origin.x - CancelRect.size.width - 4;
CancelRect.origin.y = ShowLogRect.origin.y;
[CancelButton setFrameOrigin:CancelRect.origin];
[CancelButton setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin];
[CancelButton setTarget:self];
[CancelButton setAction:@selector(hideWindow)];
}
StatusLabel = [[[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 100, 18)] autorelease];
NSRect StatusRect = [StatusLabel frame];
{
[StatusLabel setBezeled:NO];
[StatusLabel setDrawsBackground:NO];
[StatusLabel setFont:[NSFont labelFontOfSize:[NSFont systemFontSize]]];
[StatusLabel setSelectable:NO];
[StatusLabel setEditable:NO];
[StatusLabel setBordered:NO];
[StatusLabel setStringValue:@"Progress:"];
StatusRect = [StatusLabel frame];
StatusRect.size.width = CancelRect.origin.x - 16;
StatusRect.origin.x = 8;
StatusRect.origin.y = ShowLogRect.origin.y + ((ShowLogRect.size.height - [NSFont systemFontSize]) / 2);
[StatusLabel setIdentifier:@"StatusLabel"];
[StatusLabel setFrame:StatusRect];
[StatusLabel setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin];
}
ProgressIndicator = [[NSProgressIndicator new] autorelease];
NSRect ProgressRect = [ProgressIndicator frame];
{
[ProgressIndicator setStyle:NSProgressIndicatorStyleBar];
[ProgressIndicator sizeToFit];
ProgressRect = [ProgressIndicator frame];
ProgressRect.size.width = WindowRect.size.width - 16;
ProgressRect.origin.x = 8;
ProgressRect.origin.y = CancelRect.origin.y - ProgressRect.size.height - 8;
[ProgressIndicator setIdentifier:@"ProgressIndicator"];
[ProgressIndicator setIndeterminate:YES];
[ProgressIndicator setFrame:ProgressRect];
[ProgressIndicator setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin];
}
TextView = [[NSTextView new] autorelease];
NSRect TextRect = [TextView frame];
{
[TextView setIdentifier:@"TextView"];
[TextView setVerticallyResizable:YES];
[TextView setHorizontallyResizable:NO];
[TextView setBackgroundColor: [NSColor blackColor]];
[TextView setMinSize:NSMakeSize( 0.0, 0.0 ) ];
[TextView setMaxSize:NSMakeSize( FLT_MAX, FLT_MAX )];
TextRect = NSMakeRect(8, 8, WindowRect.size.width - 16, ProgressRect.origin.y - 16);
[TextView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
LogView = [[NSScrollView new] autorelease];
[LogView setHasVerticalScroller:YES];
[LogView setHasHorizontalScroller:NO];
[LogView setAutohidesScrollers:YES];
[LogView setAutoresizesSubviews:YES];
[LogView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin];
[LogView setFrame:TextRect];
[TextView setFrameSize:[LogView contentSize]];
[[TextView textContainer] setContainerSize:NSMakeSize( [LogView contentSize].width, FLT_MAX )];
[[TextView textContainer] setWidthTracksTextView:YES];
[LogView setDocumentView:TextView];
}
[View addSubview:ShowLogButton];
[View addSubview:StatusLabel];
[View addSubview:ProgressIndicator];
[View addSubview:CancelButton];
[View addSubview:LogView];
[ProgressIndicator startAnimation:nil];
if (!bHasX || !bHasY)
{
[Window center];
}
}
return self;
}
-(void)dealloc
{
[Window close];
[Window release];
[super dealloc];
}
-(void)setShowProgress:(bool)bShowProgress
{
if(bShowProgress)
{
[ProgressIndicator stopAnimation:nil];
[ProgressIndicator setIndeterminate:NO];
}
else
{
if(![LogView isHidden])
{
[self toggleLog];
}
[ProgressIndicator setIndeterminate:YES];
[ProgressIndicator startAnimation:nil];
}
}
-(void)setShowCancelButton:(bool)bShowCancelButton
{
BOOL bHideCancel = !bShowCancelButton;
[CancelButton setHidden:bHideCancel];
}
-(void)setTitleText:(NSString*)Title
{
[Window setTitle:Title];
}
-(void)setStatusText:(NSString*)Text
{
[StatusLabel setStringValue:Text];
}
-(void)setProgress:(double)Progress total:(double)Total
{
if(![ProgressIndicator isIndeterminate])
{
[ProgressIndicator setMaxValue:Total];
[ProgressIndicator setMinValue:0.0];
[ProgressIndicator setDoubleValue:Progress];
}
}
-(void)showWindow
{
[Window makeKeyAndOrderFront:nil];
}
-(void)hideWindow
{
[Window orderOut:nil];
}
-(bool)windowOpen
{
return [Window isVisible];
}
@end
FMacNativeFeedbackContext::FMacNativeFeedbackContext()
: FFeedbackContext()
, WindowController(nil)
, Context( NULL )
, OutstandingTasks(0)
, SlowTaskCount( 0 )
, bShowingConsoleForSlowTask( false )
{
MainThreadCall(^{
WindowController = [[FMacNativeFeedbackContextWindowController alloc] init];
}, true, UnrealNilEventMode);
SetDefaultTextColor();
}
FMacNativeFeedbackContext::~FMacNativeFeedbackContext()
{
do
{
FPlatformApplicationMisc::PumpMessages( true );
} while(OutstandingTasks);
MainThreadCall(^{
[WindowController release];
}, true, UnrealNilEventMode);
}
void FMacNativeFeedbackContext::Serialize(const TCHAR* Data, ELogVerbosity::Type Verbosity, const FName& Category)
{
Serialize(Data, Verbosity, Category, -1.0);
}
void FMacNativeFeedbackContext::Serialize(const TCHAR* Data, ELogVerbosity::Type Verbosity, const FName& Category, double Time)
{
FFeedbackContext::Serialize(Data, Verbosity, Category, Time);
SerializeToWindow(Data, Verbosity, Category, Time);
}
void FMacNativeFeedbackContext::SerializeRecord(const UE::FLogRecord& Record)
{
FFeedbackContext::SerializeRecord(Record);
TStringBuilder<512> Text;
Record.FormatMessageTo(Text);
SerializeToWindow(*Text, Record.GetVerbosity(), Record.GetCategory(), -1.0);
}
void FMacNativeFeedbackContext::SerializeToWindow(const TCHAR* Data, ELogVerbosity::Type Verbosity, const FName& Category, double Time)
{
if (WindowController && bShowingConsoleForSlowTask)
{
FScopeLock ScopeLock(&CriticalSection);
static bool Entry=0;
if( !GIsCriticalError || Entry )
{
// here we can change the color of the text to display, it's in the format:
// ForegroundRed | ForegroundGreen | ForegroundBlue | ForegroundBright | BackgroundRed | BackgroundGreen | BackgroundBlue | BackgroundBright
// where each value is either 0 or 1 (can leave off trailing 0's), so
// blue on bright yellow is "00101101" and red on black is "1"
// An empty string reverts to the normal gray on black
if (Verbosity == ELogVerbosity::SetColor)
{
if (FCString::Stricmp(Data, TEXT("")) == 0)
{
SetDefaultTextColor();
}
else
{
SCOPED_AUTORELEASE_POOL;
// turn the string into a bunch of 0's and 1's
TCHAR String[9];
FMemory::Memset(String, 0, sizeof(TCHAR) * UE_ARRAY_COUNT(String));
FCString::Strncpy(String, Data, UE_ARRAY_COUNT(String));
for (TCHAR* S = String; *S; S++)
{
*S -= '0';
}
NSMutableArray* Colors = [[NSMutableArray alloc] init];
NSMutableArray* AttributeKeys = [[NSMutableArray alloc] init];
// Get FOREGROUND_INTENSITY and calculate final color
CGFloat Intensity = String[3] ? 1.0 : 0.5;
[Colors addObject:[NSColor colorWithSRGBRed:(String[0] ? 1.0 * Intensity : 0.0) green:(String[1] ? 1.0 * Intensity : 0.0) blue:(String[2] ? 1.0 * Intensity : 0.0) alpha:1.0]];
// Get BACKGROUND_INTENSITY and calculate final color
Intensity = String[7] ? 1.0 : 0.5;
[Colors addObject:[NSColor colorWithSRGBRed:(String[4] ? 1.0 * Intensity : 0.0) green:(String[5] ? 1.0 * Intensity : 0.0) blue:(String[6] ? 1.0 * Intensity : 0.0) alpha:1.0]];
[AttributeKeys addObject:NSForegroundColorAttributeName];
[AttributeKeys addObject:NSBackgroundColorAttributeName];
OutstandingTasks++;
MainThreadCall(^{
if( TextViewTextColor )
[TextViewTextColor release];
TextViewTextColor = [[NSDictionary alloc] initWithObjects:Colors forKeys:AttributeKeys];
[Colors release];
[AttributeKeys release];
OutstandingTasks--;
}, false);
}
}
else
{
SCOPED_AUTORELEASE_POOL;
TCHAR OutputString[MAX_SPRINTF]=TEXT(""); //@warning: this is safe as FCString::Sprintf only use 1024 characters max
FCString::Sprintf(OutputString,TEXT("%s%s"), *FOutputDeviceHelper::FormatLogLine(Verbosity, Category, Data, GPrintLogTimes, Time), LINE_TERMINATOR);
CFStringRef CocoaText = FPlatformString::TCHARToCFString(OutputString);
OutstandingTasks++;
MainThreadCall(^{
NSAttributedString *AttributedString = [[NSAttributedString alloc] initWithString:(NSString*)CocoaText attributes:TextViewTextColor];
[[WindowController->TextView textStorage] appendAttributedString:AttributedString];
[WindowController->TextView scrollRangeToVisible:NSMakeRange([[WindowController->TextView string] length], 0)];
[AttributedString release];
CFRelease(CocoaText);
OutstandingTasks--;
}, false);
if(!MacApplication)
{
FPlatformApplicationMisc::PumpMessages( true );
}
}
}
else
{
Entry=1;
try
{
// Ignore errors to prevent infinite-recursive exception reporting.
SerializeToWindow(Data, Verbosity, Category, Time);
}
catch( ... )
{}
Entry=0;
}
}
}
bool FMacNativeFeedbackContext::YesNof(const FText& Text)
{
return GWarn->YesNof(Text);
}
bool FMacNativeFeedbackContext::ReceivedUserCancel()
{
bool bReceivedUserCancel = false;
if(WindowController != NULL && bShowingConsoleForSlowTask && ![WindowController windowOpen])
{
bReceivedUserCancel = true;
}
return bReceivedUserCancel;
}
void FMacNativeFeedbackContext::StartSlowTask(const FText& Task, bool bInShowCancelButton)
{
FFeedbackContext::StartSlowTask(Task, bInShowCancelButton);
if(WindowController != NULL && !bShowingConsoleForSlowTask)
{
MainThreadCall(^{
[WindowController setTitleText:Task.ToString().GetNSString()];
[WindowController setStatusText:@"Progress:"];
[WindowController setShowCancelButton:bInShowCancelButton];
[WindowController setShowProgress:true];
[WindowController setProgress:0 total:1];
[WindowController setShowProgress:false];
SetDefaultTextColor();
[WindowController showWindow];
bShowingConsoleForSlowTask = true;
});
}
}
void FMacNativeFeedbackContext::FinalizeSlowTask()
{
FFeedbackContext::FinalizeSlowTask();
if(bShowingConsoleForSlowTask)
{
MainThreadCall(^{
if(WindowController != NULL)
{
[WindowController hideWindow];
}
bShowingConsoleForSlowTask = false;
});
}
}
void FMacNativeFeedbackContext::ProgressReported( const float TotalProgressInterp, FText DisplayMessage )
{
if(WindowController != NULL && bShowingConsoleForSlowTask)
{
MainThreadCall(^{
[WindowController setStatusText:DisplayMessage.ToString().GetNSString()];
[WindowController setShowProgress:true];
[WindowController setProgress:TotalProgressInterp total:1];
});
}
}
FContextSupplier* FMacNativeFeedbackContext::GetContext() const
{
return Context;
}
void FMacNativeFeedbackContext::SetContext( FContextSupplier* InSupplier )
{
Context = InSupplier;
}
void FMacNativeFeedbackContext::SetDefaultTextColor()
{
SCOPED_AUTORELEASE_POOL;
FScopeLock ScopeLock( &CriticalSection );
NSMutableArray* Colors = [[NSMutableArray alloc] init];
NSMutableArray* AttributeKeys = [[NSMutableArray alloc] init];
[Colors addObject:[NSColor grayColor]];
[Colors addObject:[NSColor blackColor]];
[AttributeKeys addObject:NSForegroundColorAttributeName];
[AttributeKeys addObject:NSBackgroundColorAttributeName];
OutstandingTasks++;
MainThreadCall(^{
if( TextViewTextColor )
[TextViewTextColor release];
TextViewTextColor = [[NSDictionary alloc] initWithObjects:Colors forKeys:AttributeKeys];
[Colors release];
[AttributeKeys release];
OutstandingTasks--;
}, false);
}