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

443 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimGraphDetails.h"
#include "IAnimationBlueprintEditor.h"
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "EdGraph/EdGraph.h"
#include "AnimGraphNode_LinkedInputPose.h"
#include "Algo/Transform.h"
#include "DetailWidgetRow.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Layout/SBox.h"
#include "EdGraphSchema_K2_Actions.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Images/SImage.h"
#include "AnimGraphNode_Root.h"
#include "ScopedTransaction.h"
#include "ObjectEditorUtils.h"
#include "SKismetInspector.h"
#include "AnimationGraph.h"
#include "AnimationGraphSchema.h"
#include "AnimGraphNode_LinkedAnimLayer.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#define LOCTEXT_NAMESPACE "FAnimGraphDetails"
TSharedPtr<IDetailCustomization> FAnimGraphDetails::MakeInstance(TSharedPtr<IBlueprintEditor> InBlueprintEditor)
{
const TArray<UObject*>* Objects = (InBlueprintEditor.IsValid() ? InBlueprintEditor->GetObjectsCurrentlyBeingEdited() : nullptr);
if (Objects && Objects->Num() == 1)
{
if (UAnimBlueprint* AnimBlueprint = Cast<UAnimBlueprint>((*Objects)[0]))
{
return MakeShareable(new FAnimGraphDetails(StaticCastSharedPtr<IAnimationBlueprintEditor>(InBlueprintEditor), AnimBlueprint));
}
}
return nullptr;
}
void FAnimGraphDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
{
TArray<TWeakObjectPtr<UObject>> Objects;
DetailLayout.GetObjectsBeingCustomized(Objects);
Graph = CastChecked<UEdGraph>(Objects[0].Get());
if(IsInterface())
{
DetailLayout.HideCategory("GraphBlending");
}
bool const bIsStateMachine = !Graph->GetOuter()->IsA(UAnimBlueprint::StaticClass());
if(Objects.Num() > 1 || bIsStateMachine)
{
IDetailCategoryBuilder& InputsCategory = DetailLayout.EditCategory("Inputs", LOCTEXT("LinkedInputPoseInputsCategory", "Inputs"));
InputsCategory.SetCategoryVisibility(false);
return;
}
const bool bIsDefaultGraph = Graph->GetFName() == UEdGraphSchema_K2::GN_AnimGraph;
if(!Graph->bAllowDeletion && !bIsDefaultGraph)
{
FText ReadOnlyWarning = LOCTEXT("ReadOnlyWarning", "This graph's inputs are read-only and cannot be edited");
IDetailCategoryBuilder& InputsCategory = DetailLayout.EditCategory("Inputs", LOCTEXT("LinkedInputPoseInputsCategory", "Inputs"));
InputsCategory.SetCategoryVisibility(false);
IDetailCategoryBuilder& WarningCategoryBuilder = DetailLayout.EditCategory("GraphInputs", LOCTEXT("GraphInputsCategory", "Graph Inputs"));
WarningCategoryBuilder.AddCustomRow(ReadOnlyWarning)
.WholeRowContent()
[
SNew(STextBlock)
.Text(ReadOnlyWarning)
.Font(IDetailLayoutBuilder::GetDetailFont())
];
return;
}
if(!bIsDefaultGraph)
{
IDetailCategoryBuilder& LayerCategory = DetailLayout.EditCategory("Layer", LOCTEXT("LayerCategory", "Layer"));
{
FText GroupLabel(LOCTEXT("LayerGroup", "Group"));
FText GroupToolTip(LOCTEXT("LayerGroupToolTip", "The group of this layer. Grouped layers will run using the same underlying instance, so can share state."));
RefreshGroupSource();
LayerCategory.AddCustomRow(GroupLabel)
.NameContent()
[
SNew(STextBlock)
.Text(GroupLabel)
.ToolTipText(GroupToolTip)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
[
SAssignNew(GroupComboButton, SComboButton)
.ContentPadding(FMargin(0,0,5,0))
.ToolTipText(GroupToolTip)
.ButtonContent()
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("NoBorder"))
.Padding(FMargin(0, 0, 5, 0))
[
SNew(SEditableTextBox)
.Text(this, &FAnimGraphDetails::OnGetGroupText)
.OnTextCommitted(this, &FAnimGraphDetails::OnGroupTextCommitted)
.ToolTipText(GroupToolTip)
.SelectAllTextWhenFocused(true)
.RevertTextOnEscape(true)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
]
.MenuContent()
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
.MaxHeight(400.0f)
[
SAssignNew(GroupListView, SListView<TSharedPtr<FText>>)
.ListItemsSource(&GroupSource)
.OnGenerateRow(this, &FAnimGraphDetails::MakeGroupViewWidget)
.OnSelectionChanged(this, &FAnimGraphDetails::OnGroupSelectionChanged)
]
]
];
}
}
IDetailCategoryBuilder& InputsCategory = DetailLayout.EditCategory("Inputs", LOCTEXT("LinkedInputPoseInputsCategory", "Inputs"));
InputsCategory.RestoreExpansionState(true);
DetailLayoutBuilder = &DetailLayout;
// Gather inputs, if any
TArray<UAnimGraphNode_LinkedInputPose*> LinkedInputPoseInputs;
Graph->GetNodesOfClass<UAnimGraphNode_LinkedInputPose>(LinkedInputPoseInputs);
TSharedRef<SHorizontalBox> InputsHeaderContentWidget = SNew(SHorizontalBox);
TWeakPtr<SWidget> WeakInputsHeaderWidget = InputsHeaderContentWidget;
InputsHeaderContentWidget->AddSlot()
[
SNew(SHorizontalBox)
];
InputsHeaderContentWidget->AddSlot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SButton)
.ButtonStyle(FAppStyle::Get(), "RoundButton")
.ForegroundColor(FAppStyle::GetSlateColor("DefaultForeground"))
.ContentPadding(FMargin(2, 0))
.OnClicked(this, &FAnimGraphDetails::OnAddNewInputPoseClicked)
.HAlign(HAlign_Right)
.ToolTipText(LOCTEXT("NewInputPoseTooltip", "Create a new input pose"))
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(0, 1))
[
SNew(SImage)
.Image(FAppStyle::GetBrush("Plus"))
]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
.Padding(FMargin(2, 0, 0, 0))
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFontBold())
.Text(LOCTEXT("NewInputPoseButtonText", "New Input Pose"))
.Visibility(this, &FAnimGraphDetails::OnGetNewInputPoseTextVisibility, WeakInputsHeaderWidget)
.ShadowOffset(FVector2D(1, 1))
]
]
];
InputsCategory.HeaderContent(InputsHeaderContentWidget);
if(LinkedInputPoseInputs.Num())
{
for(UAnimGraphNode_LinkedInputPose* LinkedInputPoseNode : LinkedInputPoseInputs)
{
auto GetLinkedInputPoseLabel = [WeakLinkedInputPoseNode = TWeakObjectPtr<UAnimGraphNode_LinkedInputPose>(LinkedInputPoseNode)]()
{
if(WeakLinkedInputPoseNode.IsValid())
{
return FText::FromName(WeakLinkedInputPoseNode->Node.Name);
}
return FText::GetEmpty();
};
TArray<UObject*> ExternalObjects;
ExternalObjects.Add(LinkedInputPoseNode);
FAddPropertyParams AddPropertyParams;
AddPropertyParams.UniqueId(LinkedInputPoseNode->GetFName());
if(IDetailPropertyRow* LinkedInputPoseRow = InputsCategory.AddExternalObjects(ExternalObjects, EPropertyLocation::Default, AddPropertyParams))
{
LinkedInputPoseRow->CustomWidget()
.NameContent()
[
SNew(SBox)
.Padding(2.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("InputPose", "Input Pose"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
]
.ValueContent()
.MaxDesiredWidth(250.0f)
[
SNew(SBox)
.Padding(2.0f)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.FillWidth(1.0f)
[
SNew(STextBlock)
.Text_Lambda(GetLinkedInputPoseLabel)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
+SHorizontalBox::Slot()
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
.HAlign(HAlign_Right)
.AutoWidth()
[
SNew(SButton)
.ButtonStyle(FAppStyle::Get(), "HoverHintOnly")
.ForegroundColor(FAppStyle::GetSlateColor("DefaultForeground"))
.ContentPadding(FMargin(2, 2))
.OnClicked(this, &FAnimGraphDetails::OnRemoveInputPoseClicked, LinkedInputPoseNode)
.ToolTipText(LOCTEXT("RemoveInputPoseTooltip", "Remove this input pose"))
[
SNew(SImage)
.Image(FAppStyle::GetBrush("Cross"))
]
]
]
];
}
}
}
else
{
// Add a text widget to let the user know to hit the + icon to add parameters.
InputsCategory.AddCustomRow(FText::GetEmpty())
.WholeRowContent()
.MaxDesiredWidth(980.f)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
.AutoWidth()
[
SNew(STextBlock)
.Text(LOCTEXT("NoInputPosesAddedForAnimGraph", "Please press the + icon above to add input poses"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
];
}
if(IsInterface())
{
UAnimationGraphSchema::AutoArrangeInterfaceGraph(*Graph);
}
}
FReply FAnimGraphDetails::OnAddNewInputPoseClicked()
{
EK2NewNodeFlags NewNodeOperation = EK2NewNodeFlags::None;
FVector2D NewNodePosition(0.0f, 0.0f);
if(!IsInterface())
{
NewNodePosition = UAnimationGraphSchema::GetPositionForNewLinkedInputPoseNode(*Graph);
}
FEdGraphSchemaAction_K2NewNode::SpawnNode<UAnimGraphNode_LinkedInputPose>(Graph, NewNodePosition, EK2NewNodeFlags::None);
TSharedPtr<IAnimationBlueprintEditor> AnimBlueprintEditor = AnimBlueprintEditorPtr.Pin();
UBlueprint* Blueprint = AnimBlueprintEditor->GetBlueprintObj();
UAnimGraphNode_LinkedInputPose::ReconstructLayerNodes(Blueprint);
DetailLayoutBuilder->ForceRefreshDetails();
return FReply::Handled();
}
EVisibility FAnimGraphDetails::OnGetNewInputPoseTextVisibility(TWeakPtr<SWidget> WeakInputsHeaderWidget) const
{
return WeakInputsHeaderWidget.Pin()->IsHovered() ? EVisibility::Visible : EVisibility::Collapsed;
}
FReply FAnimGraphDetails::OnRemoveInputPoseClicked(UAnimGraphNode_LinkedInputPose* InLinkedInputPose)
{
{
FScopedTransaction Transaction(LOCTEXT("RemoveInputPose", "Remove Linked Input Pose"));
Graph->RemoveNode(InLinkedInputPose);
}
TSharedPtr<IAnimationBlueprintEditor> AnimBlueprintEditor = AnimBlueprintEditorPtr.Pin();
UBlueprint* Blueprint = AnimBlueprintEditor->GetBlueprintObj();
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
UAnimGraphNode_LinkedInputPose::ReconstructLayerNodes(Blueprint);
DetailLayoutBuilder->ForceRefreshDetails();
return FReply::Handled();
}
FText FAnimGraphDetails::OnGetGroupText() const
{
UAnimGraphNode_Root* Root = FBlueprintEditorUtils::GetAnimGraphRoot(Graph);
if (Root->Node.GetGroup() == FAnimNode_Root::DefaultSharedGroup)
{
return LOCTEXT("DefaultGroupSharedGroup", "Default Shared Group");
}
else if(Root->Node.GetGroup() == NAME_None)
{
return LOCTEXT("DefaultGroupUngrouped", "Ungrouped");
}
return FText::FromName(Root->Node.GetGroup());
}
void FAnimGraphDetails::OnGroupTextCommitted(const FText& NewText, ETextCommit::Type InTextCommit)
{
if (InTextCommit == ETextCommit::OnEnter || InTextCommit == ETextCommit::OnUserMovedFocus)
{
// Remove excess whitespace and prevent categories with just spaces
FText GroupName = FText::TrimPrecedingAndTrailing(NewText);
if(GroupName.ToString().Equals(TEXT("Ungrouped")))
{
GroupName = FText::GetEmpty();
}
FBlueprintEditorUtils::SetAnimationGraphLayerGroup(Graph, GroupName);
AnimBlueprintEditorPtr.Pin()->RefreshMyBlueprint();
RefreshGroupSource();
}
}
void FAnimGraphDetails::OnGroupSelectionChanged(TSharedPtr<FText> ProposedSelection, ESelectInfo::Type SelectInfo)
{
if(ProposedSelection.IsValid())
{
FText GroupName = *ProposedSelection.Get();
if(GroupName.ToString().Equals(TEXT("Ungrouped")))
{
GroupName = FText::GetEmpty();
}
FBlueprintEditorUtils::SetAnimationGraphLayerGroup(Graph, GroupName);
AnimBlueprintEditorPtr.Pin()->RefreshMyBlueprint();
GroupListView.Pin()->ClearSelection();
GroupComboButton.Pin()->SetIsOpen(false);
RefreshGroupSource();
}
}
TSharedRef< ITableRow > FAnimGraphDetails::MakeGroupViewWidget( TSharedPtr<FText> Item, const TSharedRef< STableViewBase >& OwnerTable )
{
return
SNew(STableRow<TSharedPtr<FString>>, OwnerTable)
[
SNew(STextBlock)
.Text(*Item.Get())
];
}
bool FAnimGraphDetails::IsInterface() const
{
TSharedPtr<IAnimationBlueprintEditor> AnimBlueprintEditor = AnimBlueprintEditorPtr.Pin();
return AnimBlueprintEditor->GetBlueprintObj()->BlueprintType == BPTYPE_Interface;
}
void FAnimGraphDetails::RefreshGroupSource()
{
TSharedPtr<IAnimationBlueprintEditor> AnimBlueprintEditor = AnimBlueprintEditorPtr.Pin();
UClass* Class = AnimBlueprintEditor->GetBlueprintObj()->SkeletonGeneratedClass;
GroupSource.Empty();
UAnimGraphNode_Root* Root = FBlueprintEditorUtils::GetAnimGraphRoot(Graph);
if(Root->Node.GetGroup() != NAME_None)
{
GroupSource.Add(MakeShared<FText>(LOCTEXT("DefaultGroupUngrouped", "Ungrouped")));
}
// Pull groups from implemented functions
for (TFieldIterator<UFunction> FunctionIt(Class, EFieldIteratorFlags::IncludeSuper); FunctionIt; ++FunctionIt)
{
const UFunction* Function = *FunctionIt;
if(Function->HasMetaData(FBlueprintMetadata::MD_AnimBlueprintFunction))
{
FText Group = FObjectEditorUtils::GetCategoryText(Function);
if (!Group.IsEmpty())
{
bool bNewCategory = true;
for (int32 GroupIndex = 0; GroupIndex < GroupSource.Num() && bNewCategory; ++GroupIndex)
{
bNewCategory &= !GroupSource[GroupIndex].Get()->EqualTo(Group);
}
if (bNewCategory)
{
GroupSource.Add(MakeShared<FText>(Group));
}
}
}
}
if(GroupListView.IsValid())
{
GroupListView.Pin()->RequestListRefresh();
}
}
#undef LOCTEXT_NAMESPACE