Files
UnrealEngine/Engine/Plugins/Animation/PoseSearch/Source/Editor/Private/PoseSearchDebuggerDatabaseView.cpp
2025-05-18 13:04:45 +08:00

1243 lines
40 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PoseSearchDebuggerDatabaseView.h"
#include "Algo/AllOf.h"
#include "Animation/AnimationSettings.h"
#include "Animation/AnimComposite.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimSequence.h"
#include "Internationalization/Regex.h"
#include "PoseSearch/PoseSearchDatabase.h"
#include "PoseSearch/PoseSearchDerivedData.h"
#include "PoseSearch/PoseSearchFeatureChannel.h"
#include "PoseSearch/PoseSearchSchema.h"
#include "PoseSearchDebuggerDatabaseRow.h"
#include "PoseSearchDebuggerSettings.h"
#include "PoseSearchDebuggerView.h"
#include "PoseSearchDebuggerViewModel.h"
#include "PoseSearchEditor.h"
#include "StructUtils/InstancedStruct.h"
#include "Trace/PoseSearchTraceProvider.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Layout/SScrollBar.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Views/SListView.h"
#define LOCTEXT_NAMESPACE "PoseSearchDebugger"
namespace UE::PoseSearch
{
static FORCEINLINE float ArraySum(TConstArrayView<float> View, int32 StartIndex, int32 Offset)
{
float Sum = 0.f;
const int32 EndIndex = StartIndex + Offset;
for (int32 i = StartIndex; i < EndIndex; ++i)
{
Sum += View[i];
}
return Sum;
}
class SCostBreakDownData
{
public:
SCostBreakDownData(const TArray<FTraceMotionMatchingStateDatabaseEntry>& DatabaseEntries, bool bIsVerbose)
{
// processing all the DatabaseEntries to collect the LabelToChannels
TLabelBuilder LabelBuilder;
for (const FTraceMotionMatchingStateDatabaseEntry& DbEntry : DatabaseEntries)
{
const UPoseSearchDatabase* Database = FTraceMotionMatchingStateMessage::GetObjectFromId<UPoseSearchDatabase>(DbEntry.DatabaseId);
if (EAsyncBuildIndexResult::Success == FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(Database, ERequestAsyncBuildFlag::ContinueRequest))
{
for (const TObjectPtr<UPoseSearchFeatureChannel>& ChannelPtr : Database->Schema->GetChannels())
{
AnalyzeChannelRecursively(LabelBuilder, ChannelPtr.Get(), bIsVerbose);
}
}
}
}
void ProcessData(TArray<TSharedRef<FDebuggerDatabaseRowData>>& InOutUnfilteredDatabaseRows) const
{
for (TSharedRef<FDebuggerDatabaseRowData>& UnfilteredDatabaseRowRef : InOutUnfilteredDatabaseRows)
{
FDebuggerDatabaseRowData& UnfilteredDatabaseRow = UnfilteredDatabaseRowRef.Get();
UnfilteredDatabaseRow.CostBreakdowns.AddDefaulted(LabelToChannels.Num());
for (int32 LabelToChannelIndex = 0; LabelToChannelIndex < LabelToChannels.Num(); ++LabelToChannelIndex)
{
const FLabelToChannels& LabelToChannel = LabelToChannels[LabelToChannelIndex];
// there should only be at most one channel per schema with the unique label,
// but we'll keep this generic allowing multiple channels from the same schema having the same label.
// the cost will be the sum of all the channels cost
float CostBreakdown = 0.f;
for (const UPoseSearchFeatureChannel* Channel : LabelToChannel.Channels)
{
// checking if the row is associated to the Channel
if (UnfilteredDatabaseRow.SharedData->SourceDatabase->Schema == Channel->GetSchema())
{
CostBreakdown += ArraySum(UnfilteredDatabaseRow.CostVector, Channel->GetChannelDataOffset(), Channel->GetChannelCardinality());
}
}
UnfilteredDatabaseRow.CostBreakdowns[LabelToChannelIndex] = CostBreakdown;
}
}
}
bool AreLabelsEqualTo(const TArray<FText>& OtherLabels) const
{
if (LabelToChannels.Num() != OtherLabels.Num())
{
return false;
}
for (int32 i = 0; i < LabelToChannels.Num(); ++i)
{
if (!LabelToChannels[i].Label.EqualTo(OtherLabels[i]))
{
return false;
}
}
return true;
}
const TArray<FText> GetLabels() const
{
TArray<FText> Labels;
for (const FLabelToChannels& LabelToChannel : LabelToChannels)
{
Labels.Add(LabelToChannel.Label);
}
return Labels;
}
private:
void AnalyzeChannelRecursively(TLabelBuilder& LabelBuilder, const UPoseSearchFeatureChannel* Channel, bool bIsVerbose)
{
LabelBuilder.Reset();
const FText Label = FText::FromString(Channel->GetLabel(LabelBuilder, ELabelFormat::Full_Vertical).ToString());
bool bLabelFound = false;
for (int32 i = 0; i < LabelToChannels.Num(); ++i)
{
if (LabelToChannels[i].Label.EqualTo(Label))
{
LabelToChannels[i].Channels.AddUnique(Channel);
bLabelFound = true;
}
}
if (!bLabelFound)
{
LabelToChannels.AddDefaulted();
LabelToChannels.Last().Label = Label;
LabelToChannels.Last().Channels.Add(Channel);
}
if (bIsVerbose)
{
for (const TObjectPtr<UPoseSearchFeatureChannel>& SubChannelPtr : Channel->GetSubChannels())
{
if (const UPoseSearchFeatureChannel* SubChannel = SubChannelPtr.Get())
{
AnalyzeChannelRecursively(LabelBuilder, SubChannel, bIsVerbose);
}
}
}
}
struct FLabelToChannels
{
FText Label;
TArray<const UPoseSearchFeatureChannel*> Channels; // NoTe: channels can be from different schemas
};
TArray<FLabelToChannels> LabelToChannels;
};
static void AddUnfilteredDatabaseRow(const UPoseSearchDatabase* Database, TConstArrayView<float> DynamicWeightsSqrt,
TArray<TSharedRef<FDebuggerDatabaseRowData>>& UnfilteredDatabaseRows, TSharedRef<FDebuggerDatabaseSharedData> SharedData,
int32 DbPoseIdx, EPoseCandidateFlags PoseCandidateFlags, TConstArrayView<uint32> PoseToPCAValuesVectorIndexes, const FPoseSearchCost& Cost = FPoseSearchCost())
{
const FSearchIndex& SearchIndex = Database->GetSearchIndex();
if (const FSearchIndexAsset* SearchIndexAsset = SearchIndex.GetAssetForPoseSafe(DbPoseIdx))
{
TSharedRef<FDebuggerDatabaseRowData>& Row = UnfilteredDatabaseRows.Add_GetRef(MakeShared<FDebuggerDatabaseRowData>(SharedData));
const float Time = Database->GetNormalizedAssetTime(DbPoseIdx);
Row->PoseIdx = DbPoseIdx;
Row->PoseCandidateFlags = PoseCandidateFlags;
Row->DbAssetIdx = SearchIndexAsset->GetSourceAssetIdx();
Row->AssetTime = Time;
Row->bMirrored = SearchIndexAsset->IsMirrored();
Row->CostVector.SetNum(Database->Schema->SchemaCardinality);
TArray<float> BufferUsedForReconstruction;
const TConstArrayView<float> PoseValues = SearchIndex.GetPoseValuesSafe(DbPoseIdx, BufferUsedForReconstruction);
// in case we modify the schema while PIE is paused and displaying the Pose Search Editor, we could end up with a stale State with a SharedData->QueryVector saved with the previous schema
// so the cardinality of SharedData->QueryVector and PoseValues don't match. In that case we just use PoseValues as query to have all costs set to zero
const bool bIsQueryVectorValid = SharedData->QueryVector.Num() == PoseValues.Num();
const TConstArrayView<float> QueryVector = bIsQueryVectorValid ? TConstArrayView<float>(SharedData->QueryVector) : TConstArrayView<float>(PoseValues);
CompareFeatureVectors(PoseValues, QueryVector, DynamicWeightsSqrt, Row->CostVector);
if (Cost.IsValid())
{
Row->PoseCost = Cost;
}
else
{
// @todo: gather ContinuingPoseCostAddend, ContinuingInteractionCostAddend properly to reconstruct the real FPoseSearchCost
const float ContinuingPoseCostAddend = 0.f;
const float ContinuingInteractionCostAddend = 0.f;
Row->PoseCost = FPoseSearchCost(ArraySum(Row->CostVector, 0, Database->Schema->SchemaCardinality), SearchIndex.PoseMetadata[DbPoseIdx].GetCostAddend(), ContinuingPoseCostAddend, ContinuingInteractionCostAddend);
}
if (!SharedData->PCAQueryVector.IsEmpty())
{
const int32 PCAValuesVectorIdx = PoseToPCAValuesVectorIndexes.IsEmpty() ? DbPoseIdx : PoseToPCAValuesVectorIndexes[DbPoseIdx];
if (PCAValuesVectorIdx >= 0)
{
TConstArrayView<float> PCAPoseValues = SearchIndex.GetPCAPoseValues(PCAValuesVectorIdx);
if (SharedData->PCAQueryVector.Num() == PCAPoseValues.Num())
{
Row->PosePCACost = CompareFeatureVectors(SharedData->PCAQueryVector, PCAPoseValues);
}
}
}
if (const FPoseSearchDatabaseAnimationAssetBase* DatabaseAsset = Database->GetDatabaseAnimationAsset<FPoseSearchDatabaseAnimationAssetBase>(*SearchIndexAsset))
{
const FVector BlendParameters = SearchIndexAsset->GetBlendParameters();
const float PlayLength = DatabaseAsset->GetPlayLength(BlendParameters);
const UObject* AnimationAsset = DatabaseAsset->GetAnimationAsset();
Row->AssetName = DatabaseAsset->GetName();
Row->AssetPath = AnimationAsset ? AnimationAsset->GetPathName() : "";
Row->bLooping = DatabaseAsset->IsLooping();
Row->BlendParameters = BlendParameters;
const float RealAssetTime = Database->GetRealAssetTime(DbPoseIdx);
const FFrameRate& DefaultFrameRate = UAnimationSettings::Get()->GetDefaultFrameRate();
Row->AnimFrame = DefaultFrameRate.AsFrameTime(RealAssetTime).RoundToFrame().Value;
Row->AnimPercentage = FMath::IsNearlyZero(PlayLength) ? 0.f : RealAssetTime / PlayLength;
}
}
}
void SDebuggerDatabaseView::Update(const FTraceMotionMatchingStateMessage& State)
{
// row cost color palette
static const FLinearColor DiscardedRowColor(0.314f, 0.314f, 0.314f); // darker gray
static const FLinearColor BestScoreRowColor = FLinearColor::Green;
static const FLinearColor WorstScoreRowColor = FLinearColor::Red;
using namespace DebuggerDatabaseColumns;
bool bIsVerbose = false;
TSharedPtr<FDebuggerViewModel> ViewModel;
if (TSharedPtr<SDebuggerView> DebuggerView = ParentDebuggerViewPtr.Pin())
{
ViewModel = DebuggerView->GetViewModel();
bIsVerbose = ViewModel->IsVerbose();
}
UnfilteredDatabaseRows.Reset();
bool bAddPCACost = false;
TArray<uint32> PoseToPCAValuesVectorIndexes;
TAlignedArray<float> DynamicWeightsSqrtBuffer;
for (const FTraceMotionMatchingStateDatabaseEntry& DbEntry : State.DatabaseEntries)
{
const UPoseSearchDatabase* Database = FTraceMotionMatchingStateMessage::GetObjectFromId<UPoseSearchDatabase>(DbEntry.DatabaseId);
if (EAsyncBuildIndexResult::Success == FAsyncPoseSearchDatabasesManagement::RequestAsyncBuildIndex(Database, ERequestAsyncBuildFlag::ContinueRequest))
{
const FSearchIndex& SearchIndex = Database->GetSearchIndex();
SearchIndex.GetPoseToPCAValuesVectorIndexes(PoseToPCAValuesVectorIndexes);
TSharedRef<FDebuggerDatabaseSharedData> SharedData = MakeShared<FDebuggerDatabaseSharedData>();
SharedData->SourceDatabase = Database;
SharedData->DatabaseName = Database->GetName();
SharedData->DatabasePath = Database->GetPathName();
SharedData->QueryVector = DbEntry.QueryVector;
// checking for DbEntry.QueryVector.Num() == Database->Schema->SchemaCardinality to make sure the stored DbEntry data is still aligned with the Database->Schema
// (this could happen when pausing PIE, change the schema cardinality, scrubbing back to previously recorded data)
if (Database->PoseSearchMode == EPoseSearchMode::PCAKDTree && DbEntry.QueryVector.Num() == Database->Schema->SchemaCardinality)
{
SharedData->PCAQueryVector.SetNumZeroed(Database->GetNumberOfPrincipalComponents());
SearchIndex.PCAProject(DbEntry.QueryVector, SharedData->PCAQueryVector);
bAddPCACost = bIsVerbose;
}
DynamicWeightsSqrtBuffer.SetNumUninitialized(SearchIndex.WeightsSqrt.Num());
TConstArrayView<float> DynamicWeightsSqrt = Database->CalculateDynamicWeightsSqrt(DynamicWeightsSqrtBuffer);
for (const FTraceMotionMatchingStatePoseEntry& PoseEntry : DbEntry.PoseEntries)
{
AddUnfilteredDatabaseRow(Database, DynamicWeightsSqrt, UnfilteredDatabaseRows, SharedData, PoseEntry.DbPoseIdx, PoseEntry.PoseCandidateFlags, PoseToPCAValuesVectorIndexes, PoseEntry.Cost);
}
if (UPoseSearchDebuggerConfig::Get().bShowAllPoses)
{
bool bAddMissingPoseEntries = false;
// excluding the already added PoseEntry
TSet<int32> PoseEntriesIdx;
for (const FTraceMotionMatchingStatePoseEntry& PoseEntry : DbEntry.PoseEntries)
{
PoseEntriesIdx.Add(PoseEntry.DbPoseIdx);
const bool bIsContinuingPose = EnumHasAnyFlags(PoseEntry.PoseCandidateFlags, EPoseCandidateFlags::Valid_ContinuingPose);
bAddMissingPoseEntries |= !bIsContinuingPose;
}
// adding the missing ones
if (bAddMissingPoseEntries)
{
for (int32 DbPoseIdx = 0; DbPoseIdx < SearchIndex.GetNumPoses(); ++DbPoseIdx)
{
if (!PoseEntriesIdx.Find(DbPoseIdx))
{
AddUnfilteredDatabaseRow(Database, DynamicWeightsSqrt, UnfilteredDatabaseRows, SharedData, DbPoseIdx, EPoseCandidateFlags::DiscardedBy_Search, PoseToPCAValuesVectorIndexes);
}
}
}
}
}
}
SCostBreakDownData CostBreakDownData(State.DatabaseEntries, bIsVerbose);
if (!UnfilteredDatabaseRows.IsEmpty())
{
CostBreakDownData.ProcessData(UnfilteredDatabaseRows);
// calculating min/max for Cost, PCACost, and breakdowns
float MinCost = UE_MAX_FLT;
float MaxCost = -UE_MAX_FLT;
float MinPCACost = UE_MAX_FLT;
float MaxPCACost = -UE_MAX_FLT;
TArray<float> MinCostBreakdowns;
TArray<float> MaxCostBreakdowns;
const int32 CostBreakdownsCardinality = UnfilteredDatabaseRows[0]->CostBreakdowns.Num();
MinCostBreakdowns.Init(UE_MAX_FLT, CostBreakdownsCardinality);
MaxCostBreakdowns.Init(-UE_MAX_FLT, CostBreakdownsCardinality);
for (TSharedRef<FDebuggerDatabaseRowData>& UnfilteredRow : UnfilteredDatabaseRows)
{
if (EnumHasAnyFlags(UnfilteredRow->PoseCandidateFlags, EPoseCandidateFlags::AnyValidMask))
{
const float Cost = UnfilteredRow->PoseCost;
MinCost = FMath::Min(MinCost, Cost);
MaxCost = FMath::Max(MaxCost, Cost);
const float PCACost = UnfilteredRow->PosePCACost;
MinPCACost = FMath::Min(MinPCACost, PCACost);
MaxPCACost = FMath::Max(MaxPCACost, PCACost);
for (int32 Index = 0; Index < CostBreakdownsCardinality; ++Index)
{
const float Value = UnfilteredRow->CostBreakdowns[Index];
if (Value != UE_MAX_FLT)
{
MinCostBreakdowns[Index] = FMath::Min(MinCostBreakdowns[Index], Value);
MaxCostBreakdowns[Index] = FMath::Max(MaxCostBreakdowns[Index], Value);
}
}
}
}
// calculating min max deltas (always >= UE_KINDA_SMALL_NUMBER to avoid division by zero)
const float DeltaCost = FMath::Max(MaxCost - MinCost, UE_KINDA_SMALL_NUMBER);
const float DeltaPCACost = FMath::Max(MaxPCACost - MinPCACost, UE_KINDA_SMALL_NUMBER);
TArray<float> DeltaCostBreakdowns;
DeltaCostBreakdowns.SetNumUninitialized(CostBreakdownsCardinality);
for (int32 Index = 0; Index < CostBreakdownsCardinality; ++Index)
{
DeltaCostBreakdowns[Index] = FMath::Max(MaxCostBreakdowns[Index] - MinCostBreakdowns[Index], UE_KINDA_SMALL_NUMBER);
}
// calculating colors for Cost, PCACost, and breakdowns (using min/max as range)
for (TSharedRef<FDebuggerDatabaseRowData>& UnfilteredRow : UnfilteredDatabaseRows)
{
if (EnumHasAnyFlags(UnfilteredRow->PoseCandidateFlags, EPoseCandidateFlags::AnyValidMask))
{
const float CostBlend = (UnfilteredRow->PoseCost - MinCost) / DeltaCost;
UnfilteredRow->CostColor = BestScoreRowColor + (WorstScoreRowColor - BestScoreRowColor) * CostBlend;
const float PCACostBlend = (UnfilteredRow->PosePCACost - MinPCACost) / DeltaPCACost;
UnfilteredRow->PCACostColor = BestScoreRowColor + (WorstScoreRowColor - BestScoreRowColor) * PCACostBlend;
UnfilteredRow->CostBreakdownsColors.SetNumUninitialized(CostBreakdownsCardinality);
for (int32 Index = 0; Index < CostBreakdownsCardinality; ++Index)
{
const float CostBreakdownBlend = (UnfilteredRow->CostBreakdowns[Index] - MinCostBreakdowns[Index]) / DeltaCostBreakdowns[Index];
UnfilteredRow->CostBreakdownsColors[Index] = BestScoreRowColor + (WorstScoreRowColor - BestScoreRowColor) * CostBreakdownBlend;
}
}
else
{
UnfilteredRow->CostColor = DiscardedRowColor;
UnfilteredRow->PCACostColor = DiscardedRowColor;
UnfilteredRow->CostBreakdownsColors.Init(DiscardedRowColor, CostBreakdownsCardinality);
}
}
}
if (bHasPCACostColumn != bAddPCACost || bWasVerbose != bIsVerbose || !CostBreakDownData.AreLabelsEqualTo(OldLabels))
{
bHasPCACostColumn = bAddPCACost;
bWasVerbose = bIsVerbose;
OldLabels = CostBreakDownData.GetLabels();
// recreating and binding the columns
Columns.Reset();
// Construct all column types
int32 ColumnIdx = 0;
AddColumn(MakeShared<FDatabaseName>(ColumnIdx++, ViewModel));
AddColumn(MakeShared<FAssetName>(ColumnIdx++));
TSharedRef<FCost> CostColumn = MakeShared<FCost>(ColumnIdx++);
AddColumn(CostColumn);
if (bAddPCACost)
{
AddColumn(MakeShared<FPCACost>(ColumnIdx++));
}
if (bIsVerbose)
{
AddColumn(MakeShared<FContinuingPoseCost>(ColumnIdx++));
AddColumn(MakeShared<FNotifyCost>(ColumnIdx++));
AddColumn(MakeShared<FContinuingInteractionCost>(ColumnIdx++));
}
else
{
AddColumn(MakeShared<FCostModifier>(ColumnIdx++));
}
int32 LabelIdx = 0;
for (const FText& Label : CostBreakDownData.GetLabels())
{
AddColumn(MakeShared<FChannelBreakdownCostColumn>(ColumnIdx++, LabelIdx++, Label));
}
AddColumn(MakeShared<FFrame>(ColumnIdx++));
AddColumn(MakeShared<FTime>(ColumnIdx++));
AddColumn(MakeShared<FPercentage>(ColumnIdx++));
AddColumn(MakeShared<FMirrored>(ColumnIdx++));
AddColumn(MakeShared<FLooping>(ColumnIdx++));
AddColumn(MakeShared<FPoseIdx>(ColumnIdx++));
AddColumn(MakeShared<FAssetIdx>(ColumnIdx++));
AddColumn(MakeShared<FBlendParameters>(ColumnIdx++));
AddColumn(MakeShared<FPoseCandidateFlags>(ColumnIdx++));
SortColumn = CostColumn->ColumnId;
// Active and Continuing Pose view scroll bars only for indenting the columns to align w/ database
ActiveView.ScrollBar->SetVisibility(EVisibility::Hidden);
ContinuingPoseView.ScrollBar->SetVisibility(EVisibility::Hidden);
// Refresh Columns
ActiveView.HeaderRow->ClearColumns();
ContinuingPoseView.HeaderRow->ClearColumns();
FilteredDatabaseView.HeaderRow->ClearColumns();
// Sort columns by index
Columns.ValueSort([](const TSharedRef<IColumn> Column0, const TSharedRef<IColumn> Column1)
{
return Column0->SortIndex < Column1->SortIndex;
});
// Compute columns widths
for (TPair<FName, TSharedRef<IColumn>>& ColumnPair : Columns)
{
ColumnPair.Value->Width = ColumnPair.Value->ComputeColumnWidth(UnfilteredDatabaseRows);
// Add spacing for sorting arrow
if (ColumnPair.Value->ColumnId == SortColumn && SortMode != EColumnSortMode::None)
{
ColumnPair.Value->Width += DebuggerDatabaseColumns::ColumnSortWidthPadding;
}
}
// Add columns from map to header row
for (TPair<FName, TSharedRef<IColumn>>& ColumnPair : Columns)
{
IColumn& Column = ColumnPair.Value.Get();
if (ColumnPair.Value->bEnabled)
{
SHeaderRow::FColumn::FArguments ColumnArgs = SHeaderRow::FColumn::FArguments()
.ColumnId(Column.ColumnId)
.DefaultTooltip(Column.GetLabelTooltip())
.SortMode(this, &SDebuggerDatabaseView::GetColumnSortMode, Column.ColumnId)
.OnSort(this, &SDebuggerDatabaseView::OnColumnSortModeChanged)
.ManualWidth(this, &SDebuggerDatabaseView::GetColumnWidth, Column.ColumnId)
.OnWidthChanged(this, &SDebuggerDatabaseView::OnColumnWidthChanged, Column.ColumnId)
.VAlignHeader(VAlign_Center)
.VAlignCell(VAlign_Center)
.HAlignHeader(HAlign_Center)
.HAlignCell(HAlign_Fill)
.HeaderContent()
[
SNew(SBox)
.VAlign( VAlign_Fill )
.HAlign( HAlign_Fill )
[
SNew(STextBlock)
.TextStyle(FAppStyle::Get(), "SmallText")
.Text(Column.GetLabel())
.Margin(FMargin(5.f, 5.f, 5.f, 5.f))
.Justification(ETextJustify::Center)
]
];
FilteredDatabaseView.HeaderRow->AddColumn(ColumnArgs);
ContinuingPoseView.HeaderRow->AddColumn(ColumnArgs);
ActiveView.HeaderRow->AddColumn(ColumnArgs);
}
}
}
SortDatabaseRows();
PopulateViewRows();
}
void SDebuggerDatabaseView::AddColumn(TSharedRef<DebuggerDatabaseColumns::IColumn>&& Column)
{
Columns.Add(Column->ColumnId, Column);
}
EColumnSortMode::Type SDebuggerDatabaseView::GetColumnSortMode(const FName ColumnId) const
{
if (ColumnId == SortColumn)
{
return SortMode;
}
return EColumnSortMode::None;
}
float SDebuggerDatabaseView::GetColumnWidth(const FName ColumnId) const
{
return Columns[ColumnId]->Width;
}
void SDebuggerDatabaseView::OnColumnSortModeChanged(const EColumnSortPriority::Type SortPriority, const FName & ColumnId, const EColumnSortMode::Type InSortMode)
{
const bool bHasSortColumnChanged = SortColumn != ColumnId;
// Restore previous sort column to its default width
if (bHasSortColumnChanged)
{
Columns[SortColumn]->Width -= DebuggerDatabaseColumns::ColumnSortWidthPadding;
}
SortColumn = ColumnId;
SortMode = InSortMode;
SortDatabaseRows();
PopulateViewRows();
// Take into account sorting arrow for the column width
if (bHasSortColumnChanged && SortMode != EColumnSortMode::None)
{
Columns[SortColumn]->Width += DebuggerDatabaseColumns::ColumnSortWidthPadding;
}
}
void SDebuggerDatabaseView::OnColumnWidthChanged(const float NewWidth, FName ColumnId) const
{
Columns[ColumnId]->Width = NewWidth;
}
void SDebuggerDatabaseView::OnFilterTextChanged(const FText& SearchText)
{
FilterText = SearchText;
PopulateViewRows();
}
void SDebuggerDatabaseView::OnShowAllPosesCheckboxChanged(ECheckBoxState State)
{
UPoseSearchDebuggerConfig::Get().bShowAllPoses = State == ECheckBoxState::Checked;
if (TSharedPtr<SDebuggerView> DebuggerView = ParentDebuggerViewPtr.Pin())
{
if (TSharedPtr<FDebuggerViewModel> ViewModel = DebuggerView->GetViewModel())
{
if (const FTraceMotionMatchingStateMessage* MotionMatchingState = ViewModel.Get()->GetMotionMatchingState())
{
Update(*MotionMatchingState);
}
}
}
}
void SDebuggerDatabaseView::OnShowOnlyBestAssetPoseCheckboxChanged(ECheckBoxState State)
{
UPoseSearchDebuggerConfig::Get().bShowOnlyBestAssetPose = State == ECheckBoxState::Checked;
PopulateViewRows();
}
void SDebuggerDatabaseView::OnHideInvalidPosesCheckboxChanged(ECheckBoxState State)
{
UPoseSearchDebuggerConfig::Get().bHideInvalidPoses = State == ECheckBoxState::Checked;
PopulateViewRows();
}
void SDebuggerDatabaseView::OnUseRegexCheckboxChanged(ECheckBoxState State)
{
UPoseSearchDebuggerConfig::Get().bUseRegex = State == ECheckBoxState::Checked;
PopulateViewRows();
}
void SDebuggerDatabaseView::OnDatabaseRowSelectionChanged(TSharedPtr<FDebuggerDatabaseRowData> Row, ESelectInfo::Type SelectInfo)
{
if (Row.IsValid())
{
OnPoseSelectionChanged.ExecuteIfBound(Row->SharedData->SourceDatabase.Get(), Row->PoseIdx, Row->AssetTime);
}
}
void SDebuggerDatabaseView::SortDatabaseRows()
{
if (!UnfilteredDatabaseRows.IsEmpty())
{
if (SortMode == EColumnSortMode::Ascending)
{
UnfilteredDatabaseRows.Sort(Columns[SortColumn]->GetSortPredicate());
}
else if (SortMode == EColumnSortMode::Descending)
{
auto DescendingPredicate = [this](const auto& Lhs, const auto& Rhs) -> bool
{
return !Columns[SortColumn]->GetSortPredicate()(Lhs, Rhs);
};
UnfilteredDatabaseRows.Sort(DescendingPredicate);
}
}
}
void SDebuggerDatabaseView::PopulateViewRows()
{
ActiveView.Rows.Reset();
ContinuingPoseView.Rows.Reset();
FilteredDatabaseView.Rows.Reset();
const int32 UnfilteredDatabaseRowsNum = UnfilteredDatabaseRows.Num();
if (UnfilteredDatabaseRowsNum > 0)
{
FString FilterString = FilterText.ToString();
TArray<FString> Tokens;
FilterString.ParseIntoArrayWS(Tokens);
const bool bHasNameFilter = !Tokens.IsEmpty();
FRegexPattern Pattern(FilterString);
TSet<int32> BestAssetPoseIndex;
if (UPoseSearchDebuggerConfig::Get().bShowOnlyBestAssetPose)
{
TArray<int32> SortIndex;
SortIndex.SetNumUninitialized(UnfilteredDatabaseRowsNum);
for (int32 i = 0; i < UnfilteredDatabaseRowsNum; ++i)
{
SortIndex[i] = i;
}
// sorting SortIndex elements by ascending order in cost, after grouping them by asset index
Algo::Sort(SortIndex, [this](int32 IndexA, int32 IndexB)
{
const FDebuggerDatabaseRowData& RowDataA = UnfilteredDatabaseRows[IndexA].Get();
const FDebuggerDatabaseRowData& RowDataB = UnfilteredDatabaseRows[IndexB].Get();
if (RowDataA.SharedData->SourceDatabase != RowDataB.SharedData->SourceDatabase)
{
const int32 DatabasePathCompare = RowDataA.SharedData->DatabasePath.Compare(RowDataB.SharedData->DatabasePath);
if (DatabasePathCompare < 0)
{
return true;
}
const int32 DatabaseNameCompare = RowDataA.SharedData->DatabaseName.Compare(RowDataB.SharedData->DatabaseName);
return DatabaseNameCompare < 0;
}
if (RowDataA.DbAssetIdx == RowDataB.DbAssetIdx)
{
return RowDataA.PoseCost < RowDataB.PoseCost;
}
return RowDataA.DbAssetIdx < RowDataB.DbAssetIdx;
});
// populating BestAssetPoseIndex only with the best pose index (lowest total cost) for each asset
int32 PreviousDbAssetIdx = INDEX_NONE;
TWeakObjectPtr<const UPoseSearchDatabase> PreviousSourceDatabase;
for (int32 i = 0; i < UnfilteredDatabaseRowsNum; ++i)
{
const FDebuggerDatabaseRowData& RowData = UnfilteredDatabaseRows[SortIndex[i]].Get();
if (PreviousDbAssetIdx != RowData.DbAssetIdx || PreviousSourceDatabase != RowData.SharedData->SourceDatabase)
{
// adding only the best pose index for the RowData.DbAssetIdx asset to BestAssetPoseIndex
// (the first we find by iterating over UnfilteredDatabaseRows[SortIndex[i]])
BestAssetPoseIndex.Add(RowData.PoseIdx);
PreviousDbAssetIdx = RowData.DbAssetIdx;
PreviousSourceDatabase = RowData.SharedData->SourceDatabase;
}
}
}
for (int32 i = 0; i < UnfilteredDatabaseRowsNum; ++i)
{
const TSharedRef<FDebuggerDatabaseRowData>& UnfilteredRow = UnfilteredDatabaseRows[i];
bool bTryAddToFilteredDatabaseViewRows = true;
if (EnumHasAnyFlags(UnfilteredRow->PoseCandidateFlags, EPoseCandidateFlags::Valid_ContinuingPose))
{
ContinuingPoseView.Rows.Add(UnfilteredRow);
bTryAddToFilteredDatabaseViewRows = false;
}
if (EnumHasAnyFlags(UnfilteredRow->PoseCandidateFlags, EPoseCandidateFlags::Valid_CurrentPose))
{
ActiveView.Rows.Add(UnfilteredRow);
bTryAddToFilteredDatabaseViewRows = false;
}
if (UPoseSearchDebuggerConfig::Get().bShowOnlyBestAssetPose)
{
if (!BestAssetPoseIndex.Find(UnfilteredRow->PoseIdx))
{
bTryAddToFilteredDatabaseViewRows = false;
}
}
if (bTryAddToFilteredDatabaseViewRows)
{
bool bPassesNameFilter = true;
if (UPoseSearchDebuggerConfig::Get().bHideInvalidPoses && !EnumHasAnyFlags(UnfilteredRow->PoseCandidateFlags, EPoseCandidateFlags::AnyValidMask))
{
bPassesNameFilter = false;
}
else if (UPoseSearchDebuggerConfig::Get().bUseRegex)
{
FRegexMatcher Matcher(Pattern, UnfilteredRow->AssetName);
bPassesNameFilter = Matcher.FindNext();
}
else if (bHasNameFilter)
{
bPassesNameFilter = Algo::AllOf(Tokens, [&](FString Token)
{
return UnfilteredRow->AssetName.Contains(Token);
});
}
if (bPassesNameFilter)
{
FilteredDatabaseView.Rows.Add(UnfilteredRow);
}
}
}
}
ActiveView.ListView->RequestListRefresh();
ContinuingPoseView.ListView->RequestListRefresh();
FilteredDatabaseView.ListView->RequestListRefresh();
if (ActiveView.Rows.Num() > 0)
{
ReasonForNoActivePose = FText::GetEmpty();
}
else
{
ReasonForNoActivePose = LOCTEXT("ReasonForNoActivePose", "Database search didn't find any candidates, or the search has not been performed");
}
if (ContinuingPoseView.Rows.Num() > 0)
{
ReasonForNoContinuingPose = FText::GetEmpty();
}
else
{
ReasonForNoContinuingPose = LOCTEXT("ReasonForNoContinuingPose", "Invalid continuing pose");
}
if (FilteredDatabaseView.Rows.Num() > 0)
{
ReasonForNoCandidates = FText::GetEmpty();
}
else if (UnfilteredDatabaseRows.Num() == 0)
{
ReasonForNoCandidates = LOCTEXT("ReasonForNoCandidates_NoSearch", "Database search didn't find any candidates, or the search has not been performed");
}
else if (UnfilteredDatabaseRows.Num() == 1)
{
ReasonForNoCandidates = LOCTEXT("ReasonForNoCandidates_OnlyContinuingPose", "The continuing pose cost cannot be lowered by searching the databases, so the search has been skipped");
}
else
{
ReasonForNoCandidates = FText::Format(LOCTEXT("ReasonForNoCandidates_AllFilteredOut", "All {0} databases poses have been filtered out"), UnfilteredDatabaseRows.Num());
}
}
TSharedRef<ITableRow> SDebuggerDatabaseView::HandleGenerateDatabaseRow(TSharedRef<FDebuggerDatabaseRowData> Item, const TSharedRef<STableViewBase>& OwnerTable) const
{
return SNew(SDebuggerDatabaseRow, OwnerTable, Item, FilteredDatabaseView.RowStyle, &FilteredDatabaseView.RowBrush, FMargin(0.0f, 2.0f, 6.0f, 2.0f))
.ColumnMap(this, &SDebuggerDatabaseView::GetColumnMap);
}
TSharedRef<ITableRow> SDebuggerDatabaseView::HandleGenerateActiveRow(TSharedRef<FDebuggerDatabaseRowData> Item, const TSharedRef<STableViewBase>& OwnerTable) const
{
return SNew(SDebuggerDatabaseRow, OwnerTable, Item, ActiveView.RowStyle, &ActiveView.RowBrush, FMargin(0.0f, 2.0f, 6.0f, 4.0f))
.ColumnMap(this, &SDebuggerDatabaseView::GetColumnMap);
}
TSharedRef<ITableRow> SDebuggerDatabaseView::HandleGenerateContinuingPoseRow(TSharedRef<FDebuggerDatabaseRowData> Item, const TSharedRef<STableViewBase>& OwnerTable) const
{
return SNew(SDebuggerDatabaseRow, OwnerTable, Item, ContinuingPoseView.RowStyle, &ContinuingPoseView.RowBrush, FMargin(0.0f, 2.0f, 6.0f, 4.0f))
.ColumnMap(this, &SDebuggerDatabaseView::GetColumnMap);
}
void SDebuggerDatabaseView::Construct(const FArguments& InArgs)
{
ParentDebuggerViewPtr = InArgs._Parent;
OnPoseSelectionChanged = InArgs._OnPoseSelectionChanged;
check(OnPoseSelectionChanged.IsBound());
// Active Row
ActiveView.HeaderRow = SNew(SHeaderRow);
// Used for spacing
ActiveView.ScrollBar = SNew(SScrollBar)
.Orientation(Orient_Vertical)
.HideWhenNotInUse(false)
.AlwaysShowScrollbar(true)
.AlwaysShowScrollbarTrack(true);
ActiveView.ListView = SNew(SListView<TSharedRef<FDebuggerDatabaseRowData>>)
.ListItemsSource(&ActiveView.Rows)
.HeaderRow(ActiveView.HeaderRow.ToSharedRef())
.OnGenerateRow(this, &SDebuggerDatabaseView::HandleGenerateActiveRow)
.ExternalScrollbar(ActiveView.ScrollBar)
.SelectionMode(ESelectionMode::SingleToggle)
.ConsumeMouseWheel(EConsumeMouseWheel::Never);
ActiveView.RowStyle = FAppStyle::GetWidgetStyle<FTableRowStyle>("TableView.Row");
ActiveView.RowBrush = *FAppStyle::GetBrush("DetailsView.CategoryTop");
// ContinuingPose Row
ContinuingPoseView.HeaderRow = SNew(SHeaderRow).Visibility(EVisibility::Collapsed);
// Used for spacing
ContinuingPoseView.ScrollBar = SNew(SScrollBar)
.Orientation(Orient_Vertical)
.HideWhenNotInUse(false)
.AlwaysShowScrollbar(true)
.AlwaysShowScrollbarTrack(true);
ContinuingPoseView.ListView = SNew(SListView<TSharedRef<FDebuggerDatabaseRowData>>)
.ListItemsSource(&ContinuingPoseView.Rows)
.HeaderRow(ContinuingPoseView.HeaderRow.ToSharedRef())
.OnGenerateRow(this, &SDebuggerDatabaseView::HandleGenerateContinuingPoseRow)
.ExternalScrollbar(ContinuingPoseView.ScrollBar)
.SelectionMode(ESelectionMode::SingleToggle)
.ConsumeMouseWheel(EConsumeMouseWheel::Never);
ContinuingPoseView.RowStyle = FAppStyle::GetWidgetStyle<FTableRowStyle>("TableView.Row");
ContinuingPoseView.RowBrush = *FAppStyle::GetBrush("DetailsView.CategoryTop");
// Filtered Database
FilteredDatabaseView.ScrollBar =
SNew(SScrollBar)
.Orientation(Orient_Vertical)
.HideWhenNotInUse(false)
.AlwaysShowScrollbar(true)
.AlwaysShowScrollbarTrack(true);
FilteredDatabaseView.HeaderRow = SNew(SHeaderRow).Visibility(EVisibility::Collapsed);
FilteredDatabaseView.ListView = SNew(SListView<TSharedRef<FDebuggerDatabaseRowData>>)
.ListItemsSource(&FilteredDatabaseView.Rows)
.HeaderRow(FilteredDatabaseView.HeaderRow.ToSharedRef())
.OnGenerateRow(this, &SDebuggerDatabaseView::HandleGenerateDatabaseRow)
.ExternalScrollbar(FilteredDatabaseView.ScrollBar)
.SelectionMode(ESelectionMode::Multi)
.ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible)
.OnSelectionChanged(this, &SDebuggerDatabaseView::OnDatabaseRowSelectionChanged);
FilteredDatabaseView.RowStyle = FAppStyle::GetWidgetStyle<FTableRowStyle>("TableView.Row");
// Set selected color to white to retain visibility when multi-selecting
FilteredDatabaseView.RowStyle.SetSelectedTextColor(FLinearColor(FVector3f(0.8f)));
FilteredDatabaseView.RowBrush = *FAppStyle::GetBrush("ToolPanel.GroupBorder");
ChildSlot
[
SNew(SScrollBox)
.Orientation(Orient_Horizontal)
.ScrollBarAlwaysVisible(true)
+ SScrollBox::Slot()
.FillSize(1.f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
// Side and top margins, ignore bottom handled by the color border below
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
.AutoHeight()
[
// Active Row text tab
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
.Padding(0.0f)
.AutoWidth()
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop"))
.Padding(FMargin(30.0f, 3.0f, 30.0f, 0.0f))
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
[
SNew(STextBlock)
.Text(FText::FromString("Active Pose"))
]
]
]
// Active row list view with scroll bar
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(0.0f)
[
SNew(SOverlay)
+ SOverlay::Slot()
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("NoBorder"))
.Padding(0.0f)
[
ActiveView.ListView.ToSharedRef()
]
]
+ SOverlay::Slot()
[
SNew(SBorder)
.Visibility(EVisibility::SelfHitTestInvisible)
.Padding(FMargin(5.f, 5.f, 5.f, 5.f))
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
[
SNew(STextBlock)
.Visibility_Lambda([this]()
{
return ReasonForNoActivePose.IsEmpty() ? EVisibility::Collapsed : EVisibility::HitTestInvisible;
})
.Margin(FMargin(5.f, 5.f, 5.f, 5.f))
.Text_Lambda([this]()
{
return ReasonForNoActivePose;
})
]
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
ActiveView.ScrollBar.ToSharedRef()
]
]
]
+ SVerticalBox::Slot()
// Side and top margins, ignore bottom handled by the color border below
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
.AutoHeight()
[
// ContinuingPose Row text tab
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
.Padding(0.0f)
.AutoWidth()
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop"))
.Padding(FMargin(30.0f, 3.0f, 30.0f, 0.0f))
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
[
SNew(STextBlock)
.Text(FText::FromString("Continuing Pose"))
]
]
]
// ContinuingPose row list view with scroll bar
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(0.0f)
[
SNew(SOverlay)
+ SOverlay::Slot()
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("NoBorder"))
.Padding(0.0f)
[
ContinuingPoseView.ListView.ToSharedRef()
]
]
+ SOverlay::Slot()
[
SNew(SBorder)
.Visibility(EVisibility::SelfHitTestInvisible)
.Padding(FMargin(5.f, 5.f, 5.f, 5.f))
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
[
SNew(STextBlock)
.Visibility_Lambda([this]()
{
return ReasonForNoContinuingPose.IsEmpty() ? EVisibility::Collapsed : EVisibility::HitTestInvisible;
})
.Margin(FMargin(5.f, 5.f, 5.f, 5.f))
.Text_Lambda([this]()
{
return ReasonForNoContinuingPose;
})
]
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
ContinuingPoseView.ScrollBar.ToSharedRef()
]
]
]
+ SVerticalBox::Slot()
.Padding(0.0f, 0.0f, 0.0f, 5.0f)
[
// Database view text tab
SNew(SVerticalBox)
+ SVerticalBox::Slot()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
.Padding(0.0f)
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop"))
.Padding(FMargin(30.0f, 3.0f, 30.0f, 0.0f))
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
[
SNew(STextBlock)
.Text(FText::FromString("Pose Candidates"))
]
]
.AutoWidth()
+ SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
[
SNew(SBorder)
.BorderImage(&FilteredDatabaseView.RowStyle.EvenRowBackgroundBrush)
]
]
.AutoHeight()
// Gray line below the tab
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f)
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop"))
.Padding(FMargin(0.0f, 3.0f, 0.0f, 3.0f))
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
]
+ SVerticalBox::Slot()
.Padding(0.0f, 0.0f, 0.0f, 5.0f)
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(10, 5, 10, 5)
[
SAssignNew(FilterBox, SSearchBox)
.OnTextChanged(this, &SDebuggerDatabaseView::OnFilterTextChanged)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(10, 5, 10, 5)
[
SNew(SCheckBox)
.IsChecked_Lambda([this]
{
return UPoseSearchDebuggerConfig::Get().bShowAllPoses ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
})
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
{
SDebuggerDatabaseView::OnShowAllPosesCheckboxChanged(State);
})
[
SNew(STextBlock)
.Text(LOCTEXT("PoseSearchDebuggerShowAllPosesFlag", "Show All Poses"))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(10, 5, 10, 5)
[
SNew(SCheckBox)
.IsChecked_Lambda([this]
{
return UPoseSearchDebuggerConfig::Get().bShowOnlyBestAssetPose ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
})
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
{
SDebuggerDatabaseView::OnShowOnlyBestAssetPoseCheckboxChanged(State);
})
[
SNew(STextBlock)
.Text(LOCTEXT("PoseSearchDebuggerShowOnlyBestAssetPoseFlag", "Only Best Asset Pose"))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(10, 5, 10, 5)
[
SNew(SCheckBox)
.IsChecked_Lambda([this]
{
return UPoseSearchDebuggerConfig::Get().bHideInvalidPoses ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
})
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
{
SDebuggerDatabaseView::OnHideInvalidPosesCheckboxChanged(State);
})
[
SNew(STextBlock)
.Text(LOCTEXT("PoseSearchDebuggerHideInvalidPosesFlag", "Hide Invalid Poses"))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(10, 5, 10, 5)
[
SNew(SCheckBox)
.IsChecked_Lambda([this]
{
return UPoseSearchDebuggerConfig::Get().bUseRegex ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
})
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
{
SDebuggerDatabaseView::OnUseRegexCheckboxChanged(State);
})
[
SNew(STextBlock)
.Text(LOCTEXT("PoseSearchDebuggerUseRegexFlag", "Use Regex"))
]
]
]
+ SVerticalBox::Slot()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(0.0f)
[
SNew(SOverlay)
+SOverlay::Slot()
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("NoBorder"))
.Padding(0.0f)
[
FilteredDatabaseView.ListView.ToSharedRef()
]
]
+ SOverlay::Slot()
[
SNew(SBorder)
.Visibility(EVisibility::SelfHitTestInvisible)
.Padding(FMargin(5.f, 5.f, 5.f, 5.f))
.HAlign(HAlign_Center)
.VAlign(VAlign_Fill)
[
SNew(STextBlock)
.Visibility_Lambda([this]()
{
return ReasonForNoCandidates.IsEmpty() ? EVisibility::Collapsed : EVisibility::HitTestInvisible;
})
.Margin(FMargin(5.f, 5.f, 5.f, 5.f))
.Text_Lambda([this]()
{
return ReasonForNoCandidates;
})
]
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
FilteredDatabaseView.ScrollBar.ToSharedRef()
]
]
]
]
];
SortMode = EColumnSortMode::Ascending;
OldLabels.Reset();
Columns.Reset();
bHasPCACostColumn = false;
bWasVerbose = false;
}
} // namespace UE::PoseSearch
#undef LOCTEXT_NAMESPACE