EM_Task/TraceInsights/Private/Insights/Widgets/SLogView.cpp

1253 lines
51 KiB
C++
Raw Permalink Normal View History

2026-02-13 16:18:33 +08:00
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SLogView.h"
#include "Algo/BinarySearch.h"
#include "Async/AsyncWork.h"
#include "EditorFontGlyphs.h"
#include "EditorStyleSet.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "SlateOptMacros.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Text/STextBlock.h"
// Insights
#include "Insights/Common/TimeUtils.h"
#include "Insights/InsightsManager.h"
#include "Insights/InsightsStyle.h"
#include "Insights/Log.h"
#include "Insights/TimingProfilerManager.h"
#include "Insights/ViewModels/MarkersTimingTrack.h" // for FTimeMarkerTrackBuilder::GetColorBy*
#include "Insights/ViewModels/TimingViewDrawHelper.h"
#include "Insights/Widgets/STimingProfilerWindow.h"
#include "Insights/Widgets/STimingView.h"
////////////////////////////////////////////////////////////////////////////////////////////////////
#define LOCTEXT_NAMESPACE "SLogView"
////////////////////////////////////////////////////////////////////////////////////////////////////
// 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.Time;
TSharedPtr<STimingProfilerWindow> Window = FTimingProfilerManager::Get()->GetProfilerWindow();
if (Window)
{
TSharedPtr<STimingView> TimingView = Window->GetTimingView();
if (TimingView)
{
if (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.Time;
TSharedPtr<STimingProfilerWindow> Window = FTimingProfilerManager::Get()->GetProfilerWindow();
if (Window)
{
TSharedPtr<STimingView> TimingView = Window->GetTimingView();
if (TimingView)
{
if (TimingView->IsTimeSelectedInclusive(Time))
{
if (IsSelected)
{
// return FSlateColor(FLinearColor(0.0f, 0.1f, 0.5f, 1.0f));
return FSlateColor(FLinearColor(0.0f, 0.05f, 0.2f, 1.0f));
}
else
{
// return FSlateColor(FLinearColor(0.2f, 0.4f, 0.8f, 1.0f));
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(FTimeMarkerTrackBuilder::GetColorByVerbosity(CacheEntry.Verbosity));
}
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(FTimeMarkerTrackBuilder::GetColorByCategory(*CacheEntry.Category.ToString()));
}
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), bIsDirty(false)
{
}
////////////////////////////////////////////////////////////////////////////////////////////////////
SLogView::~SLogView()
{
// Remove ourselves from the profiler manager.
// if (FTimingProfilerManager::Get().IsValid())
//{
// //TODO: FTimimgProfilerManager::Get()->OnRequestLogViewUpdate().RemoveAll(this);
//}
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;
bIsDirty = false;
DirtyStopwatch.Stop();
StatsText = FText::GetEmpty();
Cache.Reset();
Messages.Reset();
ListView->RebuildList();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SLogView::Construct(const FArguments& InArgs)
{
SAssignNew(ExternalScrollbar, SScrollBar)
.AlwaysShowScrollbar(true);
ChildSlot
[SNew(SVerticalBox)
// Toolbar
+ SVerticalBox::Slot()
.AutoHeight()
[SNew(SHorizontalBox)
// Icon
//+ SHorizontalBox::Slot()
//.VAlign(VAlign_Center)
//.AutoWidth()
//[
// SNew(SImage)
// .Image(FEditorStyle::GetBrush(TEXT("Log.TabIcon")))
//]
//
//// Label
//+ SHorizontalBox::Slot()
//.VAlign(VAlign_Center)
//.Padding(2.0f, 0.0f)
//.AutoWidth()
//[
// SNew(STextBlock)
// .Text(LOCTEXT("LogViewLabel", "Log View"))
//]
// Verbosity Threshold Filter
+ SHorizontalBox::Slot()
//.Padding(2.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
.AutoWidth()
[SNew(SComboButton)
.ComboButtonStyle(FEditorStyle::Get(), "GenericFilters.ComboButtonStyle")
.ForegroundColor(FLinearColor::White)
.ContentPadding(0)
.ToolTipText(LOCTEXT("VerbosityThresholdFilterToolTip", "Filter log messages by verbosity threshold."))
.OnGetMenuContent(this, &SLogView::MakeVerbosityThresholdMenu)
.HasDownArrow(true)
.ContentPadding(FMargin(1.0f, 1.0f, 1.0f, 0.0f))
.ButtonContent()
[SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(0.0f)
.AutoWidth()
[SNew(STextBlock)
.TextStyle(FEditorStyle::Get(), "GenericFilters.TextStyle")
.Font(FEditorStyle::Get().GetFontStyle("FontAwesome.9"))
.Text(FText::FromString(FString(TEXT("\xf0b0"))) /*fa-filter*/)]
+ SHorizontalBox::Slot()
.Padding(2.0f, 0.0f, 0.0f, 0.0f)
.AutoWidth()
[SNew(STextBlock)
.TextStyle(FEditorStyle::Get(), "GenericFilters.TextStyle")
.Text(LOCTEXT("VerbosityThresholdFilter", "Verbosity Threshold"))]]]
// Category Filter
+ SHorizontalBox::Slot()
.Padding(2.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
.AutoWidth()
[SNew(SComboButton)
.ComboButtonStyle(FEditorStyle::Get(), "GenericFilters.ComboButtonStyle")
.ForegroundColor(FLinearColor::White)
.ContentPadding(0)
.ToolTipText(LOCTEXT("CategoryFilterToolTip", "Filter log messages by category."))
.OnGetMenuContent(this, &SLogView::MakeCategoryFilterMenu)
.HasDownArrow(true)
.ContentPadding(FMargin(1.0f, 1.0f, 1.0f, 0.0f))
.ButtonContent()
[SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(0.0f)
.AutoWidth()
[SNew(STextBlock)
.TextStyle(FEditorStyle::Get(), "GenericFilters.TextStyle")
.Font(FEditorStyle::Get().GetFontStyle("FontAwesome.9"))
.Text(FText::FromString(FString(TEXT("\xf0b0"))) /*fa-filter*/)]
+ SHorizontalBox::Slot()
.Padding(2.0f, 0.0f, 0.0f, 0.0f)
.AutoWidth()
[SNew(STextBlock)
.TextStyle(FEditorStyle::Get(), "GenericFilters.TextStyle")
.Text(LOCTEXT("CategoryFilter", "Category Filter"))]]]
// Text Filter (Search Box)
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.Padding(2.0f, 0.0f)
.AutoWidth()
[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
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.Padding(2.0f, 0.0f)
.AutoWidth()
[SNew(STextBlock)
.Text(this, &SLogView::GetStatsText)
.ColorAndOpacity(this, &SLogView::GetStatsTextColor)]]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[SNew(SBox)
.VAlign(VAlign_Fill)
[SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f)
.VAlign(VAlign_Fill)
[SNew(SScrollBox)
.Orientation(Orient_Horizontal)
+ SScrollBox::Slot()
.VAlign(VAlign_Fill)
[SNew(SBorder)
.BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder"))
.Padding(0.0f)
[SAssignNew(ListView, SListView<TSharedPtr<FLogMessage>>)
.ExternalScrollbar(ExternalScrollbar)
.ItemHeight(20.0f)
.SelectionMode(ESelectionMode::Single)
.OnMouseButtonClick(this, &SLogView::OnMouseButtonClick)
.OnSelectionChanged(this, &SLogView::OnSelectionChanged)
.ListItemsSource(&Messages)
.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)
[SNew(SBox)
.WidthOverride(FOptionalSize(13.0f))
[ExternalScrollbar.ToSharedRef()]]]]];
// Register ourselves with the profiler manager.
// TODO: FTimingProfilerManager::Get()->OnRequestLogViewUpdate().AddSP(this, &SLogView::ProfilerManager_OnRequestLogViewUpdate);
}
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::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
int32 NewMessageCount = 0;
TSharedPtr<const Trace::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
Cache.SetSession(Session);
if (Session.IsValid())
{
Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const Trace::ILogProvider& LogProvider = Trace::ReadLogProvider(*Session.Get());
NewMessageCount = static_cast<int32>(LogProvider.GetMessageCount());
// TODO: show only categories that are used in current trace
// TODO: cause of duplicates: a) runtime, b) case insensitive, c) stripped "Log" prefix
const int32 NumCategories = static_cast<int32>(LogProvider.GetCategoryCount());
if (NumCategories != TotalNumCategories)
{
TotalNumCategories = NumCategories;
UE_LOG(TraceInsights, Log, TEXT("[LogView] Total Log Categories: %d"), TotalNumCategories);
TSet<FName> Categories;
LogProvider.EnumerateCategories([&Categories](const Trace::FLogCategory& Category)
{
FString CategoryStr(Category.Name);
if (CategoryStr.StartsWith(TEXT("Log")))
{
CategoryStr.RightChopInline(3, false);
}
if (Categories.Contains(FName(*CategoryStr)))
{
UE_LOG(TraceInsights, Log, TEXT("[LogView] Duplicated Log Category: \"%s\""), Category.Name);
}
Categories.Add(FName(*CategoryStr));
});
Filter.SyncAvailableCategories(Categories);
UE_LOG(TraceInsights, Log, TEXT("[LogView] Unique Log Categories: %d"), Filter.GetAvailableLogCategories().Num());
// Cache.Reset();
Messages.Reset();
TotalNumMessages = 0;
// ListView->RebuildList();
bIsDirty = true;
DirtyStopwatch.Start();
}
}
if (NewMessageCount != TotalNumMessages)
{
bIsDirty = true;
DirtyStopwatch.Start();
if (NewMessageCount > TotalNumMessages)
{
if (Filter.IsFilterSet())
{
// Filter messages async.
if (!FilteringAsyncTask.IsValid())
{
FilteringStopwatch.Restart();
bIsFilteringAsyncTaskCancelRequested = false;
FilteringStartIndex = TotalNumMessages;
FilteringEndIndex = NewMessageCount;
FilteringChangeNumber = Filter.GetChangeNumber();
FilteringAsyncTask = MakeUnique<FAsyncTask<FLogFilteringAsyncTask>>(FilteringStartIndex, FilteringEndIndex, Filter, SharedThis(this));
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);
FilteringAsyncTask->StartBackgroundTask();
}
else
{
// A task is already in progress.
if (FilteringStartIndex == TotalNumMessages &&
FilteringEndIndex <= NewMessageCount &&
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 < NewMessageCount; Index++)
{
Messages.Add(MakeShared<FLogMessage>(Index));
}
const int32 NumAddedMessages = NewMessageCount - TotalNumMessages;
TotalNumMessages = NewMessageCount;
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
ListView->RebuildList();
if (SelectedLogMessage.IsValid())
{
// Restore selection.
SelectedLogMessageByLogIndex(SelectedLogMessage->GetIndex());
}
bIsDirty = false;
DirtyStopwatch.Reset();
UpdateStatsText();
FilteringStopwatch.Stop();
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, NewMessageCount, DurationMs);
}
}
}
else // if (NewMessageCount < TotalNumMessages)
{
// Just reset. On next Tick() the list will grow if needed.
UE_LOG(TraceInsights, Log, TEXT("[LogView] RESET"));
Cache.Reset();
Messages.Reset();
TotalNumMessages = 0;
ListView->RebuildList();
bIsDirty = (NewMessageCount != 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 <= NewMessageCount &&
FilteringChangeNumber == Filter.GetChangeNumber())
{
FLogFilteringAsyncTask& Task = FilteringAsyncTask->GetTask();
const TArray<uint32>& FilteredMessages = Task.GetFilteredMessages();
// Add filtered messages to current Messages array.
const int32 NumFilteredMessages = FilteredMessages.Num();
for (int32 Index = 0; Index < NumFilteredMessages; Index++)
{
Messages.Add(MakeShared<FLogMessage>(FilteredMessages[Index]));
}
TotalNumMessages = Task.GetEndIndex();
TSharedPtr<FLogMessage> SelectedLogMessage = GetSelectedLogMessage();
ListView->RebuildList();
if (SelectedLogMessage.IsValid())
{
// Restore selection.
SelectedLogMessageByLogIndex(SelectedLogMessage->GetIndex());
}
bIsDirty = false;
DirtyStopwatch.Reset();
UpdateStatsText();
FilteringStopwatch.Stop();
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);
}
}
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::SelectedLogMessageByLogIndex(int32 LogIndex)
{
// We are assuming the Messages list is sorted by log index...
int32 MessageIndex = Algo::BinarySearchBy(Messages, LogIndex, &FLogMessage::GetIndex);
if (MessageIndex != INDEX_NONE)
{
ListView->SetItemSelection(Messages[MessageIndex], true);
ListView->RequestScrollIntoView(Messages[MessageIndex]);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::SelectLogMessage(TSharedPtr<FLogMessage> LogMessage)
{
if (LogMessage.IsValid())
{
TSharedPtr<STimingProfilerWindow> Window = FTimingProfilerManager::Get()->GetProfilerWindow();
if (Window)
{
TSharedPtr<STimingView> TimingView = Window->GetTimingView();
if (TimingView)
{
const double Time = Cache.Get(LogMessage->GetIndex()).Time;
if (FSlateApplication::Get().GetModifierKeys().IsShiftDown())
{
TimingView->SelectToTimeMarker(Time);
}
else
{
TimingView->SetAndCenterOnTimeMarker(Time);
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::OnMouseButtonClick(TSharedPtr<FLogMessage> LogMessage)
{
SelectLogMessage(LogMessage);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::OnSelectionChanged(TSharedPtr<FLogMessage> LogMessage, ESelectInfo::Type SelectInfo)
{
if (SelectInfo != ESelectInfo::Direct &&
SelectInfo != ESelectInfo::OnMouseClick)
{
SelectLogMessage(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();
UE_LOG(TraceInsights, Log, TEXT("[LogView] OnFilterChanged: \"%s\""), *FilterText);
Cache.Reset();
Messages.Reset();
TotalNumMessages = 0;
bIsDirty = true;
DirtyStopwatch.Start();
// The next Tick() will update the filtered list of messages.
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::UpdateStatsText()
{
if (Messages.Num() == TotalNumMessages)
{
StatsText = FText::Format(LOCTEXT("StatsText1", "{0} logs"), FText::AsNumber(TotalNumMessages));
}
else
{
StatsText = FText::Format(LOCTEXT("StatsText2", "{0} / {1} logs"), FText::AsNumber(Messages.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();
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr);
MenuBuilder.BeginSection("LogViewContextMenu");
{
if (SelectedLogMessage.IsValid())
{
FLogMessageRecord& Record = Cache.Get(SelectedLogMessage->GetIndex());
FName CategoryName(*Record.Category.ToString());
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("HideCategory", "Hide \"{0}\" Category"), Record.Category),
FText::Format(LOCTEXT("HideCategory_Tooltip", "Hide the \"{0}\" log category."), Record.Category),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SLogView::ToggleCategory_Execute, CategoryName)),
NAME_None,
EUserInterfaceActionType::Button);
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("ShowOnlyCategory", "Show Only \"{0}\" Category"), Record.Category),
FText::Format(LOCTEXT("ShowOnlyCategory_Tooltip", "Show only the \"{0}\" log category (hide all other log categories)."), Record.Category),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SLogView::ShowOnlyCategory_Execute, CategoryName)),
NAME_None,
EUserInterfaceActionType::Button);
}
MenuBuilder.AddMenuEntry(
LOCTEXT("ShowAllCategoriesCtxMenu", "Show All Categories"),
LOCTEXT("ShowAllCategoriesCtxMenu_Tooltip", "Reset category filter (show all log categories)."),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SLogView::ShowAllCategories_Execute)),
NAME_None,
EUserInterfaceActionType::Button);
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<SWidget> SLogView::MakeVerbosityThresholdMenu()
{
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr);
MenuBuilder.BeginSection("LogViewVerbosityThreshold" /*, LOCTEXT("VerbosityThresholdHeading", "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) + SHorizontalBox::Slot().AutoWidth()[SNew(STextBlock).Text(Threshold.Label).ShadowColorAndOpacity(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)).ShadowOffset(FVector2D(1.0f, 1.0f)).ColorAndOpacity(FSlateColor(FTimeMarkerTrackBuilder::GetColorByVerbosity(Threshold.Verbosity)))] + SHorizontalBox::Slot().Padding(2.0f, 0.0f).VAlign(VAlign_Center)[SNew(STextBlock).Font(FEditorStyle::Get().GetFontStyle("FontAwesome.9")).Text(this, &SLogView::VerbosityThreshold_GetSuffixGlyph, Threshold.Verbosity).ColorAndOpacity(this, &SLogView::VerbosityThreshold_GetSuffixColor, 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("LogViewShowAllCategories", LOCTEXT("QuickFilterHeading", "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("LogViewCategoriesEntries", LOCTEXT("CategoriesHeading", "Categories"));
CreateCategoriesFilterMenuSection(MenuBuilder);
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
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(FTimeMarkerTrackBuilder::GetColorByCategory(*CategoryString)));
MenuBuilder.AddMenuEntry(
FUIAction(FExecuteAction::CreateSP(this, &SLogView::ToggleCategory_Execute, CategoryName),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SLogView::ToggleCategory_IsChecked, 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();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
FText SLogView::VerbosityThreshold_GetSuffixGlyph(ELogVerbosity::Type Verbosity) const
{
return Verbosity <= Filter.GetVerbosityThreshold() ? FEditorFontGlyphs::Check : FEditorFontGlyphs::Times;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
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();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::ShowAllCategories_Execute()
{
if (Filter.IsShowAllCategoriesEnabled())
{
Filter.ToggleShowAllCategories(); // hide
Filter.ToggleShowAllCategories(); // show
}
else
{
Filter.ToggleShowAllCategories(); // show
}
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::ShowOnlyCategory_Execute(FName InName)
{
Filter.EnableOnlyCategory(InName);
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool SLogView::ToggleCategory_IsChecked(FName InName) const
{
return Filter.IsLogCategoryEnabled(InName);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void SLogView::ToggleCategory_Execute(FName InName)
{
Filter.ToggleLogCategory(InName);
OnFilterChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE