Files
2025-05-18 13:04:45 +08:00

2073 lines
66 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SLogView.h"
#include "Algo/BinarySearch.h"
#include "Async/AsyncWork.h"
#include "DesktopPlatformModule.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Commands/Commands.h"
#include "Framework/Commands/UICommandList.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "HAL/PlatformApplicationMisc.h"
#include "HAL/PlatformFileManager.h"
#include "ISourceCodeAccessModule.h"
#include "ISourceCodeAccessor.h"
#include "Logging/MessageLog.h"
#include "Modules/ModuleManager.h"
#include "SlateOptMacros.h"
#include "Styling/AppStyle.h"
#include "Styling/StyleColors.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Text/STextBlock.h"
// TraceServices
#include "TraceServices/Model/Log.h"
// TraceInsightsCore
#include "InsightsCore/Common/TimeUtils.h"
// TraceInsights
#include "Insights/InsightsManager.h"
#include "Insights/InsightsStyle.h"
#include "Insights/Log.h"
#include "Insights/TimingProfiler/TimingProfilerManager.h"
#include "Insights/TimingProfiler/Tracks/MarkersTimingTrack.h" // for FTimeMarkerTrackBuilder::GetColorBy*
#include "Insights/TimingProfiler/Widgets/STimingProfilerWindow.h"
#include "Insights/ViewModels/TimingViewDrawHelper.h"
#include "Insights/Widgets/STimingView.h"
////////////////////////////////////////////////////////////////////////////////////////////////////
#define LOCTEXT_NAMESPACE "UE::Insights::SLogView"
namespace UE::Insights
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// FLogViewCommands
////////////////////////////////////////////////////////////////////////////////////////////////////
class FLogViewCommands : public TCommands<FLogViewCommands>
{
public:
FLogViewCommands()
: TCommands<FLogViewCommands>(
TEXT("LogViewCommands"),
NSLOCTEXT("Contexts", "LogViewCommands", "Insights - Log View"),
NAME_None,
FInsightsStyle::GetStyleSetName())
{
}
virtual ~FLogViewCommands()
{
}
// UI_COMMAND takes long for the compiler to optimize
UE_DISABLE_OPTIMIZATION_SHIP
virtual void RegisterCommands() override
{
UI_COMMAND(Command_HideSelectedCategory,
"Hide Category",
"Hides the selected log category.",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(Command_ShowOnlySelectedCategory,
"Show Only Selected Category",
"Shows only the selected log category (hides all other log categories).",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(Command_ShowAllCategories,
"Show All Categories",
"Resets the category filter (shows all log categories).",
EUserInterfaceActionType::Button,
FInputChord());
UI_COMMAND(Command_CopySelected,
"Copy",
"Copies the selected log (with all its properties) to clipboard.",
EUserInterfaceActionType::Button,
FInputChord(EModifierKey::Control, EKeys::C));
UI_COMMAND(Command_CopyMessage,
"Copy Message",
"Copies the message text of the selected log to clipboard.",
EUserInterfaceActionType::Button,
FInputChord(EModifierKey::Shift, EKeys::C));
UI_COMMAND(Command_CopyRange,
"Copy Range",
"Copies all the logs in the selected time range (highlighted in blue) to clipboard.",
EUserInterfaceActionType::Button,
FInputChord(EModifierKey::Control | EModifierKey::Shift, EKeys::C));
UI_COMMAND(Command_CopyAll,
"Copy All",
"Copies all the (filtered) logs to clipboard.",
EUserInterfaceActionType::Button, FInputChord());
UI_COMMAND(Command_SaveRange,
"Save Range As...",
"Saves all the logs in the selected time range (highlighted in blue) to a text file (tab-separated values or comma-separated values).",
EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::S));
UI_COMMAND(Command_SaveAll,
"Save All As...",
"Saves all the (filtered) logs to a text file (tab-separated values or comma-separated values).",
EUserInterfaceActionType::Button, FInputChord());
UI_COMMAND(Command_OpenSource,
"Open Source",
"Opens the source file of the selected message in the registered IDE.",
EUserInterfaceActionType::Button, FInputChord());
}
UE_ENABLE_OPTIMIZATION_SHIP
TSharedPtr<FUICommandInfo> Command_HideSelectedCategory;
TSharedPtr<FUICommandInfo> Command_ShowOnlySelectedCategory;
TSharedPtr<FUICommandInfo> Command_ShowAllCategories;
TSharedPtr<FUICommandInfo> Command_CopySelected;
TSharedPtr<FUICommandInfo> Command_CopyMessage;
TSharedPtr<FUICommandInfo> Command_CopyRange;
TSharedPtr<FUICommandInfo> Command_CopyAll;
TSharedPtr<FUICommandInfo> Command_SaveRange;
TSharedPtr<FUICommandInfo> Command_SaveAll;
TSharedPtr<FUICommandInfo> Command_OpenSource;
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// SLogMessageRow
////////////////////////////////////////////////////////////////////////////////////////////////////
class SLogMessageRow : public SMultiColumnTableRow<TSharedPtr<FLogMessage>>
{
SLATE_BEGIN_ARGS(SLogMessageRow) {}
SLATE_END_ARGS()
public:
/**
* Constructs the widget.
*
* @param InArgs The construction arguments.
* @param InLogMessage The log message displayed by this row.
* @param InOwnerTableView The table to which the row must be added.
*/
void Construct(const FArguments& InArgs, TSharedPtr<FLogMessage> InLogMessage, TSharedRef<SLogView> InParentWidget, const TSharedRef<STableViewBase>& InOwnerTableView)
{
WeakLogMessage = MoveTemp(InLogMessage);
WeakParentWidget = InParentWidget;
SMultiColumnTableRow<TSharedPtr<FLogMessage>>::Construct(FSuperRowType::FArguments(), InOwnerTableView);
TSharedRef<SWidget> Row = ChildSlot.GetChildAt(0);
ChildSlot
[
SNew(SBorder)
.BorderImage(FInsightsStyle::Get().GetBrush("WhiteBrush"))
.BorderBackgroundColor(this, &SLogMessageRow::GetBackgroundColor)
[
Row
]
];
}
public:
virtual TSharedRef<SWidget> GenerateWidgetForColumn(const FName& ColumnName) override
{
if (ColumnName == LogViewColumns::IdColumnName)
{
return SNew(SBox)
.Padding(FMargin(4.0, 0.0))
[
SNew(STextBlock)
.Text(this, &SLogMessageRow::GetIndex)
];
}
else if (ColumnName == LogViewColumns::TimeColumnName)
{
return SNew(SBox)
.Padding(FMargin(4.0, 0.0))
[
SNew(STextBlock)
.Text(this, &SLogMessageRow::GetTime)
.ColorAndOpacity(this, &SLogMessageRow::GetColorAndOpacity)
];
}
else if (ColumnName == LogViewColumns::VerbosityColumnName)
{
return SNew(SBox)
.Padding(FMargin(4.0, 0.0))
[
SNew(STextBlock)
.Text(this, &SLogMessageRow::GetVerbosity)
.ColorAndOpacity(this, &SLogMessageRow::GetColorByVerbosity)
];
}
else if (ColumnName == LogViewColumns::CategoryColumnName)
{
return SNew(SBox)
.Padding(FMargin(4.0, 0.0))
[
SNew(STextBlock)
.Text(this, &SLogMessageRow::GetCategory)
.ColorAndOpacity(this, &SLogMessageRow::GetColorByCategory)
];
}
else if (ColumnName == LogViewColumns::MessageColumnName)
{
return SNew(SBox)
.Padding(FMargin(4.0, 0.0))
[
SNew(STextBlock)
.Text(this, &SLogMessageRow::GetMessage)
.HighlightText(this, &SLogMessageRow::GetMessageHighlightText)
];
}
else if (ColumnName == LogViewColumns::FileColumnName)
{
return SNew(SBox)
.Padding(FMargin(4.0, 0.0))
[
SNew(STextBlock)
.Text(this, &SLogMessageRow::GetFile)
];
}
else if (ColumnName == LogViewColumns::LineColumnName)
{
return SNew(SBox)
.Padding(FMargin(4.0, 0.0))
[
SNew(STextBlock)
.Text(this, &SLogMessageRow::GetLine)
];
}
else
{
return SNew(STextBlock).Text(LOCTEXT("UnknownColumn", "Unknown Column"));
}
}
FSlateColor GetBackgroundColor() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
TSharedPtr<FLogMessage> SelectedLogMessage = ParentWidgetPin->GetSelectedLogMessage();
if (!SelectedLogMessage || SelectedLogMessage->GetIndex() != LogMessagePin->GetIndex()) // if row is not selected
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
const double Time = CacheEntry.GetTime();
TSharedPtr<TimingProfiler::STimingView> TimingView = ParentWidgetPin->GetTimingView();
if (TimingView && TimingView->IsTimeSelectedInclusive(Time))
{
return FSlateColor(FLinearColor(0.25f, 0.5f, 1.0f, 0.25f));
}
}
}
return FSlateColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.0f));
}
FSlateColor GetColorAndOpacity() const
{
bool IsSelected = false;
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
TSharedPtr<FLogMessage> SelectedLogMessage = ParentWidgetPin->GetSelectedLogMessage();
if (SelectedLogMessage && SelectedLogMessage->GetIndex() == LogMessagePin->GetIndex())
{
IsSelected = true;
}
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
const double Time = CacheEntry.GetTime();
// Verify if time is monotonically increasing.
if (LogMessagePin->GetIndex() > 0)
{
FLogMessageRecord& PrevCacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex() - 1);
if (Time < PrevCacheEntry.GetTime())
{
return FSlateColor(FLinearColor(1.0f, 0.3f, 0.3f, 1.0f));
}
}
TSharedPtr<TimingProfiler::STimingView> TimingView = ParentWidgetPin->GetTimingView();
if (TimingView && TimingView->IsTimeSelectedInclusive(Time))
{
if (IsSelected)
{
return FSlateColor(FLinearColor(0.0f, 0.05f, 0.2f, 1.0f));
}
else
{
return FSlateColor(FLinearColor(0.4f, 0.8f, 1.6f, 1.0f));
}
}
}
if (IsSelected)
{
return FSlateColor(FLinearColor::Black);
}
else
{
return FSlateColor(FLinearColor::White);
}
}
FText GetIndex() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.GetIndexAsText();
}
else
{
return FText();
}
}
FText GetTime() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.GetTimeAsText();
}
else
{
return FText();
}
}
FText GetVerbosity() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.GetVerbosityAsText();
}
else
{
return FText();
}
}
FSlateColor GetColorByVerbosity() const
{
if (IsSelected())
return FSlateColor(FLinearColor::Black);
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return FSlateColor(TimingProfiler::FTimeMarkerTrackBuilder::GetColorByVerbosity(CacheEntry.GetVerbosity()));
}
else
{
return FSlateColor(FLinearColor::White);
}
}
FText GetCategory() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.GetCategoryAsText();
}
else
{
return FText();
}
}
FSlateColor GetColorByCategory() const
{
if (IsSelected())
return FSlateColor(FLinearColor::Black);
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return FSlateColor(TimingProfiler::FTimeMarkerTrackBuilder::GetColorByCategory(CacheEntry.GetCategory()));
}
else
{
return FSlateColor(FLinearColor::White);
}
}
FText GetMessage() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.GetMessageAsText();
}
else
{
return FText();
}
}
FText GetMessageHighlightText() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
if (ParentWidgetPin.IsValid())
{
return ParentWidgetPin->GetFilterText();
}
else
{
return FText();
}
}
FText GetFile() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.GetFileAsText();
}
else
{
return FText();
}
}
FText GetLine() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.GetLineAsText();
}
else
{
return FText();
}
}
FText GetRowToolTip() const
{
TSharedPtr<SLogView> ParentWidgetPin = WeakParentWidget.Pin();
TSharedPtr<FLogMessage> LogMessagePin = WeakLogMessage.Pin();
if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid())
{
FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex());
return CacheEntry.ToDisplayString();
}
else
{
return FText();
}
}
private:
TWeakPtr<FLogMessage> WeakLogMessage;
TWeakPtr<SLogView> WeakParentWidget;
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// SLogView
////////////////////////////////////////////////////////////////////////////////////////////////////
SLogView::SLogView()
: FilteringStartIndex(0)
, FilteringEndIndex(0)
, FilteringChangeNumber(0)
, bIsFilteringAsyncTaskCancelRequested(false)
, TotalNumCategories(0)
, TotalNumMessages(0)
, TotalNumInserts(0)
, bIsDirty(false)
{
}
////////////////////////////////////////////////////////////////////////////////////////////////////
SLogView::~SLogView()
{
Reset();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::Reset()
{
//ListView
//ExternalScrollbar
FilterTextBox->SetText(FText::GetEmpty());
Filter.Reset();
FilterChangeNumber = 0;
FilteringStartIndex = 0;
FilteringEndIndex = 0;
FilteringChangeNumber = 0;
// Clean up our async task if we're deleted before it is completed.
if (FilteringAsyncTask.IsValid())
{
if (!FilteringAsyncTask->Cancel())
{
bIsFilteringAsyncTaskCancelRequested = true;
FilteringAsyncTask->EnsureCompletion();
}
FilteringAsyncTask.Reset();
}
//bIsFilteringAsyncTaskCancelRequested = false;
//FilteringStopwatch.Stop();
TotalNumCategories = 0;
TotalNumMessages = 0;
TotalNumInserts = 0;
bIsDirty = false;
DirtyStopwatch.Stop();
StatsText = FText::GetEmpty();
Cache.Reset();
FilteredMessages.Reset();
ListView->RebuildList();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SLogView::Construct(const FArguments& InArgs)
{
SAssignNew(ExternalScrollbar, SScrollBar)
.AlwaysShowScrollbar(true);
FSlimHorizontalToolBarBuilder ToolbarBuilder(TSharedPtr<const FUICommandList>(), FMultiBoxCustomization::None);
ToolbarBuilder.SetStyle(&FInsightsStyle::Get(), "SecondaryToolbar");
ToolbarBuilder.BeginSection("Filters");
{
// Verbosity Threshold
ToolbarBuilder.AddComboButton(
FUIAction(),
FOnGetContent::CreateSP(this, &SLogView::MakeVerbosityThresholdMenu),
LOCTEXT("VerbosityThresholdText", "Verbosity Threshold"),
LOCTEXT("VerbosityThresholdToolTip", "Filter log messages by verbosity threshold."),
FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.Filter.ToolBar"),
false
);
// Category Filter
ToolbarBuilder.AddComboButton(
FUIAction(),
FOnGetContent::CreateSP(this, &SLogView::MakeCategoryFilterMenu),
LOCTEXT("CategoryFilterText", "Category Filter"),
LOCTEXT("CategoryFilterToolTip", "Filter log messages by category."),
FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.Filter.ToolBar"),
false
);
// Text Filter (Search Box)
ToolbarBuilder.AddWidget(
SAssignNew(FilterTextBox, SSearchBox)
.HintText(LOCTEXT("FilterTextBoxHint", "Search log messages"))
.ToolTipText(LOCTEXT("FilterTextBoxToolTip", "Type here to filter the list of log messages."))
.OnTextChanged(this, &SLogView::FilterTextBox_OnTextChanged)
);
// Stats Text (number of logs)
ToolbarBuilder.AddWidget(
SNew(SBox)
.VAlign(VAlign_Center)
.Padding(FMargin(4.0f, 0.0f, 0.0f, 0.0f))
[
SNew(STextBlock)
.Text(this, &SLogView::GetStatsText)
.ColorAndOpacity(this, &SLogView::GetStatsTextColor)
]
);
}
ToolbarBuilder.EndSection();
ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(0.0f))
[
ToolbarBuilder.MakeWidget()
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f)
.VAlign(VAlign_Fill)
[
SNew(SScrollBox)
.Orientation(Orient_Horizontal)
+ SScrollBox::Slot()
.VAlign(VAlign_Fill)
[
SAssignNew(ListView, SListView<TSharedPtr<FLogMessage>>)
.ExternalScrollbar(ExternalScrollbar)
.FixedLineScrollOffset(0.0)
.SelectionMode(ESelectionMode::Single)
.OnMouseButtonClick(this, &SLogView::OnMouseButtonClick)
.OnSelectionChanged(this, &SLogView::OnSelectionChanged)
.ListItemsSource(&FilteredMessages)
.OnGenerateRow(this, &SLogView::OnGenerateRow)
.ConsumeMouseWheel(EConsumeMouseWheel::Always)
.OnContextMenuOpening(FOnContextMenuOpening::CreateSP(this, &SLogView::ListView_GetContextMenu))
.HeaderRow
(
SNew(SHeaderRow)
+ SHeaderRow::Column(LogViewColumns::IdColumnName)
.ManualWidth(60.0f)
.DefaultLabel(LOCTEXT("IdColumn", "Index"))
+ SHeaderRow::Column(LogViewColumns::TimeColumnName)
.ManualWidth(94.0f)
.DefaultLabel(LOCTEXT("TimeColumn", "Time"))
+ SHeaderRow::Column(LogViewColumns::VerbosityColumnName)
.ManualWidth(80.0f)
.DefaultLabel(LOCTEXT("VerbosityColumn", "Verbosity"))
+ SHeaderRow::Column(LogViewColumns::CategoryColumnName)
.ManualWidth(120.0f)
.DefaultLabel(LOCTEXT("CategoryColumn", "Category"))
+ SHeaderRow::Column(LogViewColumns::MessageColumnName)
.ManualWidth(880.0f)
.DefaultLabel(LOCTEXT("MessageColumn", "Message"))
+ SHeaderRow::Column(LogViewColumns::FileColumnName)
.ManualWidth(600.0f)
.DefaultLabel(LOCTEXT("FileColumn", "File"))
+ SHeaderRow::Column(LogViewColumns::LineColumnName)
.ManualWidth(60.0f)
.DefaultLabel(LOCTEXT("LineColumn", "Line"))
)
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f)
[
ExternalScrollbar.ToSharedRef()
]
]
];
InitCommandList();
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<ITableRow> SLogView::OnGenerateRow(TSharedPtr<FLogMessage> InLogMessage, const TSharedRef<STableViewBase>& OwnerTable)
{
// Generate a row for the log message corresponding to InLogMessage.
return SNew(SLogMessageRow, InLogMessage, SharedThis(this), OwnerTable);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::InitCommandList()
{
FLogViewCommands::Register();
CommandList = MakeShared<FUICommandList>();
CommandList->MapAction(FLogViewCommands::Get().Command_HideSelectedCategory, FExecuteAction::CreateSP(this, &SLogView::HideSelectedCategory), FCanExecuteAction::CreateSP(this, &SLogView::CanHideSelectedCategory));
CommandList->MapAction(FLogViewCommands::Get().Command_ShowOnlySelectedCategory, FExecuteAction::CreateSP(this, &SLogView::ShowOnlySelectedCategory), FCanExecuteAction::CreateSP(this, &SLogView::CanShowOnlySelectedCategory));
CommandList->MapAction(FLogViewCommands::Get().Command_ShowAllCategories, FExecuteAction::CreateSP(this, &SLogView::ShowAllCategories), FCanExecuteAction::CreateSP(this, &SLogView::CanShowAllCategories));
CommandList->MapAction(FLogViewCommands::Get().Command_CopySelected, FExecuteAction::CreateSP(this, &SLogView::CopySelected), FCanExecuteAction::CreateSP(this, &SLogView::CanCopySelected));
CommandList->MapAction(FLogViewCommands::Get().Command_CopyMessage, FExecuteAction::CreateSP(this, &SLogView::CopyMessage), FCanExecuteAction::CreateSP(this, &SLogView::CanCopyMessage));
CommandList->MapAction(FLogViewCommands::Get().Command_CopyRange, FExecuteAction::CreateSP(this, &SLogView::CopyRange), FCanExecuteAction::CreateSP(this, &SLogView::CanCopyRange));
CommandList->MapAction(FLogViewCommands::Get().Command_CopyAll, FExecuteAction::CreateSP(this, &SLogView::CopyAll), FCanExecuteAction::CreateSP(this, &SLogView::CanCopyAll));
CommandList->MapAction(FLogViewCommands::Get().Command_SaveRange, FExecuteAction::CreateSP(this, &SLogView::SaveRange), FCanExecuteAction::CreateSP(this, &SLogView::CanSaveRange));
CommandList->MapAction(FLogViewCommands::Get().Command_SaveAll, FExecuteAction::CreateSP(this, &SLogView::SaveAll), FCanExecuteAction::CreateSP(this, &SLogView::CanSaveAll));
CommandList->MapAction(FLogViewCommands::Get().Command_OpenSource, FExecuteAction::CreateSP(this, &SLogView::OpenSource), FCanExecuteAction::CreateSP(this, &SLogView::CanOpenSource));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
LLM_SCOPE_BYTAG(Insights);
TSharedPtr<const TraceServices::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
Cache.SetSession(Session);
int32 LogProviderNumCategories = 0;
int32 LogProviderNumMessages = 0;
int32 LogProviderNumInserts = 0;
if (Session.IsValid())
{
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get());
LogProviderNumCategories = static_cast<int32>(LogProvider.GetCategoryCount());
LogProviderNumMessages = static_cast<int32>(LogProvider.GetMessageCount());
LogProviderNumInserts = static_cast<int32>(LogProvider.GetInsertCount());
}
if (LogProviderNumCategories != TotalNumCategories)
{
TSet<FName> Categories;
TMap<FName, int32> DuplicatedCategories;
if (Session.IsValid())
{
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get());
// Re-read the number of categories (as it might have changed between the read locks).
LogProviderNumCategories = static_cast<int32>(LogProvider.GetCategoryCount());
LogProvider.EnumerateCategories([&Categories, &DuplicatedCategories](const TraceServices::FLogCategoryInfo& Category)
{
FStringView CategoryStr(Category.Name);
if (CategoryStr.StartsWith(TEXT("Log")))
{
CategoryStr.RightChopInline(3);
}
FName CategoryName(CategoryStr);
if (Categories.Contains(CategoryName))
{
int32* CountPtr = DuplicatedCategories.Find(CategoryName);
if (CountPtr)
{
++(*CountPtr);
}
else
{
DuplicatedCategories.Add(CategoryName, 1);
}
}
else
{
Categories.Add(CategoryName);
}
});
}
TotalNumCategories = LogProviderNumCategories;
UE_LOG(TraceInsights, Log, TEXT("[LogView] Total Log Categories: %d"), TotalNumCategories);
Filter.SyncAvailableCategories(Categories);
const int32 NumAvailableLogCategories = Filter.GetAvailableLogCategories().Num();
if (DuplicatedCategories.Num() > 0)
{
UE_LOG(TraceInsights, Warning, TEXT("[LogView] Duplicated Log Categories: %d (+%dx)"), DuplicatedCategories.Num(), TotalNumCategories - NumAvailableLogCategories);
for (const auto& KV : DuplicatedCategories)
{
UE_LOG(TraceInsights, Log, TEXT("[LogView] \"%s\" (+%dx)"), *KV.Key.GetPlainNameString(), KV.Value);
}
}
UE_LOG(TraceInsights, Log, TEXT("[LogView] Unique Log Categories: %d"), NumAvailableLogCategories);
FilteredMessages.Reset();
TotalNumMessages = 0;
bIsDirty = true;
DirtyStopwatch.Start();
}
if (LogProviderNumInserts != TotalNumInserts)
{
TotalNumInserts = LogProviderNumInserts;
Cache.Reset();
}
if (LogProviderNumMessages != TotalNumMessages)
{
bIsDirty = true;
DirtyStopwatch.Start();
if (LogProviderNumMessages > TotalNumMessages)
{
if (Filter.IsFilterSet())
{
// Filter messages async.
if (!FilteringAsyncTask.IsValid())
{
FilteringStopwatch.Restart();
bIsFilteringAsyncTaskCancelRequested = false;
FilteringStartIndex = TotalNumMessages;
FilteringEndIndex = LogProviderNumMessages;
FilteringChangeNumber = Filter.GetChangeNumber();
FilteringAsyncTask = MakeUnique<FAsyncTask<FLogFilteringAsyncTask>>(FilteringStartIndex, FilteringEndIndex, Filter, SharedThis(this));
#if !WITH_EDITOR
UE_LOG(TraceInsights, Log, TEXT("[LogView] Start async task for filtering by%s%s%s (\"%s\") (%d to %d)"),
Filter.IsFilterSetByVerbosity() ? TEXT(" Verbosity,") : TEXT(""),
Filter.IsFilterSetByCategory() ? TEXT(" Category,") : TEXT(""),
Filter.IsFilterSetByText() ? TEXT(" Text") : TEXT(""),
*Filter.GetFilterText().ToString(),
FilteringStartIndex, FilteringEndIndex);
#endif // !WITH_EDITOR
FilteringAsyncTask->StartBackgroundTask();
}
else
{
// A task is already in progress.
if (FilteringStartIndex == TotalNumMessages &&
FilteringEndIndex <= LogProviderNumMessages &&
FilteringChangeNumber == Filter.GetChangeNumber())
{
// The filter is still valid. Just wait.
}
else
{
// The filter used by running task is obsolete. Cancel the task.
bIsFilteringAsyncTaskCancelRequested = true;
}
}
}
else // no filtering
{
FilteringStopwatch.Restart();
for (int32 Index = TotalNumMessages; Index < LogProviderNumMessages; Index++)
{
FilteredMessages.Add(MakeShared<FLogMessage>(Index));
}
const int32 NumAddedMessages = LogProviderNumMessages - TotalNumMessages;
TotalNumMessages = LogProviderNumMessages;
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
ListView->RebuildList();
if (SelectedLogMessage.IsValid())
{
// Restore selection.
SelectLogMessageByLogIndex(SelectedLogMessage->GetIndex());
}
bIsDirty = false;
DirtyStopwatch.Reset();
UpdateStatsText();
FilteringStopwatch.Stop();
#if !WITH_EDITOR
uint64 DurationMs = FilteringStopwatch.GetAccumulatedTimeMs();
if (DurationMs > 10) // avoids spams
{
UE_LOG(TraceInsights, Log, TEXT("[LogView] Updated (no filter; %d added / %d total messages) in %llu ms."),
NumAddedMessages, LogProviderNumMessages, DurationMs);
}
#endif // !WITH_EDITOR
}
}
else // if (LogProviderNumMessages < TotalNumMessages)
{
// Just reset. On next Tick() the list will grow if needed.
#if !WITH_EDITOR
UE_LOG(TraceInsights, Log, TEXT("[LogView] RESET"));
#endif // !WITH_EDITOR
Cache.Reset();
FilteredMessages.Reset();
TotalNumMessages = 0;
ListView->RebuildList();
bIsDirty = (LogProviderNumMessages != 0);
if (bIsDirty)
{
DirtyStopwatch.Start();
}
else
{
DirtyStopwatch.Reset();
}
UpdateStatsText();
}
}
else if (bIsDirty && !FilteringAsyncTask.IsValid())
{
bIsDirty = false;
DirtyStopwatch.Reset();
UpdateStatsText();
}
if (FilteringAsyncTask.IsValid() &&
FilteringAsyncTask->IsDone())
{
// A filtering async task has completed. Check if filter used is still valid.
if (!bIsFilteringAsyncTaskCancelRequested &&
FilteringStartIndex == TotalNumMessages &&
FilteringEndIndex <= LogProviderNumMessages &&
FilteringChangeNumber == Filter.GetChangeNumber())
{
FLogFilteringAsyncTask& Task = FilteringAsyncTask->GetTask();
const TArray<uint32>& FilteredMessageIndices = Task.GetFilteredMessages();
// Add filtered messages to current FilteredMessages array.
const int32 NumFilteredMessages = FilteredMessageIndices.Num();
for (int32 Index = 0; Index < NumFilteredMessages; Index++)
{
FilteredMessages.Add(MakeShared<FLogMessage>(FilteredMessageIndices[Index]));
}
TotalNumMessages = Task.GetEndIndex();
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
ListView->RebuildList();
if (SelectedLogMessage.IsValid())
{
// Restore selection.
SelectLogMessageByLogIndex(SelectedLogMessage->GetIndex());
}
bIsDirty = false;
DirtyStopwatch.Reset();
UpdateStatsText();
FilteringStopwatch.Stop();
#if !WITH_EDITOR
uint64 DurationMs = FilteringStopwatch.GetAccumulatedTimeMs();
if (DurationMs > 10) // avoids spams
{
int32 NumAsyncFilteredMessages = Task.GetEndIndex() - Task.GetStartIndex();
double Speed = static_cast<double>(NumAsyncFilteredMessages) / FilteringStopwatch.GetAccumulatedTime();
UE_LOG(TraceInsights, Log, TEXT("[LogView] Updated (%d added / %d async filtered / %d total messages) in %llu ms (%.2f messages/second)."),
NumFilteredMessages, NumAsyncFilteredMessages, TotalNumMessages, DurationMs, Speed);
}
#endif // !WITH_EDITOR
}
FilteringAsyncTask.Reset();
}
//SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedPtr<FLogMessage> SLogView::GetSelectedLogMessage() const
{
TArray<TSharedPtr<FLogMessage>> SelectedItems = ListView->GetSelectedItems();
return (SelectedItems.Num() == 1) ? SelectedItems[0] : nullptr;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::SelectLogMessage(TSharedPtr<FLogMessage> LogMessage)
{
ListView->SetSelection(LogMessage, ESelectInfo::Direct);
if (LogMessage.IsValid())
{
ListView->RequestScrollIntoView(LogMessage);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::SelectLogMessageByLogIndex(int32 LogIndex)
{
if (FilteredMessages.Num() == 0)
{
return;
}
// We are assuming the FilteredMessages list is sorted by log index.
// Find the exact match of the log index in the filtered messages list.
int32 MessageIndex = Algo::BinarySearchBy(FilteredMessages, LogIndex, &FLogMessage::GetIndex);
if (MessageIndex == INDEX_NONE)
{
return;
}
SelectLogMessage(FilteredMessages[MessageIndex]);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::SelectLogMessageByClosestTime(double Time)
{
if (FilteredMessages.Num() == 0)
{
return;
}
// Find the message with the closest time, in the unfiltered list of messages.
int32 LogIndex = 0;
TSharedPtr<const TraceServices::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
if (Session.IsValid())
{
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get());
LogIndex = (int32)LogProvider.BinarySearchClosestByTime(Time);
}
else
{
return;
}
// We are assuming the FilteredMessages list is sorted by log index.
// Find first filtered message with log index >= index.
int32 MessageIndex = Algo::LowerBoundBy(FilteredMessages, LogIndex, &FLogMessage::GetIndex);
if (MessageIndex >= FilteredMessages.Num())
{
MessageIndex = FilteredMessages.Num() - 1;
}
SelectLogMessage(FilteredMessages[MessageIndex]);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::OnSelectedLogMessageChanged(TSharedPtr<FLogMessage> LogMessage)
{
if (LogMessage.IsValid())
{
TSharedPtr<UE::Insights::TimingProfiler::STimingView> TimingView = GetTimingView();
if (TimingView)
{
const double Time = Cache.Get(LogMessage->GetIndex()).GetTime();
if (FSlateApplication::Get().GetModifierKeys().IsShiftDown())
{
TimingView->SelectToTimeMarker(Time);
}
else
{
TimingView->SetAutoScroll(false);
TimingView->SetAndCenterOnTimeMarker(Time);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::OnMouseButtonClick(TSharedPtr<FLogMessage> LogMessage)
{
OnSelectedLogMessageChanged(LogMessage);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::OnSelectionChanged(TSharedPtr<FLogMessage> LogMessage, ESelectInfo::Type SelectInfo)
{
if (SelectInfo != ESelectInfo::Direct &&
SelectInfo != ESelectInfo::OnMouseClick)
{
OnSelectedLogMessageChanged(LogMessage);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::FilterTextBox_OnTextChanged(const FText& InFilterText)
{
if (Filter.GetFilterText().ToString().Equals(InFilterText.ToString(), ESearchCase::CaseSensitive))
{
// nothing to do
return;
}
// Set filter phrases.
Filter.SetFilterText(InFilterText);
// Report possible syntax errors back to the user.
FilterTextBox->SetError(Filter.GetSyntaxErrors());
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::OnFilterChanged()
{
const FString FilterText = FilterTextBox->GetText().ToString();
#if !WITH_EDITOR
UE_LOG(TraceInsights, Log, TEXT("[LogView] OnFilterChanged: \"%s\""), *FilterText);
#endif // !WITH_EDITOR
Cache.Reset();
FilteredMessages.Reset();
TotalNumMessages = 0;
bIsDirty = true;
DirtyStopwatch.Start();
// The next Tick() will update the filtered list of messages.
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::UpdateStatsText()
{
if (FilteredMessages.Num() == TotalNumMessages)
{
StatsText = FText::Format(LOCTEXT("StatsText1", "{0} logs"), FText::AsNumber(TotalNumMessages));
}
else
{
StatsText = FText::Format(LOCTEXT("StatsText2", "{0} / {1} logs"), FText::AsNumber(FilteredMessages.Num()), FText::AsNumber(TotalNumMessages));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
FText SLogView::GetStatsText() const
{
if (bIsDirty)
{
DirtyStopwatch.Update();
double DT = DirtyStopwatch.GetAccumulatedTime();
if (DT > 1.0)
{
return FText::Format(LOCTEXT("StatsTextEx", "{0} (filtering... please wait... {1}s)"), StatsText, FText::AsNumber(FMath::RoundToInt(DT)));
}
}
return StatsText;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
FSlateColor SLogView::GetStatsTextColor() const
{
if (bIsDirty)
{
return FSlateColor(FLinearColor(1.0f, 0.5f, 0.5f, 1.0f));
}
else
{
return FSlateColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedPtr<SWidget> SLogView::ListView_GetContextMenu()
{
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
const bool bShouldCloseWindowAfterMenuSelection = true;
FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, CommandList);
MenuBuilder.BeginSection("LogViewContextMenu");
{
if (SelectedLogMessage.IsValid())
{
FLogMessageRecord& Record = Cache.Get(SelectedLogMessage->GetIndex());
FName CategoryName(Record.GetCategory());
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_HideSelectedCategory,
NAME_None,
FText::Format(LOCTEXT("HideCategory", "Hide \"{0}\" Category"), Record.GetCategoryAsText()),
FText::Format(LOCTEXT("HideCategory_Tooltip", "Hide the \"{0}\" log category."), Record.GetCategoryAsText()),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Hidden")
);
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_ShowOnlySelectedCategory,
NAME_None,
FText::Format(LOCTEXT("ShowOnlyCategory", "Show Only \"{0}\" Category"), Record.GetCategoryAsText()),
FText::Format(LOCTEXT("ShowOnlyCategory_Tooltip", "Show only the \"{0}\" log category (hide all other log categories)."), Record.GetCategoryAsText()),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Visible")
);
}
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_ShowAllCategories,
NAME_None,
TAttribute<FText>(),
TAttribute<FText>(),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Visible")
);
MenuBuilder.AddSeparator();
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_CopySelected,
NAME_None,
TAttribute<FText>(),
TAttribute<FText>(),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy")
);
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_CopyMessage,
NAME_None,
TAttribute<FText>(),
TAttribute<FText>(),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy")
);
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_CopyRange,
NAME_None,
TAttribute<FText>(),
TAttribute<FText>(),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy")
);
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_CopyAll,
NAME_None,
TAttribute<FText>(),
TAttribute<FText>(),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy")
);
MenuBuilder.AddSeparator();
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_SaveRange,
NAME_None,
TAttribute<FText>(),
TAttribute<FText>(),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Save")
);
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_SaveAll,
NAME_None,
TAttribute<FText>(),
TAttribute<FText>(),
FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Save")
);
MenuBuilder.AddSeparator();
// Open Source in IDE
{
FString File;
uint32 Line = 0;
if (SelectedLogMessage.IsValid())
{
FLogMessageRecord& Record = Cache.Get(SelectedLogMessage->GetIndex());
File = Record.GetFile();
Line = Record.GetLine();
}
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>("SourceCodeAccess");
ISourceCodeAccessor& SourceCodeAccessor = SourceCodeAccessModule.GetAccessor();
if (SourceCodeAccessor.CanAccessSourceCode())
{
const FText ItemLabel = FText::Format(LOCTEXT("ContextMenu_OpenSource", "Open Source in {0}"),
SourceCodeAccessor.GetNameText());
FText ItemToolTip;
if (!File.IsEmpty())
{
ItemToolTip = FText::Format(LOCTEXT("ContextMenu_OpenSource_Desc1", "Opens the source file of the selected message in {0}.\n{1} ({2})"),
SourceCodeAccessor.GetNameText(),
FText::FromString(File),
FText::AsNumber(Line, &FNumberFormattingOptions::DefaultNoGrouping()));
}
else
{
ItemToolTip = FText::Format(LOCTEXT("ContextMenu_OpenSource_Desc2", "Opens the source file of the selected message in {0}."),
SourceCodeAccessor.GetNameText());
}
MenuBuilder.AddMenuEntry
(
FLogViewCommands::Get().Command_OpenSource,
NAME_None,
ItemLabel,
ItemToolTip,
FSlateIcon(SourceCodeAccessor.GetStyleSet(), SourceCodeAccessor.GetOpenIconName())
);
}
else
{
const FText ItemLabel = LOCTEXT("ContextMenu_OpenSource_NoAccessor", "Open Source");
FText ItemToolTip;
if (!File.IsEmpty())
{
ItemToolTip = FText::Format(LOCTEXT("ContextMenu_OpenSource_NoAccessor_Desc1", "{0} ({1})\nSource Code Accessor is not available."),
FText::FromString(File),
FText::AsNumber(Line, &FNumberFormattingOptions::DefaultNoGrouping()));
}
else
{
ItemToolTip = LOCTEXT("ContextMenu_OpenSource_NoAccessor_Desc2", "Source Code Accessor is not available.");
}
MenuBuilder.AddMenuEntry
(
ItemLabel,
ItemToolTip,
FSlateIcon(SourceCodeAccessor.GetStyleSet(), SourceCodeAccessor.GetOpenIconName()),
FUIAction(FExecuteAction(), FCanExecuteAction::CreateLambda([]() { return false; })),
NAME_None,
EUserInterfaceActionType::None
);
}
}
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<SWidget> SLogView::MakeVerbosityThresholdMenu()
{
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr);
MenuBuilder.BeginSection("VerbosityThreshold"/*, LOCTEXT("LogVerbosityThresholdMenu_Section_VerbosityThreshold", "Verbosity Threshold")*/);
CreateVerbosityThresholdMenuSection(MenuBuilder);
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::CreateVerbosityThresholdMenuSection(FMenuBuilder& MenuBuilder)
{
struct FVerbosityThresholdInfo
{
ELogVerbosity::Type Verbosity;
FText Label;
FText ToolTip;
};
FVerbosityThresholdInfo VerbosityThresholds[] =
{
{ ELogVerbosity::VeryVerbose, LOCTEXT("VerbosityThresholdVeryVerbose", "VeryVerbose"), LOCTEXT("VerbosityThresholdVeryVerbose_Tooltip", "Show all log messages (any verbosity level)."), },
{ ELogVerbosity::Verbose, LOCTEXT("VerbosityThresholdVerbose", "Verbose"), LOCTEXT("VerbosityThresholdVerbose_Tooltip", "Show Verbose, Log, Display, Warning, Error and Fatal log messages (i.e. hide VeryVerbose log messages)."), },
{ ELogVerbosity::Log, LOCTEXT("VerbosityThresholdLog", "Log"), LOCTEXT("VerbosityThresholdLog_Tooltip", "Show Log, Display, Warning, Error and Fatal log messages (i.e. hide Verbose and VeryVerbose log messages)."), },
{ ELogVerbosity::Display, LOCTEXT("VerbosityThresholdDisplay", "Display"), LOCTEXT("VerbosityThresholdDisplay_Tooltip", "Show Display, Warning, Error and Fatal log messages (i.e. hide Log, Verbose and VeryVerbose log messages)."), },
{ ELogVerbosity::Warning, LOCTEXT("VerbosityThresholdWarning", "Warning"), LOCTEXT("VerbosityThresholdWarning_Tooltip", "Show only Warning, Error and Fatal log messages."), },
{ ELogVerbosity::Error, LOCTEXT("VerbosityThresholdError", "Error"), LOCTEXT("VerbosityThresholdError_Tooltip", "Show only Error and Fatal log messages."), },
{ ELogVerbosity::Fatal, LOCTEXT("VerbosityThresholdFatal", "Fatal"), LOCTEXT("VerbosityThresholdFatal_Tooltip", "Show only Fatal log messages."), },
};
for (int32 Index = 0; Index < sizeof(VerbosityThresholds) / sizeof(FVerbosityThresholdInfo); ++Index)
{
const FVerbosityThresholdInfo& Threshold = VerbosityThresholds[Index];
const TSharedRef<SWidget> TextBlock = SNew(SHorizontalBox)
#if 0 // shadow
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(STextBlock)
.Text(Threshold.Label)
.ColorAndOpacity(FSlateColor(UE::Insights::TimingProfiler::FTimeMarkerTrackBuilder::GetColorByVerbosity(Threshold.Verbosity)))
.ShadowColorAndOpacity(FLinearColor(0.02f, 0.02f, 0.02f, 1.0f))
.ShadowOffset(FVector2D(1.0f, 1.0f))
]
#else // rounded background
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(0.0f, -2.0f, 0.0f, -2.0f))
[
SNew(SBorder)
.BorderImage(FInsightsStyle::Get().GetBrush("RoundedBackground"))
.BorderBackgroundColor(FLinearColor(0.03f, 0.03f, 0.03f, 1.0f))
.Padding(FMargin(12.0f, 2.0f, 12.0f, 2.0f))
[
SNew(STextBlock)
.Text(Threshold.Label)
.ColorAndOpacity(FSlateColor(UE::Insights::TimingProfiler::FTimeMarkerTrackBuilder::GetColorByVerbosity(Threshold.Verbosity)))
.ShadowColorAndOpacity(FLinearColor(0.02f, 0.02f, 0.02f, 1.0f))
.ShadowOffset(FVector2D(1.0f, 1.0f))
]
]
#endif
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(2.0f, 0.0f, 2.0f, 0.0f))
.VAlign(VAlign_Center)
[
SNew(SBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.WidthOverride(12.0f)
.HeightOverride(12.0f)
[
SNew(SImage)
.ColorAndOpacity(this, &SLogView::VerbosityThreshold_GetSuffixColor, Threshold.Verbosity)
.Image(this, &SLogView::VerbosityThreshold_GetSuffixGlyph, Threshold.Verbosity)
]
];
MenuBuilder.AddMenuEntry
(
FUIAction(
FExecuteAction::CreateSP(this, &SLogView::VerbosityThreshold_Execute, Threshold.Verbosity),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SLogView::VerbosityThreshold_IsChecked, Threshold.Verbosity)),
TextBlock,
NAME_None,
Threshold.ToolTip,
EUserInterfaceActionType::RadioButton
);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<SWidget> SLogView::MakeCategoryFilterMenu()
{
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr);
MenuBuilder.BeginSection("QuickFilter", LOCTEXT("CategoryFilterMenu_Section_QuickFilter", "Quick Filter"));
{
MenuBuilder.AddMenuEntry
(
LOCTEXT("ShowAllCategories", "Show/Hide All"),
LOCTEXT("ShowAllCategories_Tooltip", "Change filtering to show/hide all categories"),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SLogView::ShowHideAllCategories_Execute),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SLogView::ShowHideAllCategories_IsChecked)),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
}
MenuBuilder.EndSection();
MenuBuilder.BeginSection("Categories", LOCTEXT("CategoryFilterMenu_Section_Categories", "Categories"));
CreateCategoriesFilterMenuSection(MenuBuilder);
MenuBuilder.EndSection();
const float MaxMenuHeight = FMath::Clamp(static_cast<float>(this->GetCachedGeometry().GetLocalSize().Y) - 40.0f, 100.0f, 500.0f);
return MenuBuilder.MakeWidget(nullptr, FMath::RoundToInt(MaxMenuHeight));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::CreateCategoriesFilterMenuSection(FMenuBuilder& MenuBuilder)
{
for (const FName& CategoryName : Filter.GetAvailableLogCategories())
{
const FString CategoryString = CategoryName.ToString();
const FText CategoryText(FText::AsCultureInvariant(CategoryString));
const TSharedRef<SWidget> TextBlock = SNew(STextBlock)
.Text(FText::AsCultureInvariant(CategoryString))
.ShadowColorAndOpacity(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f))
.ShadowOffset(FVector2D(1.0f, 1.0f))
.ColorAndOpacity(FSlateColor(UE::Insights::TimingProfiler::FTimeMarkerTrackBuilder::GetColorByCategory(*CategoryString)));
MenuBuilder.AddMenuEntry
(
FUIAction(
FExecuteAction::CreateSP(this, &SLogView::ToggleCategory, CategoryName),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SLogView::IsLogCategoryEnabled, CategoryName)),
TextBlock,
NAME_None,
FText::Format(LOCTEXT("Category_Tooltip", "Filter the Log View to show/hide category: {0}"), CategoryText),
EUserInterfaceActionType::ToggleButton
);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::VerbosityThreshold_IsChecked(ELogVerbosity::Type Verbosity) const
{
return Verbosity == Filter.GetVerbosityThreshold();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::VerbosityThreshold_Execute(ELogVerbosity::Type Verbosity)
{
Filter.SetVerbosityThreshold(Verbosity);
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
const FSlateBrush* SLogView::VerbosityThreshold_GetSuffixGlyph(ELogVerbosity::Type Verbosity) const
{
return Verbosity <= Filter.GetVerbosityThreshold() ?
FAppStyle::Get().GetBrush("Icons.Check") :
FAppStyle::Get().GetBrush("Icons.X");
}
////////////////////////////////////////////////////////////////////////////////////////////////////
FSlateColor SLogView::VerbosityThreshold_GetSuffixColor(ELogVerbosity::Type Verbosity) const
{
return Verbosity <= Filter.GetVerbosityThreshold() ?
FSlateColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)) :
FSlateColor(FLinearColor(1.0f, 0.0f, 0.0f, 1.0f));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::ShowHideAllCategories_IsChecked() const
{
return Filter.IsShowAllCategoriesEnabled();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::ShowHideAllCategories_Execute()
{
Filter.ToggleShowAllCategories();
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::IsLogCategoryEnabled(FName InName) const
{
return Filter.IsLogCategoryEnabled(InName);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::ToggleCategory(FName InName)
{
Filter.ToggleLogCategory(InName);
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanHideSelectedCategory() const
{
return ListView->GetSelectedItems().Num() == 1;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::HideSelectedCategory()
{
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
if (SelectedLogMessage.IsValid())
{
FLogMessageRecord& Record = Cache.Get(SelectedLogMessage->GetIndex());
FName CategoryName(Record.GetCategory());
Filter.DisableLogCategory(CategoryName);
OnFilterChanged();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanShowOnlySelectedCategory() const
{
return ListView->GetSelectedItems().Num() == 1;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::ShowOnlySelectedCategory()
{
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
if (SelectedLogMessage.IsValid())
{
FLogMessageRecord& Record = Cache.Get(SelectedLogMessage->GetIndex());
FName CategoryName(Record.GetCategory());
Filter.EnableOnlyCategory(CategoryName);
OnFilterChanged();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanShowAllCategories() const
{
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::ShowAllCategories()
{
if (Filter.IsShowAllCategoriesEnabled())
{
Filter.ToggleShowAllCategories(); // hide
Filter.ToggleShowAllCategories(); // show
}
else
{
Filter.ToggleShowAllCategories(); // show
}
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
FReply SLogView::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
return CommandList->ProcessCommandBindings(InKeyEvent) == true ? FReply::Handled() : FReply::Unhandled();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::AppendFormatMessageDetailed(const FLogMessageRecord& Log, TStringBuilderBase<TCHAR>& InOutStringBuilder) const
{
using namespace UE::Insights;
InOutStringBuilder.Appendf(TEXT("Index=%d"), Log.GetIndex());
InOutStringBuilder.Append(TEXT("\nTime="));
InOutStringBuilder.Append(FormatTimeHMS(Log.GetTime(), FTimeValue::Microsecond));
InOutStringBuilder.Append(TEXT("\nVerbosity="));
InOutStringBuilder.Append(::ToString(Log.GetVerbosity()));
InOutStringBuilder.Append(TEXT("\nCategory="));
InOutStringBuilder.Append(Log.GetCategoryAsString());
InOutStringBuilder.Append(TEXT("\nMessage="));
InOutStringBuilder.Append(Log.GetMessageAsString());
InOutStringBuilder.Append(TEXT("\nFile="));
InOutStringBuilder.Append(Log.GetFile());
InOutStringBuilder.Appendf(TEXT("\nLine=%d\n"), Log.GetLine());
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::AppendFormatMessageDelimitedHeader(TStringBuilderBase<TCHAR>& InOutStringBuilder, TCHAR Separator) const
{
InOutStringBuilder.Append(TEXT("Index"));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(TEXT("Time"));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(TEXT("Verbosity"));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(TEXT("Category"));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(TEXT("Message"));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(TEXT("File"));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(TEXT("Line"));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::AppendFormatMessageDelimited(const FLogMessageRecord& Log, TStringBuilderBase<TCHAR>& InOutStringBuilder, TCHAR Separator) const
{
InOutStringBuilder.Appendf(TEXT("%d"), Log.GetIndex());
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(UE::Insights::FormatTimeHMS(Log.GetTime(), UE::Insights::FTimeValue::Microsecond));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(::ToString(Log.GetVerbosity()));
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(Log.GetCategoryAsString());
InOutStringBuilder.AppendChar(Separator);
FString Message = Log.GetMessageAsString();
if (Separator == TEXT('\t'))
{
Message.ReplaceCharInline(TEXT('\t'), TEXT(' '), ESearchCase::CaseSensitive);
InOutStringBuilder.Append(Message);
}
else if (Separator == TEXT(','))
{
InOutStringBuilder.AppendChar(TEXT('\"'));
FString EscapedMessage = Message.Replace(TEXT("\""), TEXT("\"\""), ESearchCase::CaseSensitive);
InOutStringBuilder.Append(EscapedMessage);
InOutStringBuilder.AppendChar(TEXT('\"'));
}
else
{
InOutStringBuilder.Append(Message);
}
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Append(Log.GetFile());
InOutStringBuilder.AppendChar(Separator);
InOutStringBuilder.Appendf(TEXT("%d"), Log.GetLine());
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanCopySelected() const
{
return ListView->GetSelectedItems().Num() == 1;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::CopySelected() const
{
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
if (SelectedLogMessage)
{
const FLogMessageRecord& Log = Cache.Get(SelectedLogMessage->GetIndex());
TStringBuilder<1024> StringBuilder;
AppendFormatMessageDetailed(Log, StringBuilder);
FPlatformApplicationMisc::ClipboardCopy(StringBuilder.ToString());
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanCopyMessage() const
{
return ListView->GetSelectedItems().Num() == 1;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::CopyMessage() const
{
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
if (SelectedLogMessage)
{
const FLogMessageRecord& Log = Cache.Get(SelectedLogMessage->GetIndex());
FPlatformApplicationMisc::ClipboardCopy(*Log.GetMessageAsString());
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanCopyRange() const
{
if (FilteredMessages.Num() == 0)
{
return false;
}
TSharedPtr<UE::Insights::TimingProfiler::STimingView> TimingView = GetTimingView();
if (!TimingView)
{
return false;
}
const double SelectionStartTime = TimingView->GetSelectionStartTime();
const double SelectionEndTime = TimingView->GetSelectionEndTime();
return SelectionStartTime < SelectionEndTime;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::CopyRange() const
{
if (FilteredMessages.Num() == 0)
{
return;
}
TSharedPtr<UE::Insights::TimingProfiler::STimingView> TimingView = GetTimingView();
if (!TimingView)
{
return;
}
const double SelectionStartTime = TimingView->GetSelectionStartTime();
const double SelectionEndTime = TimingView->GetSelectionEndTime();
if (SelectionStartTime >= SelectionEndTime)
{
return;
}
TStringBuilder<1024> StringBuilder;
AppendFormatMessageDelimitedHeader(StringBuilder, TEXT('\t'));
StringBuilder.AppendChar(TEXT('\n'));
int32 NumMessagesInSelectedRange = 0;
TSharedPtr<const TraceServices::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
if (Session.IsValid())
{
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get());
for (const TSharedPtr<FLogMessage>& Message : FilteredMessages)
{
LogProvider.ReadMessage(Message->GetIndex(), [this, SelectionStartTime, SelectionEndTime, &StringBuilder, &NumMessagesInSelectedRange](const TraceServices::FLogMessageInfo& MessageInfo)
{
if (MessageInfo.Time >= SelectionStartTime && MessageInfo.Time <= SelectionEndTime)
{
FLogMessageRecord Log(MessageInfo);
AppendFormatMessageDelimited(Log, StringBuilder, TEXT('\t'));
StringBuilder.AppendChar(TEXT('\n'));
++NumMessagesInSelectedRange;
}
});
}
}
if (NumMessagesInSelectedRange > 0)
{
FPlatformApplicationMisc::ClipboardCopy(StringBuilder.ToString());
UE_LOG(TraceInsights, Log, TEXT("Copied %d logs to clipboard."), NumMessagesInSelectedRange);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanCopyAll() const
{
return FilteredMessages.Num() > 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::CopyAll() const
{
if (FilteredMessages.Num() == 0)
{
return;
}
TStringBuilder<1024> StringBuilder;
AppendFormatMessageDelimitedHeader(StringBuilder, TEXT('\t'));
StringBuilder.AppendChar(TEXT('\n'));
TSharedPtr<const TraceServices::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
if (Session.IsValid())
{
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get());
for (const TSharedPtr<FLogMessage>& Message : FilteredMessages)
{
LogProvider.ReadMessage(Message->GetIndex(), [this, &StringBuilder](const TraceServices::FLogMessageInfo& MessageInfo)
{
FLogMessageRecord Log(MessageInfo);
AppendFormatMessageDelimited(Log, StringBuilder, TEXT('\t'));
StringBuilder.AppendChar(TEXT('\n'));
});
}
}
FPlatformApplicationMisc::ClipboardCopy(StringBuilder.ToString());
UE_LOG(TraceInsights, Log, TEXT("Copied %d logs to clipboard."), FilteredMessages.Num());
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanSaveRange() const
{
return CanCopyRange();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::SaveRange() const
{
constexpr bool bSaveLogsInSelectedRangeOnly = true;
SaveLogsToFile(bSaveLogsInSelectedRangeOnly);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanSaveAll() const
{
return FilteredMessages.Num() > 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::SaveAll() const
{
constexpr bool bSaveLogsInSelectedRangeOnly = false;
SaveLogsToFile(bSaveLogsInSelectedRangeOnly);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::SaveLogsToFile(bool bSaveLogsInSelectedRangeOnly) const
{
if (FilteredMessages.Num() == 0)
{
return;
}
double SelectionStartTime = 0.0;
double SelectionEndTime = 0.0;
if (bSaveLogsInSelectedRangeOnly)
{
TSharedPtr<UE::Insights::TimingProfiler::STimingView> TimingView = GetTimingView();
if (!TimingView)
{
return;
}
SelectionStartTime = TimingView->GetSelectionStartTime();
SelectionEndTime = TimingView->GetSelectionEndTime();
if (SelectionStartTime >= SelectionEndTime)
{
return;
}
}
TArray<FString> SaveFilenames;
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
bool bDialogResult = false;
if (DesktopPlatform)
{
const FString DefaultBrowsePath = FPaths::ProjectLogDir();
bDialogResult = DesktopPlatform->SaveFileDialog(
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
LOCTEXT("SaveLogsToFileTitle", "Save Logs").ToString(),
DefaultBrowsePath,
TEXT(""),
TEXT("Comma-Separated Values (*.csv)|*.csv|Tab-Separated Values (*.tsv)|*.tsv|Text Files (*.txt)|*.txt|All Files (*.*)|*.*"),
EFileDialogFlags::None,
SaveFilenames
);
}
if (!bDialogResult || SaveFilenames.Num() == 0)
{
return;
}
FString& Path = SaveFilenames[0];
TCHAR Separator = TEXT('\t');
if (Path.EndsWith(TEXT(".csv")))
{
Separator = TEXT(',');
}
IFileHandle* ExportFileHandle = FPlatformFileManager::Get().GetPlatformFile().OpenWrite(*Path);
if (ExportFileHandle == nullptr)
{
FMessageLog ReportMessageLog(FInsightsManager::Get()->GetLogListingName());
ReportMessageLog.Error(LOCTEXT("FailedToOpenFile", "Save logs failed. Failed to open file for write."));
ReportMessageLog.Notify();
return;
}
UE::Insights::FStopwatch Stopwatch;
Stopwatch.Start();
UTF16CHAR BOM = UNICODE_BOM;
ExportFileHandle->Write((uint8*)&BOM, sizeof(UTF16CHAR));
// Write header.
{
TStringBuilder<1024> StringBuilder;
AppendFormatMessageDelimitedHeader(StringBuilder, Separator);
StringBuilder.AppendChar(TEXT('\n'));
FTCHARToUTF16 UTF16String(StringBuilder.ToString(), StringBuilder.Len());
ExportFileHandle->Write((const uint8*)UTF16String.Get(), UTF16String.Length() * sizeof(UTF16CHAR));
}
int32 NumMessagesInSelectedRange = 0;
TSharedPtr<const TraceServices::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
if (Session.IsValid())
{
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get());
for (const TSharedPtr<FLogMessage>& Message : FilteredMessages)
{
LogProvider.ReadMessage(Message->GetIndex(), [this, bSaveLogsInSelectedRangeOnly, SelectionStartTime, SelectionEndTime, Separator, ExportFileHandle, &NumMessagesInSelectedRange](const TraceServices::FLogMessageInfo& MessageInfo)
{
if (!bSaveLogsInSelectedRangeOnly || (MessageInfo.Time >= SelectionStartTime && MessageInfo.Time <= SelectionEndTime))
{
TStringBuilder<1024> StringBuilder;
FLogMessageRecord Log(MessageInfo);
AppendFormatMessageDelimited(Log, StringBuilder, Separator);
StringBuilder.AppendChar(TEXT('\n'));
FTCHARToUTF16 UTF16String(StringBuilder.ToString(), StringBuilder.Len());
ExportFileHandle->Write((const uint8*)UTF16String.Get(), UTF16String.Length() * sizeof(UTF16CHAR));
++NumMessagesInSelectedRange;
}
});
}
}
ExportFileHandle->Flush();
delete ExportFileHandle;
ExportFileHandle = nullptr;
Stopwatch.Stop();
const double TotalTime = Stopwatch.GetAccumulatedTime();
UE_LOG(TraceInsights, Log, TEXT("Saved %d logs to file in %.3fs."), NumMessagesInSelectedRange, TotalTime);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::CanOpenSource() const
{
return GetSelectedLogMessage().IsValid();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::OpenSource() const
{
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
if (SelectedLogMessage.IsValid())
{
FLogMessageRecord& Record = Cache.Get(SelectedLogMessage->GetIndex());
const FString File = Record.GetFile();
const uint32 Line = Record.GetLine();
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>("SourceCodeAccess");
if (FPaths::FileExists(File))
{
ISourceCodeAccessor& SourceCodeAccessor = SourceCodeAccessModule.GetAccessor();
SourceCodeAccessor.OpenFileAtLine(File, Line);
}
else
{
SourceCodeAccessModule.OnOpenFileFailed().Broadcast(File);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedPtr<UE::Insights::TimingProfiler::STimingView> SLogView::GetTimingView() const
{
using namespace UE::Insights::TimingProfiler;
TSharedPtr<STimingProfilerWindow> ProfilerWindow = FTimingProfilerManager::Get()->GetProfilerWindow();
return ProfilerWindow.IsValid() ? ProfilerWindow->GetTimingView() : nullptr;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
} // namespace UE::Insights
#undef LOCTEXT_NAMESPACE