Files
UnrealEngineUWP/Engine/Source/Editor/Persona/Private/AnimGraphNodeDetails.cpp
Thomas Sarkanen 3e92b56aa0 Animation: thread-safe execution and property access improvements
"Call function" anim node:
- Adds the ability to call functions at different points in the anim graph execution (and under different conditions, e.g. when a branch has started blending in). Only thread-safe functions are allowed to be called.
- Adds a thread-safe override point BlueprintThreadSafeUpdateAnimation, called on worker threads for the main instance and for linked instances when they are relevant in the graph (only one call per frame for linked layer instances).

Subsystems:
- Added new override points pre/post event graph(s) (moved override point for worker thread work to around the thread safe update function call).

Improves property access integration:
- Property access now shows (and allows the user to override) the call site of accesses. This is to allow users to see when their property access calls will be made, hopefully making its use less confusing for power users.
- Tweaked UX for property access nodes and dropdowns.
- Anim node pins now have property access bindings in-line on the pin.

Also adds the abilility for the anim graph to opt-in (via a config flag) to more stringent thread safety checks. Disabled by default for now as this requires content fixup.

#jira UE-115745 - Anim Blueprint Encapsulation
#rb Jurre.deBaare

[CL 16434092 by Thomas Sarkanen in ue5-main branch]
2021-05-24 04:47:52 -04:00

