Files
UnrealEngine/Engine/Source/Editor/Persona/Private/SAnimCurveViewer.cpp
2025-05-18 13:04:45 +08:00

1111 lines
32 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SAnimCurveViewer.h"
#include "AnimAssetFindReplaceCurves.h"
#include "AnimationEditorUtils.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Styling/AppStyle.h"
#include "Widgets/Input/SSpinBox.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Widgets/Input/SSearchBox.h"
#include "Animation/AnimSingleNodeInstance.h"
#include "IEditableSkeleton.h"
#include "Widgets/Colors/SColorBlock.h"
#include "CurveViewerCommands.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Animation/EditorAnimCurveBoneLinks.h"
#include "HAL/PlatformApplicationMisc.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Misc/ScopedSlowTask.h"
#include "Engine/PoseWatch.h"
#include "Filters/GenericFilter.h"
#include "Filters/SBasicFilterBar.h"
#include "Widgets/Input/STextComboBox.h"
#include "AnimPreviewInstance.h"
#include "PersonaTabs.h"
#include "SAnimAssetFindReplace.h"
#include "Widgets/Docking/SDockTab.h"
#include "SPoseWatchPicker.h"
#define LOCTEXT_NAMESPACE "SAnimCurveViewer"
namespace CurveViewerColumns
{
static const FName AnimCurveNameLabel( "Curve Name" );
static const FName AnimCurveTypeLabel("Type");
static const FName AnimCurveWeightLabel( "Weight" );
static const FName AnimCurveEditLabel( "Edit" );
}
//////////////////////////////////////////////////////////////////////////
// SAnimCurveListRow
typedef TSharedPtr< FDisplayedAnimCurveInfo > FDisplayedAnimCurveInfoPtr;
// This is a flag that is used to filter UI part
enum class EAnimCurveViewerFilterFlags : uint8
{
// Show all
ShowAll = 0,
// Show active curves
Active = 0x01,
// Show morph target curves
MorphTarget = 0x02,
// Show material curves
Material = 0x04,
};
ENUM_CLASS_FLAGS(EAnimCurveViewerFilterFlags);
class FAnimCurveViewerFilter : public FGenericFilter<EAnimCurveViewerFilterFlags>
{
public:
FAnimCurveViewerFilter(EAnimCurveViewerFilterFlags InFlags, const FString& InName, const FText& InDisplayName, const FText& InToolTipText, FLinearColor InColor, TSharedPtr<FFilterCategory> InCategory)
: FGenericFilter<EAnimCurveViewerFilterFlags>(InCategory, InName, InDisplayName, FGenericFilter<EAnimCurveViewerFilterFlags>::FOnItemFiltered())
, Flags(InFlags)
{
ToolTip = InToolTipText;
Color = InColor;
}
bool IsActive() const
{
return bIsActive;
}
EAnimCurveViewerFilterFlags GetFlags() const
{
return Flags;
}
private:
// FFilterBase interface
virtual void ActiveStateChanged(bool bActive) override
{
bIsActive = bActive;
}
virtual bool PassesFilter(EAnimCurveViewerFilterFlags InItem) const override
{
return EnumHasAnyFlags(InItem, Flags);
}
private:
EAnimCurveViewerFilterFlags Flags;
bool bIsActive = false;
};
bool FDisplayedAnimCurveInfo::GetActiveFlag(const TSharedPtr<SAnimCurveViewer>& InAnimCurveViewer, bool bInMorphTarget) const
{
const UAnimInstance* AnimInstance = InAnimCurveViewer->GetAnimInstance();
if(AnimInstance && InAnimCurveViewer->PoseWatchPicker.IsValid())
{
// Find if we want to use a pose watch
if(UPoseWatchPoseElement* PoseWatchPoseElement = InAnimCurveViewer->PoseWatchPicker->GetCurrentPoseWatch())
{
if(UAnimBlueprintGeneratedClass* AnimClass = Cast<UAnimBlueprintGeneratedClass>(AnimInstance->GetClass()))
{
// We have to grab our pose watches from the root class as no pose watches can be set on child anim BPs
if(const UAnimBlueprintGeneratedClass* RootClass = Cast<UAnimBlueprintGeneratedClass>(AnimClass->GetRootClass()))
{
const FAnimBlueprintDebugData& DebugData = RootClass->AnimBlueprintDebugData;
for(const FAnimNodePoseWatch& AnimNodePoseWatch : DebugData.AnimNodePoseWatch)
{
if(AnimNodePoseWatch.PoseWatchPoseElement == PoseWatchPoseElement)
{
UE::Anim::ECurveElementFlags Flags = AnimNodePoseWatch.GetCurves().GetFlags(CurveName);
return EnumHasAnyFlags(Flags, bInMorphTarget ? UE::Anim::ECurveElementFlags::MorphTarget : UE::Anim::ECurveElementFlags::Material);
}
}
}
}
}
else
{
// See if curve is in active set, attribute curve should have everything
const TMap<FName, float>& CurveList = AnimInstance->GetAnimationCurveList(bInMorphTarget ? EAnimCurveType::MorphTargetCurve : EAnimCurveType::MaterialCurve);
const float* CurrentValue = CurveList.Find(CurveName);
if (CurrentValue)
{
return true;
}
}
}
return false;
}
void SAnimCurveListRow::Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView, const TSharedRef<IPersonaPreviewScene>& InPreviewScene)
{
Item = InArgs._Item;
AnimCurveViewerPtr = InArgs._AnimCurveViewerPtr;
PreviewScenePtr = InPreviewScene;
check( Item.IsValid() );
SMultiColumnTableRow< TSharedPtr<FDisplayedAnimCurveInfo> >::Construct( FSuperRowType::FArguments(), InOwnerTableView );
}
TSharedRef< SWidget > SAnimCurveListRow::GenerateWidgetForColumn( const FName& ColumnName )
{
if ( ColumnName == CurveViewerColumns::AnimCurveNameLabel )
{
TSharedPtr<SAnimCurveViewer> AnimCurveViewer = AnimCurveViewerPtr.Pin();
if (AnimCurveViewer.IsValid())
{
return
SNew(SVerticalBox)
.ToolTipText(this, &SAnimCurveListRow::GetItemName)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(4)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Font(this, &SAnimCurveListRow::GetItemFont)
.Text(this, &SAnimCurveListRow::GetItemName)
.HighlightText(this, &SAnimCurveListRow::GetFilterText)
];
}
}
else if (ColumnName == CurveViewerColumns::AnimCurveTypeLabel)
{
TSharedPtr<SAnimCurveViewer> AnimCurveViewer = AnimCurveViewerPtr.Pin();
if (AnimCurveViewer.IsValid())
{
return
SNew(SVerticalBox)
.ToolTipText(LOCTEXT("AnimCurveTypeTooltip", "The type of the curve (e.g. morph target, material)."))
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.VAlign(VAlign_Center)
[
GetCurveTypeWidget()
];
}
}
else if ( ColumnName == CurveViewerColumns::AnimCurveWeightLabel )
{
// Encase the SSpinbox in an SVertical box so we can apply padding. Setting ItemHeight on the containing SListView has no effect :-(
return
SNew( SVerticalBox )
.ToolTipText(LOCTEXT("AnimCurveWeightTooltip", "The current weight of the curve."))
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding( 0.0f, 1.0f )
.VAlign( VAlign_Center )
[
SNew( SSpinBox<float> )
.Font(FAppStyle::Get().GetFontStyle("SmallFont"))
.Value(this, &SAnimCurveListRow::GetWeight)
.MinSliderValue(this, &SAnimCurveListRow::GetMinWeight)
.MaxSliderValue(this, &SAnimCurveListRow::GetMaxWeight)
.OnValueChanged( this, &SAnimCurveListRow::OnAnimCurveWeightChanged )
.OnValueCommitted( this, &SAnimCurveListRow::OnAnimCurveWeightValueCommitted )
];
}
else if ( ColumnName == CurveViewerColumns::AnimCurveEditLabel)
{
return
SNew(SVerticalBox)
.ToolTipText(LOCTEXT("OverrideTooltip", "Whether the value of this curve is being overriden, or populated from the debugged data."))
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(0.0f, 1.0f)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(SCheckBox)
.OnCheckStateChanged(this, &SAnimCurveListRow::OnAnimCurveOverrideChecked)
.IsChecked(this, &SAnimCurveListRow::IsAnimCurveOverrideChecked)
];
}
return SNullWidget::NullWidget;
}
TSharedRef< SWidget > SAnimCurveListRow::GetCurveTypeWidget()
{
return SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.f, 1.f, 1.f, 1.f)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(SImage)
.ToolTipText(LOCTEXT("CurveTypeMorphTarget_Tooltip", "MorphTarget"))
.Image_Lambda([this]()
{
bool bHasCurveType = GetActiveFlag(true);
if(bHasCurveType)
{
return FAppStyle::GetBrush("AnimCurveViewer.MorphTargetOn");
}
else
{
return FAppStyle::GetBrush("AnimCurveViewer.MorphTargetOff");
}
})
]
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.f, 1.f, 1.f, 1.f)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(SImage)
.ToolTipText(LOCTEXT("CurveTypeMaterial_Tooltip", "Material"))
.Image_Lambda([this]()
{
bool bHasCurveType = GetActiveFlag(false);
if(bHasCurveType)
{
return FAppStyle::GetBrush("AnimCurveViewer.MaterialOn");
}
else
{
return FAppStyle::GetBrush("AnimCurveViewer.MaterialOff");
}
})
];
}
bool SAnimCurveListRow::GetActiveFlag(bool bMorphTarget) const
{
// If anim viewer
TSharedPtr<SAnimCurveViewer> AnimCurveViewer = AnimCurveViewerPtr.Pin();
if (AnimCurveViewer.IsValid())
{
return Item->GetActiveFlag(AnimCurveViewer, bMorphTarget);
}
return false;
}
ECheckBoxState SAnimCurveListRow::IsAnimCurveTypeBoxChangedChecked(bool bMorphTarget) const
{
bool bHasCurveType = GetActiveFlag(bMorphTarget);
return (bHasCurveType) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
void SAnimCurveListRow::OnAnimCurveOverrideChecked(ECheckBoxState InState)
{
Item->bOverrideData = InState == ECheckBoxState::Checked;
TSharedPtr<SAnimCurveViewer> AnimCurveViewer = AnimCurveViewerPtr.Pin();
if (AnimCurveViewer.IsValid())
{
if (Item->bOverrideData)
{
AnimCurveViewer->AddAnimCurveOverride(Item->CurveName, Item->Weight);
}
else
{
// clear value so that it can be filled up
AnimCurveViewer->RemoveAnimCurveOverride(Item->CurveName);
}
}
}
ECheckBoxState SAnimCurveListRow::IsAnimCurveOverrideChecked() const
{
return (Item->bOverrideData) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
void SAnimCurveListRow::OnAnimCurveWeightChanged( float NewWeight )
{
Item->Weight = NewWeight;
Item->bOverrideData = true;
TSharedPtr<SAnimCurveViewer> AnimCurveViewer = AnimCurveViewerPtr.Pin();
if (AnimCurveViewer.IsValid())
{
// If we try to slide an entry that is not selected, we select just it
bool bItemIsSelected = AnimCurveViewer->AnimCurveListView->IsItemSelected(Item);
if (!bItemIsSelected)
{
AnimCurveViewer->AnimCurveListView->SetSelection(Item, ESelectInfo::Direct);
}
// Add override
AnimCurveViewer->AddAnimCurveOverride(Item->CurveName, Item->Weight);
// ...then any selected rows need changing by the same delta
TArray< TSharedPtr< FDisplayedAnimCurveInfo > > SelectedRows = AnimCurveViewer->AnimCurveListView->GetSelectedItems();
for (auto ItemIt = SelectedRows.CreateIterator(); ItemIt; ++ItemIt)
{
TSharedPtr< FDisplayedAnimCurveInfo > RowItem = (*ItemIt);
if (RowItem != Item) // Don't do "this" row again if it's selected
{
RowItem->Weight = NewWeight;
RowItem->bOverrideData = true;
AnimCurveViewer->AddAnimCurveOverride(RowItem->CurveName, RowItem->Weight);
}
}
if(PreviewScenePtr.IsValid())
{
PreviewScenePtr.Pin()->InvalidateViews();
}
}
}
void SAnimCurveListRow::OnAnimCurveWeightValueCommitted( float NewWeight, ETextCommit::Type CommitType)
{
if (CommitType == ETextCommit::OnEnter || CommitType == ETextCommit::OnUserMovedFocus)
{
OnAnimCurveWeightChanged(NewWeight);
}
}
FText SAnimCurveListRow::GetItemName() const
{
return FText::FromName(Item->CurveName);
}
FText SAnimCurveListRow::GetFilterText() const
{
TSharedPtr<SAnimCurveViewer> AnimCurveViewer = AnimCurveViewerPtr.Pin();
if (AnimCurveViewer.IsValid())
{
return AnimCurveViewer->GetFilterText();
}
else
{
return FText::GetEmpty();
}
}
bool SAnimCurveListRow::GetActiveWeight(float& OutWeight) const
{
bool bFoundActive = false;
// If anim viewer
TSharedPtr<SAnimCurveViewer> AnimCurveViewer = AnimCurveViewerPtr.Pin();
if (AnimCurveViewer.IsValid())
{
const UAnimInstance* AnimInstance = AnimCurveViewer->GetAnimInstance();
if(AnimInstance && AnimCurveViewer->PoseWatchPicker.IsValid())
{
// Find if we want to use a pose watch
if(UPoseWatchPoseElement* PoseWatchPoseElement = AnimCurveViewer->PoseWatchPicker->GetCurrentPoseWatch())
{
if(UAnimBlueprintGeneratedClass* AnimClass = Cast<UAnimBlueprintGeneratedClass>(AnimInstance->GetClass()))
{
// We have to grab our pose watches from the root class as no pose watches can be set on child anim BPs
if(const UAnimBlueprintGeneratedClass* RootClass = Cast<UAnimBlueprintGeneratedClass>(AnimClass->GetRootClass()))
{
const FAnimBlueprintDebugData& DebugData = RootClass->AnimBlueprintDebugData;
for(const FAnimNodePoseWatch& AnimNodePoseWatch : DebugData.AnimNodePoseWatch)
{
if(AnimNodePoseWatch.PoseWatchPoseElement == PoseWatchPoseElement)
{
bool bHasElement = false;
float CurrentValue = AnimNodePoseWatch.GetCurves().Get(Item->CurveName, bHasElement);
if(bHasElement)
{
OutWeight = CurrentValue;
bFoundActive = true;
}
break;
}
}
}
}
}
else
{
// See if curve is in active set, attribute curve should have everything
const TMap<FName, float>& CurveList = AnimInstance->GetAnimationCurveList(EAnimCurveType::AttributeCurve);
const float* CurrentValue = CurveList.Find(Item->CurveName);
if (CurrentValue)
{
OutWeight = *CurrentValue;
bFoundActive = true;
}
}
}
}
return bFoundActive;
}
FSlateFontInfo SAnimCurveListRow::GetItemFont() const
{
// Show bright if active
float Weight = 0.f;
const bool bItemActive = GetActiveWeight(Weight);
return bItemActive ? FAppStyle::Get().GetFontStyle("AnimCurveViewer.ActiveCurveFont") : FAppStyle::Get().GetFontStyle("SmallFont");
}
float SAnimCurveListRow::GetWeight() const
{
float Weight = 0.0f;
bool bItemActive = false;
if (!Item->bOverrideData)
{
bItemActive = GetActiveWeight(Weight);
MinWeight = FMath::Min(MinWeight, Weight);
MaxWeight = FMath::Max(MaxWeight, Weight);
}
if(!bItemActive)
{
Weight = Item->Weight;
}
return Weight;
}
TOptional<float> SAnimCurveListRow::GetMinWeight() const
{
return MinWeight;
}
TOptional<float> SAnimCurveListRow::GetMaxWeight() const
{
return MaxWeight;
}
//////////////////////////////////////////////////////////////////////////
// SAnimCurveViewer
void SAnimCurveViewer::Construct(const FArguments& InArgs, const TSharedRef<IPersonaPreviewScene>& InPreviewScene, FOnObjectsSelected InOnObjectsSelected)
{
OnObjectsSelected = InOnObjectsSelected;
PreviewScenePtr = InPreviewScene;
EditableSkeletonPtr = InArgs._EditableSkeleton;
InPreviewScene->RegisterOnPreviewMeshChanged(FOnPreviewMeshChanged::CreateSP(this, &SAnimCurveViewer::OnPreviewMeshChanged));
InPreviewScene->RegisterOnAnimChanged(FOnAnimChanged::CreateSP(this, &SAnimCurveViewer::OnPreviewAssetChanged));
// Register and bind all our menu commands
FCurveViewerCommands::Register();
BindCommands();
CurrentCurveFlag = EAnimCurveViewerFilterFlags::Active;
TSharedPtr<FFilterCategory> FilterCategory = MakeShared<FFilterCategory>(LOCTEXT("CurveFiltersLabel", "Curve Filters"), LOCTEXT("CurveFiltersToolTip", "Filter what kind fo curves can be displayed."));
Filters.Add(MakeShared<FAnimCurveViewerFilter>(
EAnimCurveViewerFilterFlags::Active,
"Active",
LOCTEXT("ShowActiveLabel", "Active"),
LOCTEXT("ShowActiveTooltip", "Show only active curves"),
FLinearColor::Yellow,
FilterCategory
));
Filters.Add(MakeShared<FAnimCurveViewerFilter>(
EAnimCurveViewerFilterFlags::MorphTarget,
"MorphTarget",
LOCTEXT("MorphTargetLabel", "Morph Target"),
LOCTEXT("MorphTargetTooltip", "Show morph target curves"),
FLinearColor::Red,
FilterCategory
));
Filters.Add(MakeShared<FAnimCurveViewerFilter>(
EAnimCurveViewerFilterFlags::Material,
"Material",
LOCTEXT("MaterialLabel", "Material"),
LOCTEXT("MaterialTooltip", "Show material curves"),
FLinearColor::Green,
FilterCategory
));
TSharedRef<SBasicFilterBar<EAnimCurveViewerFilterFlags>> FilterBar = SNew(SBasicFilterBar<EAnimCurveViewerFilterFlags>)
.CustomFilters(Filters)
.bPinAllFrontendFilters(true)
.UseSectionsForCategories(true)
.OnFilterChanged_Lambda([this]()
{
CurrentCurveFlag = EAnimCurveViewerFilterFlags::ShowAll;
for(const TSharedRef<FFilterBase<EAnimCurveViewerFilterFlags>>& Filter : Filters)
{
TSharedRef<FAnimCurveViewerFilter> AnimCurveFilter = StaticCastSharedRef<FAnimCurveViewerFilter>(Filter);
if(AnimCurveFilter->IsActive())
{
CurrentCurveFlag |= AnimCurveFilter->GetFlags();
}
}
RefreshCurveList(true);
});
Filters[0]->SetActive(true);
ChildSlot
[
SNew( SVerticalBox )
+SVerticalBox::Slot()
.Padding(2.0f,2.0f)
.AutoHeight()
.HAlign(HAlign_Left)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SSimpleButton)
.Text(LOCTEXT("RefreshButton", "Refresh"))
.ToolTipText(LOCTEXT("RefreshButtonTooltip", "Refresh the displayed curves, clearing out any curves that are not currently active"))
.Icon(FAppStyle::GetBrush("Icons.Refresh"))
.OnClicked_Lambda([this]()
{
AllSeenAnimCurvesMap.Reset();
RefreshCurveList(true);
return FReply::Handled();
})
]
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SSimpleButton)
.Text(LOCTEXT("FindReplaceCurvesButton", "Find/Replace Curves..."))
.ToolTipText(LOCTEXT("FindReplaceCurvesButtonTooltip", "Find and replace curves across multiple assets"))
.Icon(FAppStyle::GetBrush("Kismet.Tabs.FindResults"))
.OnClicked_Lambda([this]()
{
FindReplaceCurves();
return FReply::Handled();
})
]
]
+SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f,2.0f)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(2.0f,0.0f)
[
SBasicFilterBar<EAnimCurveViewerFilterFlags>::MakeAddFilterButton(FilterBar)
]
+SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(2.0f,0.0f)
[
SAssignNew( NameFilterBox, SSearchBox )
.SelectAllTextWhenFocused( true )
.OnTextChanged( this, &SAnimCurveViewer::OnFilterTextChanged )
.OnTextCommitted( this, &SAnimCurveViewer::OnFilterTextCommitted )
]
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(2.0f,0.0f)
[
SNew(SBox)
.MaxDesiredWidth(200.0f)
[
CreateCurveSourceSelector()
]
]
]
+SVerticalBox::Slot()
.AutoHeight()
[
FilterBar
]
+SVerticalBox::Slot()
.FillHeight(1.0f)
[
SAssignNew(AnimCurveListView, SAnimCurveListType)
.OnContextMenuOpening( this, &SAnimCurveViewer::OnGetContextMenuContent )
.ListItemsSource( &AnimCurveList )
.OnGenerateRow( this, &SAnimCurveViewer::GenerateAnimCurveRow )
.SelectionMode(ESelectionMode::Multi)
.HeaderRow
(
SNew( SHeaderRow )
+ SHeaderRow::Column(CurveViewerColumns::AnimCurveNameLabel)
.FillWidth(1.f)
.DefaultLabel( LOCTEXT( "AnimCurveNameLabel", "Curve Name" ) )
.DefaultTooltip(LOCTEXT("AnimCurveNameTooltip", "The name of the curve."))
+ SHeaderRow::Column(CurveViewerColumns::AnimCurveTypeLabel)
.FixedWidth(48.0f)
.DefaultLabel(LOCTEXT("AnimCurveTypeLabel", "Type"))
.DefaultTooltip(LOCTEXT("AnimCurveTypeTooltip", "The type of the curve (e.g. morph target, material)."))
+ SHeaderRow::Column(CurveViewerColumns::AnimCurveWeightLabel )
.FillWidth(1.f)
.DefaultLabel( LOCTEXT( "AnimCurveWeightLabel", "Weight" ) )
.DefaultTooltip(LOCTEXT("AnimCurveWeightTooltip", "The current weight of the curve."))
+ SHeaderRow::Column(CurveViewerColumns::AnimCurveEditLabel)
.FixedWidth(24.0f)
.DefaultLabel(FText::GetEmpty())
.DefaultTooltip(LOCTEXT("OverrideTooltip", "Whether the value of this curve is being overriden, or populated from the debugged data."))
)
]
];
RefreshCurveList(true);
}
SAnimCurveViewer::~SAnimCurveViewer()
{
if (PreviewScenePtr.IsValid() )
{
PreviewScenePtr.Pin()->UnregisterOnPreviewMeshChanged(this);
PreviewScenePtr.Pin()->UnregisterOnAnimChanged(this);
}
AnimationEditorUtils::OnPoseWatchesChanged().RemoveAll(this);
}
void SAnimCurveViewer::OnPreviewMeshChanged(class USkeletalMesh* OldPreviewMesh, class USkeletalMesh* NewPreviewMesh)
{
RefreshCurveList(true);
}
void SAnimCurveViewer::OnFilterTextChanged( const FText& SearchText )
{
FilterText = SearchText;
RefreshCurveList(false);
}
void SAnimCurveViewer::OnFilterTextCommitted( const FText& SearchText, ETextCommit::Type CommitInfo )
{
// Just do the same as if the user typed in the box
OnFilterTextChanged( SearchText );
}
TSharedRef<ITableRow> SAnimCurveViewer::GenerateAnimCurveRow(TSharedPtr<FDisplayedAnimCurveInfo> InInfo, const TSharedRef<STableViewBase>& OwnerTable)
{
check( InInfo.IsValid() );
return
SNew( SAnimCurveListRow, OwnerTable, PreviewScenePtr.Pin().ToSharedRef() )
.Item( InInfo )
.AnimCurveViewerPtr( SharedThis(this) );
}
void SAnimCurveViewer::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
if (GetAnimInstance())
{
// We refresh when ticking each time as curve flags can potentially vary
RefreshCurveList(false);
}
}
FReply SAnimCurveViewer::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
if (UICommandList.IsValid() && UICommandList->ProcessCommandBindings(InKeyEvent))
{
return FReply::Handled();
}
return FReply::Unhandled();
}
UAnimInstance* SAnimCurveViewer::GetAnimInstance() const
{
UDebugSkelMeshComponent* MeshComponent = PreviewScenePtr.Pin()->GetPreviewMeshComponent();
UAnimInstance* AnimInstance = MeshComponent->GetAnimInstance();
if (AnimInstance)
{
// Look at the debugged anim instance if we are targeting one
if(AnimInstance == MeshComponent->PreviewInstance.Get())
{
UAnimPreviewInstance* AnimPreviewInstance = MeshComponent->PreviewInstance;
if(USkeletalMeshComponent* DebuggedComponent = AnimPreviewInstance->GetDebugSkeletalMeshComponent())
{
AnimInstance = DebuggedComponent->GetAnimInstance();
}
}
}
return AnimInstance;
}
void SAnimCurveViewer::CreateAnimCurveList( const FString& SearchText, bool bInFullRefresh )
{
if(!AnimCurveListView.IsValid())
{
return;
}
bool bDirty = bInFullRefresh;
AnimCurveList.Reset();
auto AddCurve = [this](FName InCurveName, UE::Anim::ECurveElementFlags InFlags)
{
// Only add if the curve doesnt exist
TSharedPtr<FDisplayedAnimCurveInfo>* ExistingItem = AllSeenAnimCurvesMap.Find(InCurveName);
if(ExistingItem == nullptr)
{
TSharedRef<FDisplayedAnimCurveInfo> NewInfo = FDisplayedAnimCurveInfo::Make(InCurveName);
float Weight = 0.f;
const bool bOverride = GetAnimCurveOverride(InCurveName, Weight);
NewInfo->bOverrideData = bOverride;
NewInfo->Weight = Weight;
NewInfo->bMaterial = EnumHasAnyFlags(InFlags, UE::Anim::ECurveElementFlags::Material);
NewInfo->bMorphTarget = EnumHasAnyFlags(InFlags, UE::Anim::ECurveElementFlags::MorphTarget);
AllSeenAnimCurvesMap.Add(InCurveName, NewInfo);
}
else
{
(*ExistingItem)->bMaterial = EnumHasAnyFlags(InFlags, UE::Anim::ECurveElementFlags::Material);
(*ExistingItem)->bMorphTarget = EnumHasAnyFlags(InFlags, UE::Anim::ECurveElementFlags::MorphTarget);
}
};
// Add curve items from skeleton metadata
if(TSharedPtr<IEditableSkeleton> EditableSkeleton = EditableSkeletonPtr.Pin())
{
EditableSkeleton->GetSkeleton().ForEachCurveMetaData([&AddCurve](FName InCurveName, const FCurveMetaData& InCurveMetaData)
{
UE::Anim::ECurveElementFlags Flags = UE::Anim::ECurveElementFlags::None;
if(InCurveMetaData.Type.bMaterial)
{
Flags |= UE::Anim::ECurveElementFlags::Material;
}
if(InCurveMetaData.Type.bMorphtarget)
{
Flags |= UE::Anim::ECurveElementFlags::MorphTarget;
}
AddCurve(InCurveName, Flags);
});
}
// Add active curves if required
TSet<FName> ActiveCurves;
const UAnimInstance* AnimInstance = GetAnimInstance();
if(AnimInstance && PoseWatchPicker.IsValid())
{
// Find if we want to use a pose watch
if(UPoseWatchPoseElement* PoseWatchPoseElement = PoseWatchPicker->GetCurrentPoseWatch())
{
if(UAnimBlueprintGeneratedClass* AnimClass = Cast<UAnimBlueprintGeneratedClass>(AnimInstance->GetClass()))
{
// We have to grab our pose watches from the root class as no pose watches can be set on child anim BPs
if(const UAnimBlueprintGeneratedClass* RootClass = Cast<UAnimBlueprintGeneratedClass>(AnimClass->GetRootClass()))
{
const FAnimBlueprintDebugData& DebugData = RootClass->AnimBlueprintDebugData;
for(const FAnimNodePoseWatch& AnimNodePoseWatch : DebugData.AnimNodePoseWatch)
{
if(AnimNodePoseWatch.PoseWatchPoseElement == PoseWatchPoseElement)
{
AnimNodePoseWatch.GetCurves().ForEachElement([this, &AddCurve, &ActiveCurves](const UE::Anim::FCurveElement& InElement)
{
if (EnumHasAnyFlags(CurrentCurveFlag, EAnimCurveViewerFilterFlags::MorphTarget))
{
if(EnumHasAnyFlags(InElement.Flags, UE::Anim::ECurveElementFlags::MorphTarget))
{
AddCurve(InElement.Name, InElement.Flags);
ActiveCurves.Add(InElement.Name);
}
}
if (EnumHasAnyFlags(CurrentCurveFlag, EAnimCurveViewerFilterFlags::Material))
{
if(EnumHasAnyFlags(InElement.Flags, UE::Anim::ECurveElementFlags::Material))
{
AddCurve(InElement.Name, InElement.Flags);
ActiveCurves.Add(InElement.Name);
}
}
// If we arent filtering by curve type, just show all curves
if(!EnumHasAnyFlags(CurrentCurveFlag, EAnimCurveViewerFilterFlags::MorphTarget | EAnimCurveViewerFilterFlags::Material))
{
AddCurve(InElement.Name, InElement.Flags);
ActiveCurves.Add(InElement.Name);
}
});
break;
}
}
}
}
}
else
{
if (EnumHasAnyFlags(CurrentCurveFlag, EAnimCurveViewerFilterFlags::MorphTarget))
{
for(const TPair<FName, float>& CurveValuePair : AnimInstance->GetAnimationCurveList(EAnimCurveType::MorphTargetCurve))
{
AddCurve(CurveValuePair.Key, UE::Anim::ECurveElementFlags::MorphTarget);
ActiveCurves.Add(CurveValuePair.Key);
}
}
if (EnumHasAnyFlags(CurrentCurveFlag, EAnimCurveViewerFilterFlags::Material))
{
for(const TPair<FName, float>& CurveValuePair : AnimInstance->GetAnimationCurveList(EAnimCurveType::MaterialCurve))
{
AddCurve(CurveValuePair.Key, UE::Anim::ECurveElementFlags::Material);
ActiveCurves.Add(CurveValuePair.Key);
}
}
// If we arent filtering by curve type, show 'attribute' curves
if(!EnumHasAnyFlags(CurrentCurveFlag, EAnimCurveViewerFilterFlags::MorphTarget | EAnimCurveViewerFilterFlags::Material))
{
for(const TPair<FName, float>& CurveValuePair : AnimInstance->GetAnimationCurveList(EAnimCurveType::AttributeCurve))
{
AddCurve(CurveValuePair.Key, UE::Anim::ECurveElementFlags::None);
ActiveCurves.Add(CurveValuePair.Key);
}
}
}
}
// Iterate through all curves that have been seen
for (const TPair<FName, TSharedPtr<FDisplayedAnimCurveInfo>>& CurveNameValuePair : AllSeenAnimCurvesMap)
{
bool bAddToList = true;
// See if we pass the search filter
if (!FilterText.IsEmpty())
{
if (!CurveNameValuePair.Key.ToString().Contains(*FilterText.ToString()))
{
bAddToList = false;
}
}
// If we passed that, see if we are filtering to only active
if (bAddToList && EnumHasAnyFlags(CurrentCurveFlag, EAnimCurveViewerFilterFlags::Active))
{
bAddToList = ActiveCurves.Contains(CurveNameValuePair.Key);
}
if(CurveNameValuePair.Value->bShown != bAddToList)
{
CurveNameValuePair.Value->bShown = bAddToList;
bDirty = true;
}
// If we still want to add
if (bAddToList)
{
AnimCurveList.Add(CurveNameValuePair.Value);
}
}
if(bDirty)
{
// Sort final list
struct FSortSmartNamesAlphabetically
{
bool operator()(const TSharedPtr<FDisplayedAnimCurveInfo>& A, const TSharedPtr<FDisplayedAnimCurveInfo>& B) const
{
return (A.Get()->CurveName.Compare(B.Get()->CurveName) < 0);
}
};
AnimCurveList.Sort(FSortSmartNamesAlphabetically());
AnimCurveListView->RequestListRefresh();
}
}
TSharedRef<SWidget> SAnimCurveViewer::CreateCurveSourceSelector()
{
bool bShowPoseWatches = false;
if(const UAnimInstance* AnimInstance = GetAnimInstance())
{
if(UAnimBlueprintGeneratedClass* AnimClass = Cast<UAnimBlueprintGeneratedClass>(AnimInstance->GetClass()))
{
bShowPoseWatches = true;
}
}
if(bShowPoseWatches)
{
PoseWatchPicker = SNew(SPoseWatchPicker)
.AnimBlueprintGeneratedClass_Lambda([this]() -> const UAnimBlueprintGeneratedClass*
{
if(const UAnimInstance* AnimInstance = GetAnimInstance())
{
return Cast<UAnimBlueprintGeneratedClass>(AnimInstance->GetClass());
}
return nullptr;
})
.DefaultEntryDisplayText(LOCTEXT("PoseWatchOutputCurves", "Output Curves"));
return PoseWatchPicker.ToSharedRef();
}
return SNullWidget::NullWidget;
}
void SAnimCurveViewer::AddAnimCurveOverride( FName& Name, float Weight)
{
float& Value = OverrideCurves.FindOrAdd(Name);
Value = Weight;
UAnimSingleNodeInstance* SingleNodeInstance = Cast<UAnimSingleNodeInstance>(GetAnimInstance());
if (SingleNodeInstance)
{
SingleNodeInstance->SetPreviewCurveOverride(Name, Value, false);
}
}
void SAnimCurveViewer::RemoveAnimCurveOverride(FName& Name)
{
OverrideCurves.Remove(Name);
UAnimSingleNodeInstance* SingleNodeInstance = Cast<UAnimSingleNodeInstance>(GetAnimInstance());
if (SingleNodeInstance)
{
SingleNodeInstance->SetPreviewCurveOverride(Name, 0.f, true);
}
}
bool SAnimCurveViewer::GetAnimCurveOverride(FName& Name, float& Weight)
{
Weight = 0.f;
float* WeightPtr = OverrideCurves.Find(Name);
if (WeightPtr)
{
Weight = *WeightPtr;
return true;
}
else
{
return false;
}
}
void SAnimCurveViewer::PostUndoRedo()
{
RefreshCurveList(true);
}
void SAnimCurveViewer::OnPreviewAssetChanged(class UAnimationAsset* NewAsset)
{
OverrideCurves.Empty();
RefreshCurveList(true);
}
void SAnimCurveViewer::ApplyCustomCurveOverride(UAnimInstance* AnimInstance) const
{
for (auto Iter = OverrideCurves.CreateConstIterator(); Iter; ++Iter)
{
// @todo we might want to save original curve flags? or just change curve to apply flags only
AnimInstance->AddCurveValue(Iter.Key(), Iter.Value());
}
}
void SAnimCurveViewer::RefreshCurveList(bool bInFullRefresh)
{
CreateAnimCurveList(FilterText.ToString(), bInFullRefresh);
}
void SAnimCurveViewer::HandleCurveMetaDataChange()
{
AnimCurveList.Empty();
RefreshCurveList(true);
}
void SAnimCurveViewer::BindCommands()
{
// This should not be called twice on the same instance
check(!UICommandList.IsValid());
UICommandList = MakeShared<FUICommandList>();
FUICommandList& CommandList = *UICommandList;
// Grab the list of menu commands to bind...
const FCurveViewerCommands& MenuActions = FCurveViewerCommands::Get();
// ...and bind them all
CommandList.MapAction(
MenuActions.FindCurveUses,
FExecuteAction::CreateSP(this, &SAnimCurveViewer::OnFindCurveUsesClicked),
FCanExecuteAction::CreateSP(this, &SAnimCurveViewer::CanFindCurveUses));
}
TSharedPtr<SWidget> SAnimCurveViewer::OnGetContextMenuContent() const
{
const bool bShouldCloseWindowAfterMenuSelection = true;
FMenuBuilder MenuBuilder( bShouldCloseWindowAfterMenuSelection, UICommandList);
const FCurveViewerCommands& Actions = FCurveViewerCommands::Get();
MenuBuilder.BeginSection("AnimCurveAction", LOCTEXT( "CurveAction", "Curve Actions" ) );
MenuBuilder.AddMenuEntry(Actions.FindCurveUses);
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
void SAnimCurveViewer::OnFindCurveUsesClicked()
{
FindReplaceCurves();
}
bool SAnimCurveViewer::CanFindCurveUses()
{
return AnimCurveListView->GetNumItemsSelected() == 1;
}
void SAnimCurveViewer::FindReplaceCurves()
{
FName CurveName = NAME_None;
bool bMorphTarget = false;
bool bMaterial = false;
TArray<TSharedPtr<FDisplayedAnimCurveInfo>> SelectedItems = AnimCurveListView->GetSelectedItems();
if(SelectedItems.Num() > 0)
{
CurveName = SelectedItems[0]->CurveName;
bMorphTarget = SelectedItems[0]->GetActiveFlag(SharedThis(this), true);
bMaterial = SelectedItems[0]->GetActiveFlag(SharedThis(this), false);
}
if(TSharedPtr<SDockTab> ActiveTab = FGlobalTabmanager::Get()->GetActiveTab())
{
if(TSharedPtr<FTabManager> TabManager = ActiveTab->GetTabManagerPtr())
{
if(TSharedPtr<SDockTab> Tab = TabManager->TryInvokeTab(FPersonaTabs::FindReplaceID))
{
TSharedRef<IAnimAssetFindReplace> FindReplaceWidget = StaticCastSharedRef<IAnimAssetFindReplace>(Tab->GetContent());
FindReplaceWidget->SetCurrentProcessor(UAnimAssetFindReplaceCurves::StaticClass());
if(CurveName != NAME_None)
{
if(UAnimAssetFindReplaceCurves* Processor = FindReplaceWidget->GetProcessor<UAnimAssetFindReplaceCurves>())
{
Processor->SetFindString(CurveName.ToString());
Processor->SetFindWholeWord(true);
Processor->SetSearchMaterials(bMaterial);
Processor->SetSearchMorphTargets(bMorphTarget);
}
}
}
}
}
}
#undef LOCTEXT_NAMESPACE