// Copyright Epic Games, Inc. All Rights Reserved. #include "AnimTransitionNodeDetails.h" #include "AnimGraphNode_TransitionResult.h" #include "AnimStateConduitNode.h" #include "AnimStateNodeBase.h" #include "AnimStateTransitionNode.h" #include "Animation/AnimInstance.h" #include "Animation/AnimStateMachineTypes.h" #include "AnimationTransitionGraph.h" #include "Containers/Array.h" #include "Containers/EnumAsByte.h" #include "Containers/Map.h" #include "Delegates/Delegate.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" #include "Engine/Blueprint.h" #include "Fonts/SlateFontInfo.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/PlatformCrt.h" #include "HAL/PlatformMath.h" #include "IDetailPropertyRow.h" #include "Internationalization/Internationalization.h" #include "Internationalization/Text.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "Layout/Margin.h" #include "Layout/WidgetPath.h" #include "Math/Color.h" #include "Misc/AssertionMacros.h" #include "PropertyEditorModule.h" #include "PropertyHandle.h" #include "SKismetLinearExpression.h" #include "SlateOptMacros.h" #include "SlotBase.h" #include "Styling/AppStyle.h" #include "Templates/Casts.h" #include "Templates/SubclassOf.h" #include "Textures/SlateIcon.h" #include "UObject/NameTypes.h" #include "UObject/Object.h" #include "UObject/ObjectPtr.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/STextEntryPopup.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SWindow.h" #include "Widgets/Text/STextBlock.h" class SWidget; #define LOCTEXT_NAMESPACE "FAnimStateNodeDetails" ///////////////////////////////////////////////////////////////////////// TSharedRef FAnimTransitionNodeDetails::MakeInstance() { return MakeShareable( new FAnimTransitionNodeDetails ); } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void FAnimTransitionNodeDetails::CustomizeDetails( IDetailLayoutBuilder& DetailBuilder ) { // Get a handle to the node we're viewing const TArray< TWeakObjectPtr >& SelectedObjects = DetailBuilder.GetSelectedObjects(); bool bTransitionToConduit = false; for (int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex) { const TWeakObjectPtr& CurrentObject = SelectedObjects[ObjectIndex]; if (CurrentObject.IsValid()) { if (UAnimStateTransitionNode* TransitionNodePtr = Cast(CurrentObject.Get())) { if(!TransitionNode.IsValid()) { TransitionNode = TransitionNodePtr; } UAnimStateNodeBase* NextState = TransitionNodePtr->GetNextState(); if((NextState != NULL) && (NextState->IsA())) { bTransitionToConduit = true; } } } } UAnimStateTransitionNode* TransNode = TransitionNode.Get(); IDetailCategoryBuilder& TransitionCategory = DetailBuilder.EditCategory("Transition", LOCTEXT("TransitionCategoryTitle", "Transition") ); if (bTransitionToConduit) { // Transitions to conduits are just shorthand for some other real transition; // All of the blend related settings are ignored, so hide them. DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, Bidirectional)); DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, CrossfadeDuration)); DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, BlendMode)); DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, CustomBlendCurve)); DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, LogicType)); DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, PriorityOrder)); DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, BlendProfileWrapper)); } else { TransitionCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, PriorityOrder)).DisplayName(LOCTEXT("PriorityOrderLabel", "Priority Order")); TransitionCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, Bidirectional)).DisplayName(LOCTEXT("BidirectionalLabel", "Bidirectional")); TSharedPtr LogicTypeHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, LogicType)); TransitionCategory.AddProperty(LogicTypeHandle) .DisplayName(LOCTEXT("BlendLogicLabel", "Blend Logic") ) .CustomWidget() .NameContent() [ LogicTypeHandle->CreatePropertyNameWidget() ] .ValueContent() .MaxDesiredWidth(300.0f) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ LogicTypeHandle->CreatePropertyValueWidget() ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) .FillWidth(1.0f) .Padding(3.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .OnClicked(this, &FAnimTransitionNodeDetails::OnClickEditBlendGraph) .Visibility( this, &FAnimTransitionNodeDetails::GetBlendGraphButtonVisibility, SelectedObjects.Num() > 1) .Text(LOCTEXT("EditBlendGraph", "Edit Blend Graph")) .TextStyle(&FAppStyle::Get(), TEXT("TinyText")) ] ]; if (TransitionNode != NULL && SelectedObjects.Num() == 1) { // The sharing option for the rule TransitionCategory.AddCustomRow( LOCTEXT("TransitionRuleSharingLabel", "Transition Rule Sharing") ) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("TransitionRuleSharingLabel", "Transition Rule Sharing")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() .MaxDesiredWidth(300.0f) [ GetWidgetForInlineShareMenu( TAttribute::Create(TAttribute::FGetter::CreateLambda([TransNode]() { return FText::FromString(TransNode->SharedRulesName); })), TAttribute::Create(TAttribute::FGetter::CreateLambda([TransNode]() { return TransNode->bSharedRules; })), FOnClicked::CreateSP(this, &FAnimTransitionNodeDetails::OnPromoteToSharedClick, true), FOnClicked::CreateSP(this, &FAnimTransitionNodeDetails::OnUnshareClick, true), FOnGetContent::CreateSP(this, &FAnimTransitionNodeDetails::OnGetShareableNodesMenu, true)) ]; // Show the rule itself UEdGraphPin* CanExecPin = NULL; if (UAnimationTransitionGraph* TransGraph = Cast(TransNode->BoundGraph)) { if (UAnimGraphNode_TransitionResult* ResultNode = TransGraph->GetResultNode()) { CanExecPin = ResultNode->FindPin(TEXT("bCanEnterTransition")); } } if (TransNode->bAutomaticRuleBasedOnSequencePlayerInState) { if (CanExecPin != nullptr && CanExecPin->LinkedTo.Num() > 0) { TransitionCategory.AddCustomRow(LOCTEXT("AnimGraphNodeDetailsAutomaticRule_RowWarning", "Automatic Rule")) [ SNew(SBox) .VAlign(VAlign_Center) .HAlign(HAlign_Left) [ SNew(STextBlock) .Text(LOCTEXT("AnimGraphNodeDetailsAutomaticRule_Warning", "Warning : Automatic Rule Based Transition will override graph exit rule.")) .ColorAndOpacity(FCoreStyle::Get().GetColor("ErrorReporting.WarningBackgroundColor")) .Font(IDetailLayoutBuilder::GetDetailFontBold()) ] ]; } else { TransitionCategory.AddCustomRow(LOCTEXT("AnimGraphNodeDetailsAutomaticRule_Row", "Automatic Rule")) [ SNew(SBox) .VAlign(VAlign_Center) .HAlign(HAlign_Left) [ SNew(STextBlock) .Text(LOCTEXT("AnimGraphNodeDetailsAutomaticRule", "Automatic Rule Based Transition")) .Font(IDetailLayoutBuilder::GetDetailFontBold()) ] ]; } } else { // indicate if a native transition rule applies to this UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForNodeChecked(TransitionNode.Get()); if(Blueprint && Blueprint->ParentClass) { UAnimInstance* AnimInstance = CastChecked(Blueprint->ParentClass->GetDefaultObject()); if(AnimInstance) { UEdGraph* ParentGraph = TransitionNode->GetGraph(); UAnimStateNodeBase* PrevState = TransitionNode->GetPreviousState(); UAnimStateNodeBase* NextState = TransitionNode->GetNextState(); if(PrevState != nullptr && NextState != nullptr && ParentGraph != nullptr) { FName FunctionName; if(AnimInstance->HasNativeTransitionBinding(ParentGraph->GetFName(), FName(*PrevState->GetStateName()), FName(*NextState->GetStateName()), FunctionName)) { TransitionCategory.AddCustomRow( LOCTEXT("NativeBindingPresent_Filter", "Transition has native binding") ) [ SNew(STextBlock) .Text(FText::Format(LOCTEXT("NativeBindingPresent", "Transition has native binding to {0}()"), FText::FromName(FunctionName))) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) ]; } } } } TransitionCategory.AddCustomRow( CanExecPin ? CanExecPin->PinFriendlyName : FText::GetEmpty() ) [ SNew(SKismetLinearExpression, CanExecPin) ]; } } ////////////////////////////////////////////////////////////////////////// IDetailCategoryBuilder& CrossfadeCategory = DetailBuilder.EditCategory("BlendSettings", LOCTEXT("BlendSettingsCategoryTitle", "Blend Settings") ); if (TransitionNode != NULL && SelectedObjects.Num() == 1) { // The sharing option for the crossfade settings CrossfadeCategory.AddCustomRow( LOCTEXT("TransitionCrossfadeSharingLabel", "Transition Crossfade Sharing") ) .NameContent() [ SNew(STextBlock) .Text(LOCTEXT("TransitionCrossfadeSharingLabel", "Transition Crossfade Sharing")) .Font(IDetailLayoutBuilder::GetDetailFont()) ] .ValueContent() .MaxDesiredWidth(300.0f) [ GetWidgetForInlineShareMenu( TAttribute::Create(TAttribute::FGetter::CreateLambda([TransNode]() { return FText::FromString(TransNode->SharedCrossfadeName); })), TAttribute::Create(TAttribute::FGetter::CreateLambda([TransNode]() { return TransNode->bSharedCrossfade; })), FOnClicked::CreateSP(this, &FAnimTransitionNodeDetails::OnPromoteToSharedClick, false), FOnClicked::CreateSP(this, &FAnimTransitionNodeDetails::OnUnshareClick, false), FOnGetContent::CreateSP(this, &FAnimTransitionNodeDetails::OnGetShareableNodesMenu, false)) ]; } //@TODO: Gate editing these on shared non-authoritative ones CrossfadeCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, CrossfadeDuration)).DisplayName( LOCTEXT("DurationLabel", "Duration") ); CrossfadeCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, BlendMode)).DisplayName(LOCTEXT("ModeLabel", "Mode")); CrossfadeCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, CustomBlendCurve)).DisplayName(LOCTEXT("CurveLabel", "Custom Blend Curve")); CrossfadeCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, BlendProfileWrapper)).DisplayName(LOCTEXT("BlendProfileLabel", "Blend Profile")); ////////////////////////////////////////////////////////////////////////// IDetailCategoryBuilder& NotificationCategory = DetailBuilder.EditCategory("Notifications", LOCTEXT("NotificationsCategoryTitle", "Notifications") ); NotificationCategory.AddCustomRow( LOCTEXT("StartTransitionEventPropertiesCategoryLabel", "Start Transition Event") ) [ SNew( STextBlock ) .Text( LOCTEXT("StartTransitionEventPropertiesCategoryLabel", "Start Transition Event") ) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) ]; CreateTransitionEventPropertyWidgets(NotificationCategory, TEXT("TransitionStart")); NotificationCategory.AddCustomRow( LOCTEXT("EndTransitionEventPropertiesCategoryLabel", "End Transition Event" ) ) [ SNew( STextBlock ) .Text( LOCTEXT("EndTransitionEventPropertiesCategoryLabel", "End Transition Event" ) ) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) ]; CreateTransitionEventPropertyWidgets(NotificationCategory, TEXT("TransitionEnd")); NotificationCategory.AddCustomRow( LOCTEXT("InterruptTransitionEventPropertiesCategoryLabel", "Interrupt Transition Event") ) [ SNew( STextBlock ) .Text( LOCTEXT("InterruptTransitionEventPropertiesCategoryLabel", "Interrupt Transition Event") ) .Font( IDetailLayoutBuilder::GetDetailFontBold() ) ]; CreateTransitionEventPropertyWidgets(NotificationCategory, TEXT("TransitionInterrupt")); } DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, TransitionStart)); DetailBuilder.HideProperty(GET_MEMBER_NAME_CHECKED(UAnimStateTransitionNode, TransitionEnd)); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION /** RuleShare = true if we are sharing the rules of this transition (else we are implied to be sharing the crossfade settings) */ FReply FAnimTransitionNodeDetails::OnPromoteToSharedClick(bool RuleShare) { TSharedPtr< SWindow > Parent = FSlateApplication::Get().GetActiveTopLevelWindow(); if ( Parent.IsValid() ) { // Show dialog to enter new track name TSharedRef TextEntry = SNew(STextEntryPopup) .Label( LOCTEXT("PromoteAnimTransitionNodeToSharedLabel", "Shared Transition Name") ) .OnTextCommitted(this, &FAnimTransitionNodeDetails::PromoteToShared, RuleShare); // Show dialog to enter new event name FSlateApplication::Get().PushMenu( Parent.ToSharedRef(), FWidgetPath(), TextEntry, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect( FPopupTransitionEffect::TypeInPopup ) ); TextEntryWidget = TextEntry; } return FReply::Handled(); } void FAnimTransitionNodeDetails::PromoteToShared(const FText& NewTransitionName, ETextCommit::Type CommitInfo, bool bRuleShare) { if (CommitInfo == ETextCommit::OnEnter) { if (UAnimStateTransitionNode* TransNode = TransitionNode.Get()) { if (bRuleShare) { TransNode->MakeRulesShareable(NewTransitionName.ToString()); AssignUniqueColorsToAllSharedNodes(TransNode->GetGraph()); } else { TransNode->MakeCrossfadeShareable(NewTransitionName.ToString()); } } } FSlateApplication::Get().DismissAllMenus(); } FReply FAnimTransitionNodeDetails::OnUnshareClick(bool bUnshareRule) { if (UAnimStateTransitionNode* TransNode = TransitionNode.Get()) { if (bUnshareRule) { TransNode->UnshareRules(); } else { TransNode->UnshareCrossade(); } } return FReply::Handled(); } TSharedRef FAnimTransitionNodeDetails::OnGetShareableNodesMenu(bool bShareRules) { FMenuBuilder MenuBuilder(true, NULL); FText SectionText; if (bShareRules) { SectionText = LOCTEXT("PickSharedAnimTransition", "Shared Transition Rules"); } else { SectionText = LOCTEXT("PickSharedAnimCrossfadeSettings", "Shared Settings"); } MenuBuilder.BeginSection("AnimTransitionSharableNodes", SectionText); if (UAnimStateTransitionNode* RawTransitionNode = TransitionNode.Get()) { const UEdGraph* CurrentGraph = RawTransitionNode->GetGraph(); // Collect all unique shared transitions and group them by their name. TMultiMap SharedTransitions; if (UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(CurrentGraph)) { TArray StateNodes; FBlueprintEditorUtils::GetAllNodesOfClassEx(Blueprint, StateNodes); for (UAnimStateNodeBase* StateNodeBase : StateNodes) { if (UAnimStateTransitionNode* GraphTransNode = Cast(StateNodeBase)) { if (bShareRules && !GraphTransNode->SharedRulesName.IsEmpty()) { SharedTransitions.Add(GraphTransNode->SharedRulesName, GraphTransNode); } if (!bShareRules && !GraphTransNode->SharedCrossfadeName.IsEmpty()) { SharedTransitions.Add(GraphTransNode->SharedCrossfadeName, GraphTransNode); } } } } // Get the unique shared transition names TSet SharedTransitionKeys; SharedTransitions.GetKeys(SharedTransitionKeys); // Iterate through the unique shared transition names and list all the places where they are referenced in the tooltip. TArray UsedIn; for (const FString& Key : SharedTransitionKeys) { UsedIn.Reset(); SharedTransitions.MultiFind(Key, UsedIn, /*bMaintainOrder=*/true); if (UsedIn.IsEmpty()) { continue; } FTextBuilder ToolTipBuilder; ToolTipBuilder.AppendLine(LOCTEXT("AnimTransitionUsedBy", "Used by:")); for (const UAnimStateTransitionNode* UsedInTransitionNode : UsedIn) { ToolTipBuilder.AppendLine(UsedInTransitionNode->GetGraph()->GetName()); } FUIAction Action = FUIAction( FExecuteAction::CreateSP(this, &FAnimTransitionNodeDetails::BecomeSharedWith, UsedIn[0], bShareRules)); MenuBuilder.AddMenuEntry( FText::FromString(Key), ToolTipBuilder.ToText(), FSlateIcon(), Action); } } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void FAnimTransitionNodeDetails::BecomeSharedWith(UAnimStateTransitionNode* NewNode, bool bShareRules) { if (UAnimStateTransitionNode* TransNode = TransitionNode.Get()) { if (bShareRules) { TransNode->UseSharedRules(NewNode); } else { TransNode->UseSharedCrossfade(NewNode); } } } void FAnimTransitionNodeDetails::AssignUniqueColorsToAllSharedNodes(UEdGraph* CurrentGraph) { TArray SourceList; for (int32 idx=0; idx < CurrentGraph->Nodes.Num(); idx++) { if (UAnimStateTransitionNode* Node = Cast(CurrentGraph->Nodes[idx])) { if (Node->bSharedRules) { int32 colorIdx = SourceList.AddUnique(Node->BoundGraph)+1; FLinearColor SharedColor; SharedColor.R = (colorIdx & 1 ? 1.0f : 0.15f); SharedColor.G = (colorIdx & 2 ? 1.0f : 0.15f); SharedColor.B = (colorIdx & 4 ? 1.0f : 0.15f); SharedColor.A = 1.0f; // Storing this on the UAnimStateTransitionNode really bugs me. But its a pain to iterate over all the widget nodes at once // and we may want the shared color to be customizable in the details view Node->SharedColor = SharedColor; } } } } FReply FAnimTransitionNodeDetails::OnClickEditBlendGraph() { if (UAnimStateTransitionNode* TransitionNodePtr = TransitionNode.Get()) { if (TransitionNodePtr->CustomTransitionGraph != NULL) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(TransitionNodePtr->CustomTransitionGraph); } } return FReply::Handled(); } EVisibility FAnimTransitionNodeDetails::GetBlendGraphButtonVisibility(bool bMultiSelect) const { if(!bMultiSelect) { if (UAnimStateTransitionNode* TransitionNodePtr = TransitionNode.Get()) { if (TransitionNodePtr->LogicType == ETransitionLogicType::TLT_Custom) { return EVisibility::Visible; } } } return EVisibility::Hidden; } void FAnimTransitionNodeDetails::CreateTransitionEventPropertyWidgets(IDetailCategoryBuilder& TransitionCategory, FString TransitionName) { TSharedPtr NameProperty = TransitionCategory.GetParentLayout().GetProperty(*(TransitionName + TEXT(".NotifyName"))); TransitionCategory.AddProperty( NameProperty ) .DisplayName( LOCTEXT("CreateTransition_CustomBlueprintEvent", "Custom Blueprint Event") ); } TSharedRef FAnimTransitionNodeDetails::GetWidgetForInlineShareMenu(const TAttribute& InSharedNameText, const TAttribute& bInIsCurrentlyShared, FOnClicked PromoteClick, FOnClicked DemoteClick, FOnGetContent GetContentMenu) { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew( SComboButton ) .ContentPadding(FMargin(4.0f, 2.0f)) .ToolTipText(LOCTEXT("UseSharedAnimationTransition_ToolTip", "Use Shared Transition")) .OnGetMenuContent( GetContentMenu ) .ButtonContent() [ SNew(STextBlock) .Text_Lambda( [bInIsCurrentlyShared, InSharedNameText]() { return bInIsCurrentlyShared.Get() ? InSharedNameText.Get() : LOCTEXT("SharedTransition", "Use Shared"); } ) .Font( IDetailLayoutBuilder::GetDetailFont() ) ] ] +SHorizontalBox::Slot() .AutoWidth() .Padding(3.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .ContentPadding(FMargin(4.0f, 2.0f)) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .OnClicked_Lambda([bInIsCurrentlyShared, DemoteClick, PromoteClick]() { return bInIsCurrentlyShared.Get() ? DemoteClick.Execute() : PromoteClick.Execute(); } ) .Text_Lambda([bInIsCurrentlyShared](){ return bInIsCurrentlyShared.Get() ? LOCTEXT("UnshareLabel", "Unshare") : LOCTEXT("ShareLabel", "Promote To Shared"); } ) .TextStyle(&FAppStyle::Get(), TEXT("TinyText")) ]; } void FAnimTransitionNodeDetails::OnBlendProfileChanged(UBlendProfile* NewProfile, TSharedPtr ProfileProperty) { if(ProfileProperty.IsValid()) { ProfileProperty->SetValue((const UObject*&)NewProfile); } } #undef LOCTEXT_NAMESPACE