// 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 { public: FLogViewCommands() : TCommands( 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 Command_HideSelectedCategory; TSharedPtr Command_ShowOnlySelectedCategory; TSharedPtr Command_ShowAllCategories; TSharedPtr Command_CopySelected; TSharedPtr Command_CopyMessage; TSharedPtr Command_CopyRange; TSharedPtr Command_CopyAll; TSharedPtr Command_SaveRange; TSharedPtr Command_SaveAll; TSharedPtr Command_OpenSource; }; //////////////////////////////////////////////////////////////////////////////////////////////////// // SLogMessageRow //////////////////////////////////////////////////////////////////////////////////////////////////// class SLogMessageRow : public SMultiColumnTableRow> { 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 InLogMessage, TSharedRef InParentWidget, const TSharedRef& InOwnerTableView) { WeakLogMessage = MoveTemp(InLogMessage); WeakParentWidget = InParentWidget; SMultiColumnTableRow>::Construct(FSuperRowType::FArguments(), InOwnerTableView); TSharedRef Row = ChildSlot.GetChildAt(0); ChildSlot [ SNew(SBorder) .BorderImage(FInsightsStyle::Get().GetBrush("WhiteBrush")) .BorderBackgroundColor(this, &SLogMessageRow::GetBackgroundColor) [ Row ] ]; } public: virtual TSharedRef 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr LogMessagePin = WeakLogMessage.Pin(); if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid()) { TSharedPtr 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 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr LogMessagePin = WeakLogMessage.Pin(); if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid()) { TSharedPtr 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 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); if (ParentWidgetPin.IsValid()) { return ParentWidgetPin->GetFilterText(); } else { return FText(); } } FText GetFile() const { TSharedPtr ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr 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 ParentWidgetPin = WeakParentWidget.Pin(); TSharedPtr LogMessagePin = WeakLogMessage.Pin(); if (ParentWidgetPin.IsValid() && LogMessagePin.IsValid()) { FLogMessageRecord& CacheEntry = ParentWidgetPin->GetCache().Get(LogMessagePin->GetIndex()); return CacheEntry.ToDisplayString(); } else { return FText(); } } private: TWeakPtr WeakLogMessage; TWeakPtr 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(), 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>) .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 SLogView::OnGenerateRow(TSharedPtr InLogMessage, const TSharedRef& 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(); 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 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(LogProvider.GetCategoryCount()); LogProviderNumMessages = static_cast(LogProvider.GetMessageCount()); LogProviderNumInserts = static_cast(LogProvider.GetInsertCount()); } if (LogProviderNumCategories != TotalNumCategories) { TSet Categories; TMap 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(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>(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(Index)); } const int32 NumAddedMessages = LogProviderNumMessages - TotalNumMessages; TotalNumMessages = LogProviderNumMessages; TSharedPtr 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& 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(FilteredMessageIndices[Index])); } TotalNumMessages = Task.GetEndIndex(); TSharedPtr 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(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 SLogView::GetSelectedLogMessage() const { TArray> SelectedItems = ListView->GetSelectedItems(); return (SelectedItems.Num() == 1) ? SelectedItems[0] : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SLogView::SelectLogMessage(TSharedPtr 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 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 LogMessage) { if (LogMessage.IsValid()) { TSharedPtr 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 LogMessage) { OnSelectedLogMessageChanged(LogMessage); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SLogView::OnSelectionChanged(TSharedPtr 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 SLogView::ListView_GetContextMenu() { TSharedPtr 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(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Visible") ); MenuBuilder.AddSeparator(); MenuBuilder.AddMenuEntry ( FLogViewCommands::Get().Command_CopySelected, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy") ); MenuBuilder.AddMenuEntry ( FLogViewCommands::Get().Command_CopyMessage, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy") ); MenuBuilder.AddMenuEntry ( FLogViewCommands::Get().Command_CopyRange, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy") ); MenuBuilder.AddMenuEntry ( FLogViewCommands::Get().Command_CopyAll, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy") ); MenuBuilder.AddSeparator(); MenuBuilder.AddMenuEntry ( FLogViewCommands::Get().Command_SaveRange, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Save") ); MenuBuilder.AddMenuEntry ( FLogViewCommands::Get().Command_SaveAll, NAME_None, TAttribute(), TAttribute(), 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("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 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 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 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(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 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 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 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& 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& 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& 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 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 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 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 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 Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get()); for (const TSharedPtr& 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 Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get()); for (const TSharedPtr& 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 TimingView = GetTimingView(); if (!TimingView) { return; } SelectionStartTime = TimingView->GetSelectionStartTime(); SelectionEndTime = TimingView->GetSelectionEndTime(); if (SelectionStartTime >= SelectionEndTime) { return; } } TArray 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 Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const TraceServices::ILogProvider& LogProvider = TraceServices::ReadLogProvider(*Session.Get()); for (const TSharedPtr& 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 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("SourceCodeAccess"); if (FPaths::FileExists(File)) { ISourceCodeAccessor& SourceCodeAccessor = SourceCodeAccessModule.GetAccessor(); SourceCodeAccessor.OpenFileAtLine(File, Line); } else { SourceCodeAccessModule.OnOpenFileFailed().Broadcast(File); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedPtr SLogView::GetTimingView() const { using namespace UE::Insights::TimingProfiler; TSharedPtr ProfilerWindow = FTimingProfilerManager::Get()->GetProfilerWindow(); return ProfilerWindow.IsValid() ? ProfilerWindow->GetTimingView() : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// } // namespace UE::Insights #undef LOCTEXT_NAMESPACE