// Copyright Epic Games, Inc. All Rights Reserved. #include "AnimationNodes/SAnimationGraphNode.h" #include "Widgets/SBoxPanel.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "AnimGraphNode_Base.h" #include "IDocumentation.h" #include "AnimationEditorUtils.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Animation/AnimInstance.h" #include "GraphEditorSettings.h" #include "SLevelOfDetailBranchNode.h" #include "Widgets/Layout/SSpacer.h" #include "AnimationGraphSchema.h" #include "AnimGraphNode_CustomProperty.h" #include "BlueprintMemberReferenceCustomization.h" #include "SGraphPin.h" #include "Widgets/Layout/SWrapBox.h" #include "Brushes/SlateColorBrush.h" #include "PropertyEditorModule.h" #include "IPropertyRowGenerator.h" #include "IDetailTreeNode.h" #include "Widgets/Layout/SGridPanel.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "SGraphPanel.h" #include "SPoseWatchOverlay.h" #define LOCTEXT_NAMESPACE "AnimationGraphNode" void SAnimationGraphNode::Construct(const FArguments& InArgs, UAnimGraphNode_Base* InNode) { this->GraphNode = InNode; this->SetCursor(EMouseCursor::CardinalCross); this->UpdateGraphNode(); ReconfigurePinWidgetsForPropertyBindings(CastChecked(GraphNode), SharedThis(this), [this](UEdGraphPin* InPin){ return FindWidgetForPin(InPin); }); const FSlateBrush* ImageBrush = FAppStyle::Get().GetBrush(TEXT("Graph.AnimationFastPathIndicator")); IndicatorWidget = SNew(SImage) .Image(ImageBrush) .ToolTip(IDocumentation::Get()->CreateToolTip(LOCTEXT("AnimGraphNodeIndicatorTooltip", "Fast path enabled: This node is not using any Blueprint calls to update its data."), NULL, TEXT("Shared/GraphNodes/Animation"), TEXT("GraphNode_FastPathInfo"))) .Visibility(EVisibility::Visible); PoseViewWidget = SNew(SPoseWatchOverlay, InNode); LastHighDetailSize = FVector2D::ZeroVector; } void SAnimationGraphNode::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { SGraphNodeK2Base::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); if (CachedContentArea != nullptr && !UseLowDetailNodeContent()) { LastHighDetailSize = CachedContentArea->GetTickSpaceGeometry().Size; } } TArray SAnimationGraphNode::GetOverlayWidgets(bool bSelected, const FVector2D& WidgetSize) const { TArray Widgets; if (UAnimGraphNode_Base* AnimNode = CastChecked(GraphNode, ECastCheckedType::NullAllowed)) { if (AnimNode->BlueprintUsage == EBlueprintUsage::DoesNotUseBlueprint) { const FSlateBrush* ImageBrush = FAppStyle::Get().GetBrush(TEXT("Graph.AnimationFastPathIndicator")); FOverlayWidgetInfo Info; Info.OverlayOffset = FVector2D(WidgetSize.X - (ImageBrush->ImageSize.X * 0.5f), -(ImageBrush->ImageSize.Y * 0.5f)); Info.Widget = IndicatorWidget; Widgets.Add(Info); } if (PoseViewWidget->IsPoseWatchValid()) { FOverlayWidgetInfo Info; Info.OverlayOffset = PoseViewWidget->GetOverlayOffset(); Info.Widget = PoseViewWidget; Widgets.Add(Info); } } return Widgets; } TSharedRef SAnimationGraphNode::CreateTitleWidget(TSharedPtr InNodeTitle) { // Store title widget reference NodeTitle = InNodeTitle; // hook up invalidation delegate UAnimGraphNode_Base* AnimGraphNode = CastChecked(GraphNode); AnimGraphNode->OnNodeTitleChangedEvent().AddSP(this, &SAnimationGraphNode::HandleNodeTitleChanged); return SGraphNodeK2Base::CreateTitleWidget(InNodeTitle); } void SAnimationGraphNode::HandleNodeTitleChanged() { if(NodeTitle.IsValid()) { NodeTitle->MarkDirty(); } } void SAnimationGraphNode::GetNodeInfoPopups(FNodeInfoContext* Context, TArray& Popups) const { SGraphNodeK2Base::GetNodeInfoPopups(Context, Popups); UAnimBlueprint* AnimBlueprint = Cast(FBlueprintEditorUtils::FindBlueprintForNode(GraphNode)); if(AnimBlueprint) { UAnimInstance* ActiveObject = Cast(AnimBlueprint->GetObjectBeingDebugged()); UAnimBlueprintGeneratedClass* Class = AnimBlueprint->GetAnimBlueprintGeneratedClass(); const FLinearColor Color(1.f, 0.5f, 0.25f); // Display various types of debug data if ((ActiveObject != NULL) && (Class != NULL)) { if (Class->GetAnimNodeProperties().Num()) { if(int32* NodeIndexPtr = Class->GetAnimBlueprintDebugData().NodePropertyToIndexMap.Find(TWeakObjectPtr(Cast(GraphNode)))) { int32 AnimNodeIndex = *NodeIndexPtr; // reverse node index temporarily because of a bug in NodeGuidToIndexMap AnimNodeIndex = Class->GetAnimNodeProperties().Num() - AnimNodeIndex - 1; if (FAnimBlueprintDebugData::FNodeValue* DebugInfo = Class->GetAnimBlueprintDebugData().NodeValuesThisFrame.FindByPredicate([AnimNodeIndex](const FAnimBlueprintDebugData::FNodeValue& InValue){ return InValue.NodeID == AnimNodeIndex; })) { Popups.Emplace(nullptr, Color, DebugInfo->Text); } } } } } } void SAnimationGraphNode::CreateBelowPinControls(TSharedPtr MainBox) { if (UAnimGraphNode_Base* AnimNode = CastChecked(GraphNode, ECastCheckedType::NullAllowed)) { auto UseLowDetailNode = [this]() { return GetCurrentLOD() <= EGraphRenderingLOD::LowDetail; }; // Insert above the error reporting bar MainBox->InsertSlot(FMath::Max(0, MainBox->NumSlots() - TagAndFunctionsSlotReverseIndex)) .AutoHeight() .Padding(4.0f, 2.0f, 4.0f, 2.0f) [ SNew(SVerticalBox) .IsEnabled_Lambda([this](){ return IsNodeEditable(); }) +SVerticalBox::Slot() .AutoHeight() [ CreateNodeFunctionsWidget(AnimNode, MakeAttributeLambda(UseLowDetailNode)) ] ]; MainBox->InsertSlot(FMath::Max(0, MainBox->NumSlots() - TagAndFunctionsSlotReverseIndex)) .AutoHeight() .Padding(4.0f, 2.0f, 4.0f, 2.0f) [ SNew(SVerticalBox) .IsEnabled_Lambda([this](){ return IsNodeEditable(); }) +SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Right) [ CreateNodeTagWidget(AnimNode, MakeAttributeLambda(UseLowDetailNode)) ] ]; } } TSharedRef SAnimationGraphNode::CreateNodeContentArea() { CachedContentArea = SGraphNodeK2Base::CreateNodeContentArea(); return SNew(SLevelOfDetailBranchNode) .UseLowDetailSlot(this, &SAnimationGraphNode::UseLowDetailNodeContent) .LowDetail() [ SNew(SSpacer) .Size(this, &SAnimationGraphNode::GetLowDetailDesiredSize) ] .HighDetail() [ CachedContentArea.ToSharedRef() ]; } bool SAnimationGraphNode::UseLowDetailNodeContent() const { if (LastHighDetailSize.IsNearlyZero()) { return false; } if (const SGraphPanel* MyOwnerPanel = GetOwnerPanel().Get()) { return (MyOwnerPanel->GetCurrentLOD() <= EGraphRenderingLOD::LowestDetail); } return false; } FVector2D SAnimationGraphNode::GetLowDetailDesiredSize() const { return LastHighDetailSize; } // Widget used to allow functions to be viewed and edited on nodes class SAnimNodeFunctionsWidget : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SAnimNodeFunctionsWidget) {} SLATE_ATTRIBUTE(bool, UseLowDetail) SLATE_END_ARGS() void Construct(const FArguments& InArgs, UAnimGraphNode_Base* InNode) { UseLowDetail = InArgs._UseLowDetail; FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().LoadModuleChecked("PropertyEditor"); PropertyRowGenerator = PropertyEditorModule.CreatePropertyRowGenerator(FPropertyRowGeneratorArgs()); PropertyRowGenerator->RegisterInstancedCustomPropertyTypeLayout(FMemberReference::StaticStruct()->GetFName(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FBlueprintMemberReferenceDetails::MakeInstance)); PropertyRowGenerator->SetObjects({ InNode }); TSharedPtr GridPanel; ChildSlot [ SAssignNew(GridPanel, SGridPanel) ]; GridPanel->SetVisibility(EVisibility::Collapsed); int32 RowIndex = 0; // Add bound functions auto AddFunctionBindingWidget = [this, InNode, &GridPanel, &RowIndex](FName InCategory, FName InMemberName) { GridPanel->SetVisibility(EVisibility::Visible); // Find row TSharedPtr PropertyHandle; TSharedPtr DetailTreeNode; for (const TSharedRef& RootTreeNode : PropertyRowGenerator->GetRootTreeNodes()) { if(RootTreeNode->GetNodeName() == InCategory) { TArray> Children; RootTreeNode->GetChildren(Children); for (int32 ChildIdx = 0; ChildIdx < Children.Num(); ChildIdx++) { TSharedPtr ChildPropertyHandle = Children[ChildIdx]->CreatePropertyHandle(); if (ChildPropertyHandle.IsValid() && ChildPropertyHandle->GetProperty() && ChildPropertyHandle->GetProperty()->GetFName() == InMemberName) { DetailTreeNode = Children[ChildIdx]; PropertyHandle = ChildPropertyHandle; PropertyHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateLambda([InNode]() { InNode->ReconstructNode(); })); break; } } } } if(DetailTreeNode.IsValid() && PropertyHandle.IsValid()) { DetailNodes.Add(DetailTreeNode); FNodeWidgets NodeWidgets = DetailTreeNode->CreateNodeWidgets(); GridPanel->AddSlot(0, RowIndex) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(10.0f, 2.0f, 2.0f, 2.0f) [ SNew(SLevelOfDetailBranchNode) .UseLowDetailSlot(UseLowDetail) .LowDetail() [ SNew(SSpacer) .Size(FVector2D(24.0f, 24.f)) ] .HighDetail() [ NodeWidgets.NameWidget.ToSharedRef() ] ]; GridPanel->AddSlot(1, RowIndex) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(2.0f, 2.0f, 10.0f, 2.0f) [ SNew(SLevelOfDetailBranchNode) .UseLowDetailSlot(UseLowDetail) .LowDetail() [ SNew(SSpacer) .Size(FVector2D(24.0f, 24.f)) ] .HighDetail() [ NodeWidgets.ValueWidget.ToSharedRef() ] ]; RowIndex++; } }; if(InNode->InitialUpdateFunction.ResolveMember(InNode->GetBlueprintClassFromNode()) != nullptr) { AddFunctionBindingWidget("Functions", GET_MEMBER_NAME_CHECKED(UAnimGraphNode_Base, InitialUpdateFunction)); } if(InNode->BecomeRelevantFunction.ResolveMember(InNode->GetBlueprintClassFromNode()) != nullptr) { AddFunctionBindingWidget("Functions", GET_MEMBER_NAME_CHECKED(UAnimGraphNode_Base, BecomeRelevantFunction)); } if(InNode->UpdateFunction.ResolveMember(InNode->GetBlueprintClassFromNode()) != nullptr) { AddFunctionBindingWidget("Functions", GET_MEMBER_NAME_CHECKED(UAnimGraphNode_Base, UpdateFunction)); } if(DetailNodes.Num() == 0) { // If we didnt add a function binding, remove the row generator as we dont need it and its expensive (as it ticks) PropertyRowGenerator.Reset(); } } // Property row generator used to display function properties on nodes TSharedPtr PropertyRowGenerator; // Hold a reference to the root ptr of the details tree we use to display function properties TArray> DetailNodes; // Attribute allowing LOD TAttribute UseLowDetail; }; TSharedRef SAnimationGraphNode::CreateNodeFunctionsWidget(UAnimGraphNode_Base* InAnimNode, TAttribute InUseLowDetail) { return SNew(SAnimNodeFunctionsWidget, InAnimNode) .UseLowDetail(InUseLowDetail); } TSharedRef SAnimationGraphNode::CreateNodeTagWidget(UAnimGraphNode_Base* InAnimNode, TAttribute InUseLowDetail) { return SNew(SLevelOfDetailBranchNode) .Visibility_Lambda([InAnimNode](){ return (InAnimNode->Tag != NAME_None) ? EVisibility::Visible : EVisibility::Collapsed; }) .UseLowDetailSlot(InUseLowDetail) .LowDetail() [ SNew(SSpacer) .Size(FVector2D(24.0f, 24.f)) ] .HighDetail() [ SNew(SBox) .Padding(FMargin(4.0f, 0.0f, 4.0f, 4.0f)) [ SNew(SInlineEditableTextBlock) .ToolTipText_Lambda([InAnimNode](){ return FText::Format(LOCTEXT("TagFormat_Tooltip", "Tag: {0}\nThis node can be referenced elsewhere in this Anim Blueprint using this tag"), FText::FromName(InAnimNode->GetTag())); }) .Style(&FAppStyle::Get().GetWidgetStyle("AnimGraph.Node.Tag")) .Text_Lambda([InAnimNode](){ return FText::FromName(InAnimNode->GetTag()); }) .OnTextCommitted_Lambda([InAnimNode](const FText& InText, ETextCommit::Type InCommitType){ InAnimNode->SetTag(*InText.ToString()); }) ] ]; } void SAnimationGraphNode::ReconfigurePinWidgetsForPropertyBindings(UAnimGraphNode_Base* InAnimGraphNode, TSharedRef InGraphNodeWidget, TFunctionRef(UEdGraphPin*)> InFindWidgetForPin) { for(UEdGraphPin* Pin : InAnimGraphNode->Pins) { FEdGraphPinType PinType = Pin->PinType; if(Pin->Direction == EGPD_Input && !UAnimationGraphSchema::IsPosePin(PinType)) { TSharedPtr PinWidget = InFindWidgetForPin(Pin); if(PinWidget.IsValid()) { // Tweak padding a little to improve extended appearance PinWidget->GetLabelAndValue()->SetInnerSlotPadding(FVector2D(2.0f, 0.0f)); TAttribute bIsEnabled = MakeAttributeLambda([WeakWidget = TWeakPtr(InGraphNodeWidget)]() { return WeakWidget.IsValid() ? WeakWidget.Pin()->IsNodeEditable() : false; }); TSharedPtr PropertyBindingWidget = UAnimationGraphSchema::MakeBindingWidgetForPin({ InAnimGraphNode }, Pin->GetFName(), true, bIsEnabled); if(PropertyBindingWidget.IsValid()) { // Add binding widget PinWidget->GetLabelAndValue()->AddSlot() [ PropertyBindingWidget.ToSharedRef() ]; // Hide any value widgets when we have bindings if(PinWidget->GetValueWidget() != SNullWidget::NullWidget) { PinWidget->GetValueWidget()->SetVisibility(MakeAttributeLambda([WeakPropertyBindingWidget = TWeakPtr(PropertyBindingWidget), WeakPinWidget = TWeakPtr(PinWidget)]() { EVisibility Visibility = EVisibility::Collapsed; if (WeakPinWidget.IsValid()) { Visibility = WeakPinWidget.Pin()->GetDefaultValueVisibility(); } if(Visibility == EVisibility::Visible && WeakPropertyBindingWidget.IsValid()) { Visibility = WeakPropertyBindingWidget.Pin()->GetVisibility() == EVisibility::Visible ? EVisibility::Collapsed : EVisibility::Visible; } return Visibility; })); } } } } } } #undef LOCTEXT_NAMESPACE