1239 lines
40 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimGraphNodeDetails.h"
#include "Modules/ModuleManager.h"
#include "UObject/UnrealType.h"
#include "Widgets/Text/STextBlock.h"
#include "BoneContainer.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/AnimationAsset.h"
#include "DetailWidgetRow.h"
#include "IDetailPropertyRow.h"
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "PropertyCustomizationHelpers.h"
#include "SlateOptMacros.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SButton.h"
#include "Animation/AnimInstance.h"
#include "Animation/EditorParentPlayerListObj.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Widgets/SToolTip.h"
#include "IDocumentation.h"
#include "ObjectEditorUtils.h"
#include "AnimGraphNode_Base.h"
#include "Widgets/Views/STreeView.h"
#include "BoneSelectionWidget.h"
#include "Animation/BlendProfile.h"
#include "AnimGraphNode_AssetPlayerBase.h"
#include "ISkeletonEditorModule.h"
#include "EdGraph/EdGraph.h"
#include "BlueprintEditor.h"
#include "Animation/EditorAnimCurveBoneLinks.h"
#include "IEditableSkeleton.h"
#include "IDetailChildrenBuilder.h"
#include "Widgets/Input/SNumericEntryBox.h"
#include "BoneControllers/AnimNode_SkeletalControlBase.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Styling/CoreStyle.h"
#include "LODInfoUILayout.h"
#include "IPersonaToolkit.h"
#include "Interfaces/Interface_BoneReferenceSkeletonProvider.h"
#include "IPropertyAccessEditor.h"
#include "Algo/Accumulate.h"
#include "Kismet2/BlueprintEditorUtils.h"
#define LOCTEXT_NAMESPACE "KismetNodeWithOptionalPinsDetails"
/////////////////////////////////////////////////////
// FAnimGraphNodeDetails
TSharedRef<IDetailCustomization> FAnimGraphNodeDetails::MakeInstance()
{
return MakeShareable(new FAnimGraphNodeDetails());
}
void FAnimGraphNodeDetails::CustomizeDetails(class IDetailLayoutBuilder& DetailBuilder)
{
TArray< TWeakObjectPtr<UObject> > SelectedObjectsList;
DetailBuilder.GetObjectsBeingCustomized(SelectedObjectsList);
// Hide the pin options property; it's represented inline per-property instead
IDetailCategoryBuilder& PinOptionsCategory = DetailBuilder.EditCategory("PinOptions");
TSharedRef<IPropertyHandle> AvailablePins = DetailBuilder.GetProperty("ShowPinForProperties");
DetailBuilder.HideProperty(AvailablePins);
TSharedRef<IPropertyHandle> PropertyBindings = DetailBuilder.GetProperty("PropertyBindings");
DetailBuilder.HideProperty(PropertyBindings);
// get first animgraph nodes
UAnimGraphNode_Base* AnimGraphNode = Cast<UAnimGraphNode_Base>(SelectedObjectsList[0].Get());
if (AnimGraphNode == nullptr)
{
return;
}
// make sure type matches with all the nodes.
const UAnimGraphNode_Base* FirstNodeType = AnimGraphNode;
for (int32 Index = 1; Index < SelectedObjectsList.Num(); ++Index)
{
UAnimGraphNode_Base* CurrentNode = Cast<UAnimGraphNode_Base>(SelectedObjectsList[Index].Get());
if (!CurrentNode || CurrentNode->GetClass() != FirstNodeType->GetClass())
{
// if type mismatches, multi selection doesn't work, just return
return;
}
}
TargetSkeleton = AnimGraphNode->GetAnimBlueprint()->TargetSkeleton;
TargetSkeletonName = TargetSkeleton ? FString::Printf(TEXT("%s'%s'"), *TargetSkeleton->GetClass()->GetName(), *TargetSkeleton->GetPathName()) : FString(TEXT(""));
// Get the node property
const FStructProperty* NodeProperty = AnimGraphNode->GetFNodeProperty();
if (NodeProperty == nullptr)
{
return;
}
// customize anim graph node's own details if needed
AnimGraphNode->CustomizeDetails(DetailBuilder);
// Hide the Node property as we are going to be adding its inner properties below
TSharedRef<IPropertyHandle> NodePropertyHandle = DetailBuilder.GetProperty(NodeProperty->GetFName(), AnimGraphNode->GetClass());
DetailBuilder.HideProperty(NodePropertyHandle);
uint32 NumChildHandles = 0;
FPropertyAccess::Result Result = NodePropertyHandle->GetNumChildren(NumChildHandles);
if (Result != FPropertyAccess::Fail)
{
for (uint32 ChildHandleIndex = 0; ChildHandleIndex < NumChildHandles; ++ChildHandleIndex)
{
TSharedPtr<IPropertyHandle> TargetPropertyHandle = NodePropertyHandle->GetChildHandle(ChildHandleIndex);
if (TargetPropertyHandle.IsValid())
{
FProperty* TargetProperty = TargetPropertyHandle->GetProperty();
IDetailCategoryBuilder& CurrentCategory = DetailBuilder.EditCategory(FObjectEditorUtils::GetCategoryFName(TargetProperty));
int32 CustomPinIndex = AnimGraphNode->ShowPinForProperties.IndexOfByPredicate([TargetProperty](const FOptionalPinFromProperty& InOptionalPin)
{
return TargetProperty->GetFName() == InOptionalPin.PropertyName;
});
if (CustomPinIndex != INDEX_NONE)
{
const FOptionalPinFromProperty& OptionalPin = AnimGraphNode->ShowPinForProperties[CustomPinIndex];
// Not optional
if (!OptionalPin.bCanToggleVisibility && OptionalPin.bShowPin)
{
// Always displayed as a pin, so hide the property
DetailBuilder.HideProperty(TargetPropertyHandle);
continue;
}
if (!TargetPropertyHandle->GetProperty())
{
continue;
}
// if customized, do not do anything
if (TargetPropertyHandle->IsCustomized())
{
continue;
}
// sometimes because of order of customization
// this gets called first for the node you'd like to customize
// then the above statement won't work
// so you can mark certain property to have meta data "CustomizeProperty"
// which will trigger below statement
if (OptionalPin.bPropertyIsCustomized)
{
continue;
}
TSharedRef<SWidget> InternalCustomWidget = CreatePropertyWidget(TargetProperty, TargetPropertyHandle.ToSharedRef(), AnimGraphNode->GetClass());
if (OptionalPin.bCanToggleVisibility)
{
IDetailPropertyRow& PropertyRow = CurrentCategory.AddProperty(TargetPropertyHandle);
TSharedPtr<SWidget> NameWidget;
TSharedPtr<SWidget> ValueWidget;
FDetailWidgetRow Row;
PropertyRow.GetDefaultWidgets(NameWidget, ValueWidget, Row);
ValueWidget = (InternalCustomWidget == SNullWidget::NullWidget) ? ValueWidget : InternalCustomWidget;
const FName OptionalPinArrayEntryName(*FString::Printf(TEXT("ShowPinForProperties[%d].bShowPin"), CustomPinIndex));
TSharedRef<IPropertyHandle> ShowHidePropertyHandle = DetailBuilder.GetProperty(OptionalPinArrayEntryName);
ShowHidePropertyHandle->MarkHiddenByCustomization();
ValueWidget->SetVisibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateSP(this, &FAnimGraphNodeDetails::GetVisibilityOfProperty, ShowHidePropertyHandle)));
// If we have an edit condition, that comes as part of the default name widget, so just use a text block to avoid duplicate checkboxes
TSharedPtr<SWidget> PropertyNameWidget;
if (TargetProperty->HasMetaData(TEXT("EditCondition")))
{
PropertyNameWidget = SNew(STextBlock)
.Text(TargetProperty->GetDisplayNameText())
.Font(IDetailLayoutBuilder::GetDetailFont())
.ToolTipText(TargetProperty->GetToolTipText());
}
else
{
PropertyNameWidget = NameWidget;
}
NameWidget = PropertyNameWidget;
// we only show children if visibility is one
// whenever toggles, this gets called, so it will be refreshed
const bool bShowChildren = GetVisibilityOfProperty(ShowHidePropertyHandle) == EVisibility::Visible;
PropertyRow.CustomWidget(bShowChildren)
.NameContent()
.MinDesiredWidth(Row.NameWidget.MinWidth)
.MaxDesiredWidth(Row.NameWidget.MaxWidth)
[
NameWidget.ToSharedRef()
]
.ValueContent()
.MinDesiredWidth(Row.ValueWidget.MinWidth)
.MaxDesiredWidth(Row.ValueWidget.MaxWidth)
[
ValueWidget.ToSharedRef()
];
}
else if (InternalCustomWidget != SNullWidget::NullWidget)
{
// A few properties are internally customized within this customization. Here we
// catch instances of these that don't have an optional pin flag.
IDetailPropertyRow& PropertyRow = CurrentCategory.AddProperty(TargetPropertyHandle);
PropertyRow.CustomWidget()
.NameContent()
[
TargetPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
InternalCustomWidget
];
}
else
{
CurrentCategory.AddProperty(TargetPropertyHandle);
}
}
}
}
}
}
TSharedRef<SWidget> FAnimGraphNodeDetails::CreatePropertyWidget(FProperty* TargetProperty, TSharedRef<IPropertyHandle> TargetPropertyHandle, UClass* NodeClass)
{
if(const FObjectPropertyBase* ObjectProperty = CastField<const FObjectPropertyBase>( TargetProperty ))
{
if(ObjectProperty->PropertyClass->IsChildOf(UAnimationAsset::StaticClass()))
{
bool bAllowClear = !(ObjectProperty->PropertyFlags & CPF_NoClear);
return SNew(SObjectPropertyEntryBox)
.PropertyHandle(TargetPropertyHandle)
.AllowedClass(ObjectProperty->PropertyClass)
.AllowClear(bAllowClear)
.OnShouldFilterAsset(FOnShouldFilterAsset::CreateSP(this, &FAnimGraphNodeDetails::OnShouldFilterAnimAsset, NodeClass));
}
}
return SNullWidget::NullWidget;
}
bool FAnimGraphNodeDetails::OnShouldFilterAnimAsset( const FAssetData& AssetData, UClass* NodeToFilterFor ) const
{
FAssetDataTagMapSharedView::FFindTagResult Result = AssetData.TagsAndValues.FindTag("Skeleton");
if (Result.IsSet() && TargetSkeleton && TargetSkeleton->IsCompatibleSkeletonByAssetData(AssetData))
{
const UClass* AssetClass = AssetData.GetClass();
// If node is an 'asset player', only let you select the right kind of asset for it
if (!NodeToFilterFor->IsChildOf(UAnimGraphNode_AssetPlayerBase::StaticClass()) || SupportNodeClassForAsset(AssetClass, NodeToFilterFor))
{
return false;
}
}
return true;
}
EVisibility FAnimGraphNodeDetails::GetVisibilityOfProperty(TSharedRef<IPropertyHandle> Handle) const
{
bool bShowAsPin;
if (FPropertyAccess::Success == Handle->GetValue(/*out*/ bShowAsPin))
{
return bShowAsPin ? EVisibility::Hidden : EVisibility::Visible;
}
else
{
return EVisibility::Visible;
}
}
void FAnimGraphNodeDetails::OnBlendProfileChanged(UBlendProfile* NewProfile, TSharedPtr<IPropertyHandle> PropertyHandle)
{
if(PropertyHandle.IsValid())
{
PropertyHandle->SetValue(NewProfile);
}
}
TSharedRef<IPropertyTypeCustomization> FInputScaleBiasCustomization::MakeInstance()
{
return MakeShareable(new FInputScaleBiasCustomization());
}
float GetMinValue(float Scale, float Bias)
{
return Scale != 0.0f ? (FMath::Abs(Bias) < SMALL_NUMBER ? 0.0f : -Bias) / Scale : 0.0f; // to avoid displaying of - in front of 0
}
float GetMaxValue(float Scale, float Bias)
{
return Scale != 0.0f ? (1.0f - Bias) / Scale : 0.0f;
}
void UpdateInputScaleBiasWithMinValue(float MinValue, TSharedRef<class IPropertyHandle> InputBiasScaleStructPropertyHandle)
{
InputBiasScaleStructPropertyHandle->NotifyPreChange();
TSharedRef<class IPropertyHandle> BiasProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Bias").ToSharedRef();
TSharedRef<class IPropertyHandle> ScaleProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Scale").ToSharedRef();
TArray<void*> BiasDataArray;
TArray<void*> ScaleDataArray;
BiasProperty->AccessRawData(BiasDataArray);
ScaleProperty->AccessRawData(ScaleDataArray);
check(BiasDataArray.Num() == ScaleDataArray.Num());
for(int32 DataIndex = 0; DataIndex < BiasDataArray.Num(); ++DataIndex)
{
float* BiasPtr = (float*)BiasDataArray[DataIndex];
float* ScalePtr = (float*)ScaleDataArray[DataIndex];
check(BiasPtr);
check(ScalePtr);
const float MaxValue = GetMaxValue(*ScalePtr, *BiasPtr);
const float Difference = MaxValue - MinValue;
*ScalePtr = Difference != 0.0f? 1.0f / Difference : 0.0f;
*BiasPtr = -MinValue * *ScalePtr;
}
InputBiasScaleStructPropertyHandle->NotifyPostChange(EPropertyChangeType::ValueSet);
}
void UpdateInputScaleBiasWithMaxValue(float MaxValue, TSharedRef<class IPropertyHandle> InputBiasScaleStructPropertyHandle)
{
InputBiasScaleStructPropertyHandle->NotifyPreChange();
TSharedRef<class IPropertyHandle> BiasProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Bias").ToSharedRef();
TSharedRef<class IPropertyHandle> ScaleProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Scale").ToSharedRef();
TArray<void*> BiasDataArray;
TArray<void*> ScaleDataArray;
BiasProperty->AccessRawData(BiasDataArray);
ScaleProperty->AccessRawData(ScaleDataArray);
check(BiasDataArray.Num() == ScaleDataArray.Num());
for(int32 DataIndex = 0; DataIndex < BiasDataArray.Num(); ++DataIndex)
{
float* BiasPtr = (float*)BiasDataArray[DataIndex];
float* ScalePtr = (float*)ScaleDataArray[DataIndex];
check(BiasPtr);
check(ScalePtr);
const float MinValue = GetMinValue(*ScalePtr, *BiasPtr);
const float Difference = MaxValue - MinValue;
*ScalePtr = Difference != 0.0f ? 1.0f / Difference : 0.0f;
*BiasPtr = -MinValue * *ScalePtr;
}
InputBiasScaleStructPropertyHandle->NotifyPostChange(EPropertyChangeType::ValueSet);
}
TOptional<float> GetMinValueInputScaleBias(TSharedRef<class IPropertyHandle> InputBiasScaleStructPropertyHandle)
{
TSharedRef<class IPropertyHandle> BiasProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Bias").ToSharedRef();
TSharedRef<class IPropertyHandle> ScaleProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Scale").ToSharedRef();
float Scale = 1.0f;
float Bias = 0.0f;
if(ScaleProperty->GetValue(Scale) == FPropertyAccess::Success && BiasProperty->GetValue(Bias) == FPropertyAccess::Success)
{
return GetMinValue(Scale, Bias);
}
return TOptional<float>();
}
TOptional<float> GetMaxValueInputScaleBias(TSharedRef<class IPropertyHandle> InputBiasScaleStructPropertyHandle)
{
TSharedRef<class IPropertyHandle> BiasProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Bias").ToSharedRef();
TSharedRef<class IPropertyHandle> ScaleProperty = InputBiasScaleStructPropertyHandle->GetChildHandle("Scale").ToSharedRef();
float Scale = 1.0f;
float Bias = 0.0f;
if(ScaleProperty->GetValue(Scale) == FPropertyAccess::Success && BiasProperty->GetValue(Bias) == FPropertyAccess::Success)
{
return GetMaxValue(Scale, Bias);
}
return TOptional<float>();
}
void FInputScaleBiasCustomization::CustomizeHeader(TSharedRef<class IPropertyHandle> StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void FInputScaleBiasCustomization::CustomizeChildren( TSharedRef<class IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils )
{
TWeakPtr<IPropertyHandle> WeakStructPropertyHandle = StructPropertyHandle;
StructBuilder
.AddProperty(StructPropertyHandle)
.CustomWidget()
.NameContent()
[
StructPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
.MinDesiredWidth(250.0f)
.MaxDesiredWidth(250.0f)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.Padding(FMargin(0.0f, 2.0f, 3.0f, 2.0f))
[
SNew(SNumericEntryBox<float>)
.Font(IDetailLayoutBuilder::GetDetailFont())
.ToolTipText(LOCTEXT("MinInputScaleBias", "Minimum input value"))
.AllowSpin(true)
.MinSliderValue(0.0f)
.MaxSliderValue(2.0f)
.Value_Lambda([WeakStructPropertyHandle]()
{
return GetMinValueInputScaleBias(WeakStructPropertyHandle.Pin().ToSharedRef());
})
.OnValueChanged_Lambda([WeakStructPropertyHandle](float InValue)
{
UpdateInputScaleBiasWithMinValue(InValue, WeakStructPropertyHandle.Pin().ToSharedRef());
})
]
+SHorizontalBox::Slot()
.Padding(FMargin(0.0f, 2.0f, 0.0f, 2.0f))
[
SNew(SNumericEntryBox<float>)
.Font(IDetailLayoutBuilder::GetDetailFont())
.ToolTipText(LOCTEXT("MaxInputScaleBias", "Maximum input value"))
.AllowSpin(true)
.MinSliderValue(0.0f)
.MaxSliderValue(2.0f)
.Value_Lambda([WeakStructPropertyHandle]()
{
return GetMaxValueInputScaleBias(WeakStructPropertyHandle.Pin().ToSharedRef());
})
.OnValueChanged_Lambda([WeakStructPropertyHandle](float InValue)
{
UpdateInputScaleBiasWithMaxValue(InValue, WeakStructPropertyHandle.Pin().ToSharedRef());
})
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
/////////////////////////////////////////////////////////////////////////////////////////////
// FBoneReferenceCustomization
/////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<IPropertyTypeCustomization> FBoneReferenceCustomization::MakeInstance()
{
return MakeShareable(new FBoneReferenceCustomization());
}
void FBoneReferenceCustomization::CustomizeHeader( TSharedRef<IPropertyHandle> StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils )
{
// set property handle
SetPropertyHandle(StructPropertyHandle);
// set editable skeleton info from struct
SetEditableSkeleton(StructPropertyHandle);
if (TargetEditableSkeleton.IsValid() && BoneNameProperty->IsValidHandle())
{
HeaderRow
.NameContent()
[
StructPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
.MaxDesiredWidth(0.0f)
[
SNew(SBoneSelectionWidget)
.ToolTipText(StructPropertyHandle->GetToolTipText())
.OnBoneSelectionChanged(this, &FBoneReferenceCustomization::OnBoneSelectionChanged)
.OnGetSelectedBone(this, &FBoneReferenceCustomization::GetSelectedBone)
.OnGetReferenceSkeleton(this, &FBoneReferenceCustomization::GetReferenceSkeleton)
];
}
else
{
// if this FBoneReference is used by some other Outers, this will fail
// should warn programmers instead of silent fail
ensureAlways(!bEnsureOnInvalidSkeleton);
UE_LOG(LogAnimation, Warning, TEXT("FBoneReferenceCustomization::CustomizeHeader: SetEditableSkeleton failed to find an appropriate skeleton!"));
}
}
void FBoneReferenceCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
}
void FBoneReferenceCustomization::SetEditableSkeleton(TSharedRef<IPropertyHandle> StructPropertyHandle)
{
TArray<UObject*> Objects;
StructPropertyHandle->GetOuterObjects(Objects);
USkeleton* TargetSkeleton = nullptr;
TSharedPtr<IEditableSkeleton> EditableSkeleton;
bEnsureOnInvalidSkeleton = true;
auto FindSkeletonForObject = [this, &TargetSkeleton, &EditableSkeleton](UObject* InObject)
{
for( ; InObject; InObject = InObject->GetOuter())
{
if (UAnimGraphNode_Base* AnimGraphNode = Cast<UAnimGraphNode_Base>(InObject))
{
TargetSkeleton = AnimGraphNode->GetAnimBlueprint()->TargetSkeleton;
break;
}
if (USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(InObject))
{
TargetSkeleton = SkeletalMesh->GetSkeleton();
break;
}
if (ULODInfoUILayout* LODInfoUILayout = Cast<ULODInfoUILayout>(InObject))
{
USkeletalMesh* SkeletalMesh = LODInfoUILayout->GetPersonaToolkit()->GetPreviewMesh();
check(SkeletalMesh);
TargetSkeleton = SkeletalMesh->GetSkeleton();
break;
}
if (UAnimationAsset* AnimationAsset = Cast<UAnimationAsset>(InObject))
{
if(AnimationAsset->IsAsset())
{
TargetSkeleton = AnimationAsset->GetSkeleton();
break;
}
}
if (UAnimInstance* AnimInstance = Cast<UAnimInstance>(InObject))
{
if (AnimInstance->CurrentSkeleton)
{
TargetSkeleton = AnimInstance->CurrentSkeleton;
break;
}
else if (UAnimBlueprintGeneratedClass* AnimBPClass = Cast<UAnimBlueprintGeneratedClass>(AnimInstance->GetClass()))
{
TargetSkeleton = AnimBPClass->TargetSkeleton;
break;
}
}
// editor animation curve bone links are responsible for linking joints to curve
// this is editor object that only exists for editor
if (UEditorAnimCurveBoneLinks* AnimCurveObj = Cast<UEditorAnimCurveBoneLinks>(InObject))
{
EditableSkeleton = AnimCurveObj->EditableSkeleton.Pin();
break;
}
if (IBoneReferenceSkeletonProvider* SkeletonProvider = Cast<IBoneReferenceSkeletonProvider>(InObject))
{
TargetSkeleton = SkeletonProvider->GetSkeleton(bEnsureOnInvalidSkeleton);
break;
}
}
return TargetSkeleton != nullptr || EditableSkeleton != nullptr;
};
for (UObject* Object : Objects)
{
if(FindSkeletonForObject(Object))
{
break;
}
}
if (TargetSkeleton != nullptr)
{
ISkeletonEditorModule& SkeletonEditorModule = FModuleManager::LoadModuleChecked<ISkeletonEditorModule>("SkeletonEditor");
EditableSkeleton = SkeletonEditorModule.CreateEditableSkeleton(TargetSkeleton);
}
TargetEditableSkeleton = EditableSkeleton;
}
TSharedPtr<IPropertyHandle> FBoneReferenceCustomization::FindStructMemberProperty(TSharedRef<IPropertyHandle> PropertyHandle, const FName& PropertyName)
{
uint32 NumChildren = 0;
PropertyHandle->GetNumChildren(NumChildren);
for (uint32 ChildIdx = 0; ChildIdx < NumChildren; ++ChildIdx)
{
TSharedPtr<IPropertyHandle> ChildHandle = PropertyHandle->GetChildHandle(ChildIdx);
if (ChildHandle->GetProperty()->GetFName() == PropertyName)
{
return ChildHandle;
}
}
return TSharedPtr<IPropertyHandle>();
}
void FBoneReferenceCustomization::SetPropertyHandle(TSharedRef<IPropertyHandle> StructPropertyHandle)
{
BoneNameProperty = FindStructMemberProperty(StructPropertyHandle, GET_MEMBER_NAME_CHECKED(FBoneReference, BoneName));
check(BoneNameProperty->IsValidHandle());
}
void FBoneReferenceCustomization::OnBoneSelectionChanged(FName Name)
{
BoneNameProperty->SetValue(Name);
}
FName FBoneReferenceCustomization::GetSelectedBone(bool& bMultipleValues) const
{
FString OutText;
FPropertyAccess::Result Result = BoneNameProperty->GetValueAsFormattedString(OutText);
bMultipleValues = (Result == FPropertyAccess::MultipleValues);
return FName(*OutText);
}
const struct FReferenceSkeleton& FBoneReferenceCustomization::GetReferenceSkeleton() const
{
// retruning dummy skeleton if any reason, it is invalid
static FReferenceSkeleton DummySkeleton;
return (TargetEditableSkeleton.IsValid()) ? TargetEditableSkeleton.Get()->GetSkeleton().GetReferenceSkeleton() : DummySkeleton;
}
/////////////////////////////////////////////////////////////////////////////////////////////
// FBoneSocketTargetCustomization
/////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<IPropertyTypeCustomization> FBoneSocketTargetCustomization::MakeInstance()
{
return MakeShareable(new FBoneSocketTargetCustomization());
}
void FBoneSocketTargetCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
// set property handle
SetPropertyHandle(StructPropertyHandle);
// set editable skeleton info from struct
SetEditableSkeleton(StructPropertyHandle);
Build(StructPropertyHandle, ChildBuilder);
}
void FBoneSocketTargetCustomization::SetPropertyHandle(TSharedRef<IPropertyHandle> StructPropertyHandle)
{
TSharedPtr<IPropertyHandle> BoneReferenceProperty = FindStructMemberProperty(StructPropertyHandle, GET_MEMBER_NAME_CHECKED(FBoneSocketTarget, BoneReference));
check(BoneReferenceProperty->IsValidHandle());
BoneNameProperty = FindStructMemberProperty(BoneReferenceProperty.ToSharedRef(), GET_MEMBER_NAME_CHECKED(FBoneReference, BoneName));
TSharedPtr<IPropertyHandle> SocketReferenceProperty = FindStructMemberProperty(StructPropertyHandle, GET_MEMBER_NAME_CHECKED(FBoneSocketTarget, SocketReference));
check(SocketReferenceProperty->IsValidHandle());
SocketNameProperty = FindStructMemberProperty(SocketReferenceProperty.ToSharedRef(), GET_MEMBER_NAME_CHECKED(FSocketReference, SocketName));
UseSocketProperty = FindStructMemberProperty(StructPropertyHandle, GET_MEMBER_NAME_CHECKED(FBoneSocketTarget, bUseSocket));
check(BoneNameProperty->IsValidHandle() && SocketNameProperty->IsValidHandle() && UseSocketProperty->IsValidHandle());
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void FBoneSocketTargetCustomization::Build(TSharedRef<IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& ChildBuilder)
{
if (TargetEditableSkeleton.IsValid() && BoneNameProperty->IsValidHandle())
{
ChildBuilder
.AddProperty(StructPropertyHandle)
.CustomWidget()
.NameContent()
[
StructPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
SNew(SBoneSelectionWidget)
.ToolTipText(StructPropertyHandle->GetToolTipText())
.bShowSocket(true)
.OnBoneSelectionChanged(this, &FBoneSocketTargetCustomization::OnBoneSelectionChanged)
.OnGetSelectedBone(this, &FBoneSocketTargetCustomization::GetSelectedBone)
.OnGetReferenceSkeleton(this, &FBoneReferenceCustomization::GetReferenceSkeleton)
.OnGetSocketList(this, &FBoneSocketTargetCustomization::GetSocketList)
];
}
else
{
// if this FBoneSocketTarget is used by some other Outers, this will fail
// should warn programmers instead of silent fail
ensureAlways(false);
}
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
TSharedPtr<IPropertyHandle> FBoneSocketTargetCustomization::GetNameProperty() const
{
bool bUseSocket = false;
if (UseSocketProperty->GetValue(bUseSocket) == FPropertyAccess::Success)
{
if (bUseSocket)
{
return SocketNameProperty;
}
return BoneNameProperty;
}
return TSharedPtr<IPropertyHandle>();
}
void FBoneSocketTargetCustomization::OnBoneSelectionChanged(FName Name)
{
// figure out if the name is BoneName or socket name
if (TargetEditableSkeleton.IsValid())
{
bool bUseSocket = false;
if (GetReferenceSkeleton().FindBoneIndex(Name) == INDEX_NONE)
{
// make sure socket exists
const TArray<class USkeletalMeshSocket*>& Sockets = GetSocketList();
for (int32 Idx = 0; Idx < Sockets.Num(); ++Idx)
{
if (Sockets[Idx]->SocketName == Name)
{
bUseSocket = true;
break;
}
}
// we should find one
ensure(bUseSocket);
}
// set correct value
UseSocketProperty->SetValue(bUseSocket);
TSharedPtr<IPropertyHandle> NameProperty = GetNameProperty();
if (ensureAlways(NameProperty.IsValid()))
{
NameProperty->SetValue(Name);
}
}
}
FName FBoneSocketTargetCustomization::GetSelectedBone(bool& bMultipleValues) const
{
FString OutText;
TSharedPtr<IPropertyHandle> NameProperty = GetNameProperty();
if (NameProperty.IsValid())
{
FPropertyAccess::Result Result = NameProperty->GetValueAsFormattedString(OutText);
bMultipleValues = (Result == FPropertyAccess::MultipleValues);
}
else
{
// there is no single value
bMultipleValues = true;
return NAME_None;
}
return FName(*OutText);
}
const TArray<class USkeletalMeshSocket*>& FBoneSocketTargetCustomization::GetSocketList() const
{
if (TargetEditableSkeleton.IsValid())
{
return TargetEditableSkeleton.Get()->GetSkeleton().Sockets;
}
static TArray<class USkeletalMeshSocket*> DummyList;
return DummyList;
}
/////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<IDetailCustomization> FAnimGraphParentPlayerDetails::MakeInstance(TSharedRef<FBlueprintEditor> InBlueprintEditor)
{
return MakeShareable(new FAnimGraphParentPlayerDetails(InBlueprintEditor));
}
void FAnimGraphParentPlayerDetails::CustomizeDetails(class IDetailLayoutBuilder& DetailBuilder)
{
TArray<TWeakObjectPtr<UObject>> SelectedObjects;
DetailBuilder.GetObjectsBeingCustomized(SelectedObjects);
check(SelectedObjects.Num() == 1);
EditorObject = Cast<UEditorParentPlayerListObj>(SelectedObjects[0].Get());
check(EditorObject);
IDetailCategoryBuilder& Category = DetailBuilder.EditCategory("AnimGraphOverrides");
DetailBuilder.HideProperty("Overrides");
struct FObjectToEntryBuilder
{
private:
TMap<UObject*, TSharedPtr<FPlayerTreeViewEntry>> ObjectToEntryMap;
TArray<TSharedPtr<FPlayerTreeViewEntry>>& ListEntries;
private:
TSharedPtr<FPlayerTreeViewEntry> AddObject(UObject* Object)
{
TSharedPtr<FPlayerTreeViewEntry> Result = ObjectToEntryMap.FindRef(Object);
if (!Result.IsValid() && (Object != nullptr))
{
bool bTopLevel = false;
TSharedPtr<FPlayerTreeViewEntry> ThisNode;
if (UBlueprint* Blueprint = Cast<UBlueprint>(Object))
{
ThisNode = MakeShareable(new FPlayerTreeViewEntry(Blueprint->GetName(), EPlayerTreeViewEntryType::Blueprint));
bTopLevel = true;
}
else if (UAnimGraphNode_StateMachine* StateMachineNode = Cast<UAnimGraphNode_StateMachine>(Object))
{
// Don't create a node for these, the graph speaks for it
}
else if (UAnimGraphNode_AssetPlayerBase * AssetPlayerBase = Cast<UAnimGraphNode_AssetPlayerBase>(Object))
{
FString Title = AssetPlayerBase->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
ThisNode = MakeShareable(new FPlayerTreeViewEntry(Title, EPlayerTreeViewEntryType::Node));
}
else if (UAnimGraphNode_Base* Node = Cast<UAnimGraphNode_Base>(Object))
{
ThisNode = MakeShareable(new FPlayerTreeViewEntry(Node->GetName(), EPlayerTreeViewEntryType::Node));
}
else if (UEdGraph* Graph = Cast<UEdGraph>(Object))
{
ThisNode = MakeShareable(new FPlayerTreeViewEntry(Graph->GetName(), EPlayerTreeViewEntryType::Graph));
}
if (ThisNode.IsValid())
{
ObjectToEntryMap.Add(Object, ThisNode);
}
if (bTopLevel)
{
ListEntries.Add(ThisNode);
Result = ThisNode;
}
else
{
TSharedPtr<FPlayerTreeViewEntry> Outer = AddObject(Object->GetOuter());
Result = Outer;
if (ThisNode.IsValid())
{
Result = ThisNode;
check(Outer.IsValid())
Outer->Children.Add(Result);
}
}
}
return Result;
}
void SortInternal(TArray<TSharedPtr<FPlayerTreeViewEntry>>& ListToSort)
{
ListToSort.Sort([](TSharedPtr<FPlayerTreeViewEntry> A, TSharedPtr<FPlayerTreeViewEntry> B) { return A->EntryName < B->EntryName; });
for (TSharedPtr<FPlayerTreeViewEntry>& Entry : ListToSort)
{
SortInternal(Entry->Children);
}
}
public:
FObjectToEntryBuilder(TArray<TSharedPtr<FPlayerTreeViewEntry>>& InListEntries)
: ListEntries(InListEntries)
{
}
void AddNode(UAnimGraphNode_Base* Node, FAnimParentNodeAssetOverride& Override)
{
TSharedPtr<FPlayerTreeViewEntry> Result = AddObject(Node);
if (Result.IsValid())
{
Result->Override = &Override;
}
}
void Sort()
{
SortInternal(ListEntries);
}
};
FObjectToEntryBuilder EntryBuilder(ListEntries);
// Build a hierarchy of entires for a tree view in the form of Blueprint->Graph->Node
for (FAnimParentNodeAssetOverride& Override : EditorObject->Overrides)
{
UAnimGraphNode_Base* Node = EditorObject->GetVisualNodeFromGuid(Override.ParentNodeGuid);
EntryBuilder.AddNode(Node, Override);
}
// Sort the nodes
EntryBuilder.Sort();
FDetailWidgetRow& Row = Category.AddCustomRow(FText::GetEmpty());
TSharedRef<STreeView<TSharedPtr<FPlayerTreeViewEntry>>> TreeView = SNew(STreeView<TSharedPtr<FPlayerTreeViewEntry>>)
.SelectionMode(ESelectionMode::None)
.OnGenerateRow(this, &FAnimGraphParentPlayerDetails::OnGenerateRow)
.OnGetChildren(this, &FAnimGraphParentPlayerDetails::OnGetChildren)
.TreeItemsSource(&ListEntries)
.HeaderRow
(
SNew(SHeaderRow)
+SHeaderRow::Column(FName("Name"))
.FillWidth(0.5f)
.DefaultLabel(LOCTEXT("ParentPlayer_NameCol", "Name"))
+SHeaderRow::Column(FName("Asset"))
.FillWidth(0.5f)
.DefaultLabel(LOCTEXT("ParentPlayer_AssetCol", "Asset"))
);
// Expand top level (blueprint) entries so the panel seems less empty
for (TSharedPtr<FPlayerTreeViewEntry> Entry : ListEntries)
{
TreeView->SetItemExpansion(Entry, true);
}
Row
[
TreeView->AsShared()
];
}
TSharedRef<ITableRow> FAnimGraphParentPlayerDetails::OnGenerateRow(TSharedPtr<FPlayerTreeViewEntry> EntryPtr, const TSharedRef< STableViewBase >& OwnerTable)
{
return SNew(SParentPlayerTreeRow, OwnerTable).Item(EntryPtr).OverrideObject(EditorObject).BlueprintEditor(BlueprintEditorPtr);
}
void FAnimGraphParentPlayerDetails::OnGetChildren(TSharedPtr<FPlayerTreeViewEntry> InParent, TArray< TSharedPtr<FPlayerTreeViewEntry> >& OutChildren)
{
OutChildren.Append(InParent->Children);
}
void SParentPlayerTreeRow::Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView)
{
Item = InArgs._Item;
EditorObject = InArgs._OverrideObject;
BlueprintEditor = InArgs._BlueprintEditor;
if(Item->Override)
{
GraphNode = EditorObject->GetVisualNodeFromGuid(Item->Override->ParentNodeGuid);
}
else
{
GraphNode = NULL;
}
SMultiColumnTableRow<TSharedPtr<FAnimGraphParentPlayerDetails>>::Construct(FSuperRowType::FArguments(), InOwnerTableView);
}
TSharedRef<SWidget> SParentPlayerTreeRow::GenerateWidgetForColumn(const FName& ColumnName)
{
TSharedPtr<SHorizontalBox> HorizBox;
SAssignNew(HorizBox, SHorizontalBox);
if(ColumnName == "Name")
{
HorizBox->AddSlot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SExpanderArrow, SharedThis(this))
];
Item->GenerateNameWidget(HorizBox);
}
else if(Item->Override)
{
HorizBox->AddSlot()
.Padding(2)
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SButton)
.ButtonStyle(FEditorStyle::Get(), "ToggleButton")
.ToolTip(IDocumentation::Get()->CreateToolTip(LOCTEXT("FocusNodeButtonTip", "Open the graph that contains this node in read-only mode and focus on the node"), NULL, "Shared/Editors/Persona", "FocusNodeButton"))
.OnClicked(FOnClicked::CreateSP(this, &SParentPlayerTreeRow::OnFocusNodeButtonClicked))
.Content()
[
SNew(SImage)
.Image(FEditorStyle::GetBrush("GenericViewButton"))
]
];
TArray<const UClass*> AllowedClasses;
AllowedClasses.Add(UAnimationAsset::StaticClass());
HorizBox->AddSlot()
.VAlign(VAlign_Center)
.FillWidth(1.f)
[
SNew(SObjectPropertyEntryBox)
.ObjectPath(this, &SParentPlayerTreeRow::GetCurrentAssetPath)
.OnShouldFilterAsset(this, &SParentPlayerTreeRow::OnShouldFilterAsset)
.OnObjectChanged(this, &SParentPlayerTreeRow::OnAssetSelected)
.AllowedClass(GetCurrentAssetToUse()->GetClass())
];
HorizBox->AddSlot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SButton)
.ButtonStyle(FEditorStyle::Get(), "NoBorder")
.Visibility(this, &SParentPlayerTreeRow::GetResetToDefaultVisibility)
.OnClicked(this, &SParentPlayerTreeRow::OnResetButtonClicked)
.ToolTip(IDocumentation::Get()->CreateToolTip(LOCTEXT("ResetToParentButtonTip", "Undo the override, returning to the default asset for this node"), NULL, "Shared/Editors/Persona", "ResetToParentButton"))
.Content()
[
SNew(SImage)
.Image(FEditorStyle::GetBrush("PropertyWindow.DiffersFromDefault"))
]
];
}
return HorizBox.ToSharedRef();
}
bool SParentPlayerTreeRow::OnShouldFilterAsset(const FAssetData& AssetData)
{
const USkeleton* CurrentSkeleton = GraphNode->GetAnimBlueprint()->TargetSkeleton;
return !CurrentSkeleton->IsCompatibleSkeletonByAssetData(AssetData);
}
void SParentPlayerTreeRow::OnAssetSelected(const FAssetData& AssetData)
{
Item->Override->NewAsset = Cast<UAnimationAsset>(AssetData.GetAsset());
EditorObject->ApplyOverrideToBlueprint(*Item->Override);
}
FReply SParentPlayerTreeRow::OnFocusNodeButtonClicked()
{
TSharedPtr<FBlueprintEditor> SharedBlueprintEditor = BlueprintEditor.Pin();
if(SharedBlueprintEditor.IsValid())
{
if(GraphNode)
{
UEdGraph* EdGraph = GraphNode->GetGraph();
TSharedPtr<SGraphEditor> GraphEditor = SharedBlueprintEditor->OpenGraphAndBringToFront(EdGraph);
if (GraphEditor.IsValid())
{
GraphEditor->JumpToNode(GraphNode, false);
}
}
return FReply::Handled();
}
return FReply::Unhandled();
}
const UAnimationAsset* SParentPlayerTreeRow::GetCurrentAssetToUse() const
{
if(Item->Override->NewAsset)
{
return Item->Override->NewAsset;
}
if(GraphNode)
{
return GraphNode->GetAnimationAsset();
}
return NULL;
}
EVisibility SParentPlayerTreeRow::GetResetToDefaultVisibility() const
{
FAnimParentNodeAssetOverride* HierarchyOverride = EditorObject->GetBlueprint()->GetAssetOverrideForNode(Item->Override->ParentNodeGuid, true);
if(HierarchyOverride)
{
return Item->Override->NewAsset != HierarchyOverride->NewAsset ? EVisibility::Visible : EVisibility::Hidden;
}
return Item->Override->NewAsset != GraphNode->GetAnimationAsset() ? EVisibility::Visible : EVisibility::Hidden;
}
FReply SParentPlayerTreeRow::OnResetButtonClicked()
{
FAnimParentNodeAssetOverride* HierarchyOverride = EditorObject->GetBlueprint()->GetAssetOverrideForNode(Item->Override->ParentNodeGuid, true);
Item->Override->NewAsset = HierarchyOverride ? ToRawPtr(HierarchyOverride->NewAsset) : GraphNode->GetAnimationAsset();
// Apply will remove the override from the object
EditorObject->ApplyOverrideToBlueprint(*Item->Override);
return FReply::Handled();
}
FString SParentPlayerTreeRow::GetCurrentAssetPath() const
{
const UAnimationAsset* Asset = GetCurrentAssetToUse();
return Asset ? Asset->GetPathName() : FString("");
}
FORCENOINLINE bool FPlayerTreeViewEntry::operator==(const FPlayerTreeViewEntry& Other)
{
return EntryName == Other.EntryName;
}
void FPlayerTreeViewEntry::GenerateNameWidget(TSharedPtr<SHorizontalBox> Box)
{
// Get an appropriate image icon for the row
const FSlateBrush* EntryImageBrush = NULL;
switch(EntryType)
{
case EPlayerTreeViewEntryType::Blueprint:
EntryImageBrush = FEditorStyle::GetBrush("ClassIcon.Blueprint");
break;
case EPlayerTreeViewEntryType::Graph:
EntryImageBrush = FEditorStyle::GetBrush("GraphEditor.EventGraph_16x");
break;
case EPlayerTreeViewEntryType::Node:
EntryImageBrush = FEditorStyle::GetBrush("GraphEditor.Default_16x");
break;
default:
break;
}
Box->AddSlot()
.VAlign(VAlign_Center)
.AutoWidth()
[
SNew(SImage)
.Image(EntryImageBrush)
];
Box->AddSlot()
.VAlign(VAlign_Center)
.Padding(FMargin(5.0f, 0.0f, 0.0f, 0.0f))
.AutoWidth()
[
SNew(STextBlock)
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
.Text(FText::FromString(EntryName))
];
}
void FAnimGraphNodeBindingExtension::GetOptionalPinData(const IPropertyHandle& PropertyHandle, int32& OutOptionalPinIndex, UAnimGraphNode_Base*& OutAnimGraphNode) const
{
OutOptionalPinIndex = INDEX_NONE;
TArray<UObject*> Objects;
PropertyHandle.GetOuterObjects(Objects);
FProperty* Property = PropertyHandle.GetProperty();
if(Property)
{
OutAnimGraphNode = Cast<UAnimGraphNode_Base>(Objects[0]);
if (OutAnimGraphNode != nullptr)
{
OutOptionalPinIndex = OutAnimGraphNode->ShowPinForProperties.IndexOfByPredicate([Property](const FOptionalPinFromProperty& InOptionalPin)
{
return Property->GetFName() == InOptionalPin.PropertyName;
});
}
}
}
bool FAnimGraphNodeBindingExtension::IsPropertyExtendable(const UClass* InObjectClass, const IPropertyHandle& PropertyHandle) const
{
int32 OptionalPinIndex;
UAnimGraphNode_Base* AnimGraphNode;
GetOptionalPinData(PropertyHandle, OptionalPinIndex, AnimGraphNode);
if(OptionalPinIndex != INDEX_NONE)
{
const FOptionalPinFromProperty& OptionalPin = AnimGraphNode->ShowPinForProperties[OptionalPinIndex];
// Not optional
if (!OptionalPin.bCanToggleVisibility && OptionalPin.bShowPin)
{
return false;
}
if(!PropertyHandle.GetProperty())
{
return false;
}
return OptionalPin.bCanToggleVisibility;
}
return false;
}
TSharedRef<SWidget> FAnimGraphNodeBindingExtension::GenerateExtensionWidget(const IDetailLayoutBuilder& InDetailBuilder, const UClass* InObjectClass, TSharedPtr<IPropertyHandle> InPropertyHandle)
{
int32 OptionalPinIndex;
UAnimGraphNode_Base* AnimGraphNode;
GetOptionalPinData(*InPropertyHandle.Get(), OptionalPinIndex, AnimGraphNode);
check(OptionalPinIndex != INDEX_NONE);
TArray<UObject*> OuterObjects;
InPropertyHandle->GetOuterObjects(OuterObjects);
TArray<UAnimGraphNode_Base*> AnimGraphNodes;
Algo::Transform(OuterObjects, AnimGraphNodes, [](UObject* InObject){ return Cast<UAnimGraphNode_Base>(InObject); });
FProperty* AnimNodeProperty = InPropertyHandle->GetProperty();
TSharedPtr<IPropertyHandle> ParentHandle = InPropertyHandle->GetParentHandle();
FProperty* ParentProperty = ParentHandle.IsValid() ? ParentHandle->GetProperty() : nullptr;
FName PropertyName;
if(FArrayProperty* ParentArrayProperty = CastField<FArrayProperty>(ParentProperty))
{
int32 ArrayIndex = InPropertyHandle->GetIndexInArray();
check(ArrayIndex != INDEX_NONE);
PropertyName = FName(ParentArrayProperty->GetFName(), InPropertyHandle->GetIndexInArray() + 1);
AnimNodeProperty = ParentArrayProperty;
}
else
{
PropertyName = AnimNodeProperty->GetFName();
}
const FName OptionalPinArrayEntryName(*FString::Printf(TEXT("ShowPinForProperties[%d].bShowPin"), OptionalPinIndex));
TSharedRef<IPropertyHandle> ShowPinPropertyHandle = InDetailBuilder.GetProperty(OptionalPinArrayEntryName, UAnimGraphNode_Base::StaticClass());
ShowPinPropertyHandle->MarkHiddenByCustomization();
UAnimGraphNode_Base::FAnimPropertyBindingWidgetArgs Args(AnimGraphNodes, AnimNodeProperty, PropertyName, OptionalPinIndex);
Args.bOnGraphNode = false;
return UAnimGraphNode_Base::MakePropertyBindingWidget(Args);
}
#undef LOCTEXT_NAMESPACE