// Copyright Epic Games, Inc. All Rights Reserved. #include "SConnectionWindow.h" #include "Async/TaskGraphInterfaces.h" #include "Internationalization/Internationalization.h" #include "SlateOptMacros.h" #include "SocketSubsystem.h" #include "Styling/StyleColors.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/Text/STextBlock.h" // TraceAnalysis #include "Trace/ControlClient.h" #include "Trace/StoreConnection.h" // TraceInsightsCore #include "InsightsCore/Version.h" // TraceInsightsFrontend #include "InsightsFrontend/Common/InsightsFrontendStyle.h" #include "InsightsFrontend/Common/Log.h" #define LOCTEXT_NAMESPACE "UE::Insights::SConnectionWindow" namespace UE::Insights { // Should match GDefaultChannels (from Runtime\Core\Private\ProfilingDebugging\TraceAuxiliary.cpp). // We cannot use GDefaultChannels directly as UE_TRACE_ENABLED may be off. static const TCHAR* GInsightsDefaultChannelPreset = TEXT("cpu,gpu,frame,log,bookmark,screenshot,region"); //////////////////////////////////////////////////////////////////////////////////////////////////// SConnectionWindow::SConnectionWindow() { } //////////////////////////////////////////////////////////////////////////////////////////////////// SConnectionWindow::~SConnectionWindow() { if (ConnectTask && !ConnectTask->IsComplete()) { FTaskGraphInterface::Get().WaitUntilTaskCompletes(ConnectTask); ConnectTask = nullptr; } } //////////////////////////////////////////////////////////////////////////////////////////////////// BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SConnectionWindow::Construct(const FArguments& InArgs, TSharedRef InTraceStoreConnection) { TraceStoreConnection = InTraceStoreConnection; ChildSlot [ SNew(SOverlay) // Version + SOverlay::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Top) .Padding(0.0f, -16.0f, 4.0f, 0.0f) [ SNew(STextBlock) .Clipping(EWidgetClipping::ClipToBoundsWithoutIntersecting) .Text(FText::FromString(TEXT(UNREAL_INSIGHTS_VERSION_STRING_EX))) .ColorAndOpacity(FLinearColor(0.15f, 0.15f, 0.15f, 1.0f)) ] + SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) .Padding(0.0f, 0.0f, 0.0f, 0.0f) [ SNew(SBox) [ SNew(SBorder) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) .Padding(0.0f) .BorderImage(FAppStyle::Get().GetBrush("WhiteBrush")) .BorderBackgroundColor(FSlateColor(EStyleColor::Panel)) ] ] // Overlay slot for the main window area + SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ SAssignNew(MainContentPanel, SVerticalBox) + SVerticalBox::Slot() .HAlign(HAlign_Fill) .AutoHeight() .Padding(3.0f, 3.0f) [ ConstructConnectPanel() ] ] ]; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION //////////////////////////////////////////////////////////////////////////////////////////////////// BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION TSharedRef SConnectionWindow::ConstructConnectPanel() { FText InitialChannelsExampleText = FText::FromString(FString::Printf(TEXT("default,counter,stats,file,loadtime,assetloadtime,task\ndefault=%s"), GInsightsDefaultChannelPreset)); TSharedRef Widget = SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(12.0f, 12.0f, 12.0f, 0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) .MinDesiredWidth(180.0f) .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("TraceRecorderAddressText", "Trace recorder IP address")) ] ] + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) .Padding(6.0f, 0.0f, 0.0f, 0.0f) [ SNew(SBox) .MinDesiredWidth(120.0f) [ SAssignNew(TraceRecorderAddressTextBox, SEditableTextBox) ] ] ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(12.0f, 8.0f, 12.0f, 0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) .MinDesiredWidth(180.0f) .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("RunningInstanceAddressText", "Running instance IP address")) ] ] + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) .Padding(6.0f, 0.0f, 0.0f, 0.0f) [ SNew(SBox) .MinDesiredWidth(120.0f) [ SAssignNew(RunningInstanceAddressTextBox, SEditableTextBox) ] ] ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(12.0f, 8.0f, 12.0f, 0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) .MinDesiredWidth(180.0f) .HAlign(HAlign_Right) [ SNew(STextBlock) .Text(LOCTEXT("InitialChannelsText", "Initial channels")) ] ] + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) .Padding(6.0f, 0.0f, 0.0f, 0.0f) [ SNew(SBox) .MinDesiredWidth(120.0f) [ SAssignNew(ChannelsTextBox, SEditableTextBox) ] ] ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(198.0f, 4.0f, 12.0f, 0.0f) [ SNew(STextBlock) .Text(LOCTEXT("InitialChannelsNoteText", "Comma-separated list of channels/presets to enable when connected.")) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(198.0f, 2.0f, 12.0f, 0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Top) .Padding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)) [ SNew(STextBlock) .Text(LOCTEXT("InitialChannelsExamplesTitle", "Examples")) ] + SHorizontalBox::Slot() .Padding(6.0f, 0.0f, 0.0f, 0.0f) [ SNew(SEditableTextBox) .IsReadOnly(true) .Text(InitialChannelsExampleText) ] ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(198.0f, 2.0f, 12.0f, 0.0f) [ SNew(STextBlock) .Text(LOCTEXT("InitialChannelsNote2Text", "Some channels/presets (like \"memory\") cannot be enabled on late connections.")) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(12.0f, 8.0f, 12.0f, 12.0f) [ SNew(SBox) .HAlign(HAlign_Right) [ SNew(SButton) .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("PrimaryButton")) .Text(LOCTEXT("Connect", "Connect")) .ToolTipText(LOCTEXT("ConnectToolTip", "Late connect the running instance at specified address with the trace recorder.")) .OnClicked(this, &SConnectionWindow::Connect_OnClicked) .IsEnabled_Lambda([this]() { return !bIsConnecting; }) ] ] // Notification area overlay + SVerticalBox::Slot() .FillHeight(1.0f) .HAlign(HAlign_Right) .VAlign(VAlign_Bottom) .Padding(16.0f) [ SAssignNew(NotificationList, SNotificationList) ]; const FText LocalHost = FText::FromString(TEXT("127.0.0.1")); if (TraceStoreConnection->IsLocalHost()) { TSharedPtr RecorderAddr; if (ISocketSubsystem* Sockets = ISocketSubsystem::Get()) { bool bCanBindAll = false; RecorderAddr = Sockets->GetLocalHostAddr(*GLog, bCanBindAll); } if (RecorderAddr.IsValid()) { const FString RecorderAddrStr = RecorderAddr->ToString(false); TraceRecorderAddressTextBox->SetText(FText::FromString(RecorderAddrStr)); } else { TraceRecorderAddressTextBox->SetText(LocalHost); } } else // remote trace store { uint32 StoreAddress = 0; uint32 StorePort = 0; if (TraceStoreConnection->GetStoreAddressAndPort(StoreAddress, StorePort)) { const FString RecorderAddrStr = FString::Printf(TEXT("%u.%u.%u.%u"), (StoreAddress >> 24) & 0xFF, (StoreAddress >> 16) & 0xFF, (StoreAddress >> 8) & 0xFF, StoreAddress & 0xFF); TraceRecorderAddressTextBox->SetText(FText::FromString(RecorderAddrStr)); } else { TraceRecorderAddressTextBox->SetText(FText::FromString(TraceStoreConnection->GetLastStoreHost())); } } RunningInstanceAddressTextBox->SetText(LocalHost); ChannelsTextBox->SetText(FText::FromStringView(TEXT("default"))); return Widget; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SConnectionWindow::Connect_OnClicked() { FText TraceRecorderAddressText = TraceRecorderAddressTextBox->GetText(); if (TraceRecorderAddressText.IsEmptyOrWhitespace()) { // nothing to do return FReply::Handled(); } const FString& TraceRecorderAddressStr = TraceRecorderAddressText.ToString(); FText RunningInstanceAddressText = RunningInstanceAddressTextBox->GetText(); if (RunningInstanceAddressText.IsEmptyOrWhitespace()) { // nothing to do return FReply::Handled(); } const FString& RunningInstanceAddressStr = RunningInstanceAddressText.ToString(); FGraphEventArray Prerequisites; FGraphEventArray* PrerequisitesPtr = nullptr; if (ConnectTask.IsValid()) { Prerequisites.Add(ConnectTask); PrerequisitesPtr = &Prerequisites; } const FString ChannelsExpandedStr = ChannelsTextBox->GetText().ToString().Replace(TEXT("default"), GInsightsDefaultChannelPreset); FGraphEventRef PreConnectTask = FFunctionGraphTask::CreateAndDispatchWhenReady( [this, TraceRecorderAddressStr, RunningInstanceAddressStr, ChannelsExpandedStr] { bIsConnecting = true; UE_LOG(LogInsightsFrontend, Log, TEXT("[Connection] Try connecting to \"%s\"..."), *RunningInstanceAddressStr); UE::Trace::FControlClient ControlClient; FString IPStr, PortStr; RunningInstanceAddressStr.Split(TEXT(":"), &IPStr, &PortStr); uint16 Port = 1985; if (!PortStr.IsEmpty()) { Port = uint16(FCString::Atoi(*PortStr)); } else { IPStr = RunningInstanceAddressStr; } if (ControlClient.Connect(*IPStr, Port)) { UE_LOG(LogInsightsFrontend, Log, TEXT("[Connection] SendSendTo(\"%s\")..."), *TraceRecorderAddressStr); ControlClient.SendSendTo(*TraceRecorderAddressStr); UE_LOG(LogInsightsFrontend, Log, TEXT("[Connection] ToggleChannel(\"%s\")..."), *ChannelsExpandedStr); ControlClient.SendToggleChannel(*ChannelsExpandedStr, true); bIsConnectedSuccessfully = true; } else { bIsConnectedSuccessfully = false; } }, TStatId{}, PrerequisitesPtr, ENamedThreads::AnyBackgroundThreadNormalTask); ConnectTask = FFunctionGraphTask::CreateAndDispatchWhenReady( [this, RunningInstanceAddressStr] { if (bIsConnectedSuccessfully) { UE_LOG(LogInsightsFrontend, Log, TEXT("[Connection] Successfully connected.")); FNotificationInfo NotificationInfo(FText::Format(LOCTEXT("ConnectSuccess", "Successfully connected to \"{0}\"!"), FText::FromString(RunningInstanceAddressStr))); NotificationInfo.bFireAndForget = false; NotificationInfo.bUseLargeFont = false; NotificationInfo.bUseSuccessFailIcons = true; NotificationInfo.ExpireDuration = 10.0f; TSharedRef NotificationItem = NotificationList->AddNotification(NotificationInfo); NotificationItem->SetCompletionState(SNotificationItem::CS_Success); NotificationItem->ExpireAndFadeout(); } else { UE_LOG(LogInsightsFrontend, Warning, TEXT("[Connection] Failed to connect to \"%s\"!"), *RunningInstanceAddressStr); FNotificationInfo NotificationInfo(FText::Format(LOCTEXT("ConnectFailed", "Failed to connect to \"{0}\"!"), FText::FromString(RunningInstanceAddressStr))); NotificationInfo.bFireAndForget = false; NotificationInfo.bUseLargeFont = false; NotificationInfo.bUseSuccessFailIcons = true; NotificationInfo.ExpireDuration = 10.0f; TSharedRef NotificationItem = NotificationList->AddNotification(NotificationInfo); NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); NotificationItem->ExpireAndFadeout(); } bIsConnecting = false; }, TStatId{}, PreConnectTask, ENamedThreads::GameThread); return FReply::Handled(); } //////////////////////////////////////////////////////////////////////////////////////////////////// } // namespace UE::Insights #undef LOCTEXT_NAMESPACE