// Copyright Epic Games, Inc. All Rights Reserved. #include "PoseDriverDetails.h" #include "AnimGraphNode_PoseDriver.h" #include "AnimNodes/AnimNode_PoseDriver.h" #include "Animation/AnimBlueprint.h" #include "Animation/PoseAsset.h" #include "Animation/Skeleton.h" #include "Animation/SmartName.h" #include "Containers/UnrealString.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Fonts/SlateFontInfo.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Framework/Views/ITypedTableView.h" #include "IDetailPropertyRow.h" #include "Internationalization/Internationalization.h" #include "Layout/BasicLayoutWidgetSlot.h" #include "Layout/Margin.h" #include "Layout/Visibility.h" #include "Math/Color.h" #include "Math/Rotator.h" #include "Math/UnrealMathSSE.h" #include "Math/Vector.h" #include "Math/Vector2D.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "PropertyCustomizationHelpers.h" #include "PropertyHandle.h" #include "RBF/RBFSolver.h" #include "SCurveEditor.h" #include "SSearchableComboBox.h" #include "SlotBase.h" #include "Styling/AppStyle.h" #include "Templates/Casts.h" #include "Textures/SlateIcon.h" #include "Types/SlateStructs.h" #include "UObject/Class.h" #include "UObject/Object.h" #include "UObject/ObjectMacros.h" #include "UObject/ObjectPtr.h" #include "UObject/UObjectGlobals.h" #include "UObject/UnrealType.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Input/SRotatorInputBox.h" #include "Widgets/Input/SVectorInputBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SExpandableArea.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/Layout/SWidgetSwitcher.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SHeaderRow.h" class ITableRow; class STableViewBase; class SWidget; struct FGeometry; struct FPointerEvent; #define LOCTEXT_NAMESPACE "PoseDriverDetails" static const FName ColumnId_Target("Target"); TArray< TSharedPtr > SPDD_TargetRow::DistanceMethodOptions; TArray< TSharedPtr > SPDD_TargetRow::FunctionTypeOptions; class FSoloToggleButton : public SButton { public: SLATE_BEGIN_ARGS(FSoloToggleButton) {} SLATE_EVENT(FSimpleDelegate, OnSoloStartAction) SLATE_EVENT(FSimpleDelegate, OnSoloEndAction) SLATE_ATTRIBUTE(bool, SoloState) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { OnSoloStartAction = InArgs._OnSoloStartAction; OnSoloEndAction = InArgs._OnSoloEndAction; SoloState = InArgs._SoloState; SButton::Construct( SButton::FArguments() .Text(FText::FromString(TEXT("S"))) .ButtonStyle(FAppStyle::Get(), "FlatButton.Default") .TextStyle(FAppStyle::Get(), "FlatButton.DefaultTextStyle") .ContentPadding(4.0f) .ForegroundColor(FSlateColor::UseForeground()) .OnPressed(this, &FSoloToggleButton::OnButtonPressed) .OnReleased(this, &FSoloToggleButton::OnButtonReleased) .IsFocusable(false) ); SetAppearPressed(SoloState); } FReply OnMouseButtonDoubleClick(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent) override { OnSoloStartAction.ExecuteIfBound(); return FReply::Handled(); } private: void OnButtonPressed() { OnSoloStartAction.ExecuteIfBound(); } void OnButtonReleased() { OnSoloEndAction.ExecuteIfBound(); } FSimpleDelegate OnSoloStartAction; FSimpleDelegate OnSoloEndAction; TAttribute< bool > SoloState; }; void SPDD_TargetRow::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { TargetInfoPtr = InArgs._TargetInfo; PoseDriverDetailsPtr = InArgs._PoseDriverDetails; if (DistanceMethodOptions.Num() == 0) { UEnum* Enum = FindObjectChecked(nullptr, TEXT("/Script/AnimGraphRuntime.ERBFDistanceMethod")); for (int32 i = 0; i < Enum->NumEnums() - 1; i++) { DistanceMethodOptions.Add(MakeShareable(new FString(Enum->GetDisplayNameTextByIndex(i).ToString()))); } } if (FunctionTypeOptions.Num() == 0) { UEnum* Enum = FindObjectChecked(nullptr, TEXT("/Script/AnimGraphRuntime.ERBFFunctionType")); for (int32 i = 0; i < Enum->NumEnums() - 1; i++) { FunctionTypeOptions.Add(MakeShareable(new FString(Enum->GetDisplayNameTextByIndex(i).ToString()))); } } // Register delegate so TargetInfo can trigger UI expansion TSharedPtr TargetInfo = TargetInfoPtr.Pin(); TargetInfo->ExpandTargetDelegate.AddSP(this, &SPDD_TargetRow::ExpandTargetInfo); SMultiColumnTableRow< TSharedPtr >::Construct(FSuperRowType::FArguments(), InOwnerTableView); } TSharedRef< SWidget > SPDD_TargetRow::GenerateWidgetForColumn(const FName& ColumnName) { TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); TSharedPtr TargetEntryVertBox; TSharedRef RowWidget = SNew(SBox) .Padding(2.f) [ SNew(SBorder) .Padding(0.f) .ForegroundColor(FLinearColor::White) .BorderImage(FAppStyle::GetBrush("NoBorder")) [ SAssignNew(ExpandArea, SExpandableArea) .Padding(0.f) .InitiallyCollapsed(true) .BorderBackgroundColor(FLinearColor(.6f, .6f, .6f)) .OnAreaExpansionChanged(this, &SPDD_TargetRow::OnTargetExpansionChanged) .HeaderContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(this, &SPDD_TargetRow::GetTargetTitleText) ] + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SSpacer) ] + SHorizontalBox::Slot() .Padding(0,3) .AutoWidth() [ SNew(SBox) .MinDesiredWidth(150.f) .Content() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(TAttribute::Create(TAttribute::FGetter::CreateLambda([this] { return 1.f - this->GetTargetWeight(); }))) [ SNew(SSpacer) ] + SHorizontalBox::Slot() .FillWidth(TAttribute::Create(TAttribute::FGetter::CreateLambda([this] { return this->GetTargetWeight(); }))) [ SNew(SImage) .ColorAndOpacity(this, &SPDD_TargetRow::GetWeightBarColor) .Image(FAppStyle::GetBrush("WhiteBrush")) ] ] ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(FMargin(3, 0)) [ SNew(SBox) .MinDesiredWidth(40.f) .MaxDesiredWidth(40.f) .Content() [ SNew(STextBlock) .Text(this, &SPDD_TargetRow::GetTargetWeightText) ] ] + SHorizontalBox::Slot() .AutoWidth() .Padding(FMargin(3, 0, 6, 0)) [ SNew(FSoloToggleButton) .SoloState(TAttribute::Create(TAttribute::FGetter::CreateLambda([this] { return this->IsSoloTarget(); }))) .OnSoloStartAction(FSimpleDelegate::CreateSP(this, &SPDD_TargetRow::SoloTargetStart)) .OnSoloEndAction(FSimpleDelegate::CreateSP(this, &SPDD_TargetRow::SoloTargetEnd)) .ToolTipText(LOCTEXT("SoloTarget", "Hold to solo temporarily. Doube-click to keep solo enabled.")) ] + SHorizontalBox::Slot() .AutoWidth() [ PropertyCustomizationHelpers::MakeDeleteButton(FSimpleDelegate::CreateSP(this, &SPDD_TargetRow::RemoveTarget), LOCTEXT("RemoveTarget", "Remove Target")) ] ] .BodyContent() [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SAssignNew(TargetEntryVertBox, SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(2.0f) .VAlign(VAlign_Fill) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(0, 0, 3, 0)) [ SNew(STextBlock) .Text(LOCTEXT("Scale", "Scale:")) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) .MinDesiredWidth(150.f) .Content() [ SNew(SNumericEntryBox) .MinSliderValue(0.f) .MaxSliderValue(1.f) .Value(this, &SPDD_TargetRow::GetScale) .OnValueChanged(this, &SPDD_TargetRow::SetScale) .AllowSpin(true) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(STextBlock) .Text(LOCTEXT("DrivenName","Drive:")) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SSearchableComboBox) .OptionsSource(&PoseDriverDetails->DrivenNameOptions) .OnGenerateWidget(this, &SPDD_TargetRow::MakeDrivenNameWidget) .OnSelectionChanged(this, &SPDD_TargetRow::OnDrivenNameChanged) .Content() [ SNew(STextBlock) .Text(this, &SPDD_TargetRow::GetDrivenNameText) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(STextBlock) .Text(LOCTEXT("IsHidden", "Hidden:")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(SCheckBox) .IsChecked_Lambda([this]() { return IsHidden() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged(this, &SPDD_TargetRow::OnIsHiddenChanged) .Padding(FMargin(4.0f, 0.0f)) .ToolTipText(LOCTEXT("IsHiddenToolTip", "Define if this target should be hidden from debug drawing.")) ] + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SSpacer) ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(2.0f) .VAlign(VAlign_Fill) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(STextBlock) .Text(LOCTEXT("Override", "Override:")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(SSearchableComboBox) .OptionsSource(&DistanceMethodOptions) .OnGenerateWidget(this, &SPDD_TargetRow::MakeDrivenNameWidget) .OnSelectionChanged(this, &SPDD_TargetRow::OnDistanceMethodChanged) .IsEnabled_Lambda([this]() { return IsOverrideEnabled(); }) .Content() [ SNew(STextBlock) .Text(this, &SPDD_TargetRow::GetDistanceMethodAsText) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(SSearchableComboBox) .Visibility_Lambda([this]() { return IsCustomCurveEnabled() ? EVisibility::Collapsed : EVisibility::Visible; }) .OptionsSource(&FunctionTypeOptions) .OnGenerateWidget(this, &SPDD_TargetRow::MakeDrivenNameWidget) .OnSelectionChanged(this, &SPDD_TargetRow::OnFunctionTypeChanged) .IsEnabled_Lambda([this]() { return IsOverrideEnabled(); }) .Content() [ SNew(STextBlock) .Text(this, &SPDD_TargetRow::GetFunctionTypeAsText) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(STextBlock) .Text(LOCTEXT("CustomCurve", "Curve:")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(SCheckBox) .IsChecked_Lambda([this]() { return IsCustomCurveEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged(this, &SPDD_TargetRow::OnApplyCustomCurveChanged) .Padding(FMargin(4.0f, 0.0f)) .ToolTipText(LOCTEXT("CustomCurveTooltip", "Define a custom response curve for this target.")) ] + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SSpacer) ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(2.0f) .VAlign(VAlign_Fill) [ SNew(SBox) .Visibility_Lambda([this]() { return IsCustomCurveEnabled() ? EVisibility::Visible : EVisibility::Collapsed; }) .IsEnabled_Lambda([this]() { return IsOverrideEnabled(); }) .Content() [ SAssignNew(CurveEditor, SCurveEditor) .ViewMinInput(0.f) .ViewMaxInput(1.f) .ViewMinOutput(0.f) .ViewMaxOutput(1.f) .TimelineLength(1.f) .DesiredSize(FVector2D(512, 128)) .HideUI(true) ] ] ] ] ] ]; CurveEditor->SetCurveOwner(this); // Find number of bones we are reading, which gives the number of entry boxes we need int32 NumSourceBones = PoseDriverDetails->GetFirstSelectedPoseDriver()->Node.SourceBones.Num(); for (int32 BoneIndex = 0; BoneIndex < NumSourceBones; BoneIndex++) { TargetEntryVertBox->AddSlot() .AutoHeight() .Padding(2.0f) .VAlign(VAlign_Fill) [ SNew(SBox) .MaxDesiredWidth(800.f) .Content() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SWidgetSwitcher) .WidgetIndex(this, &SPDD_TargetRow::GetTransRotWidgetIndex) +SWidgetSwitcher::Slot() [ SNew(SVectorInputBox) .AllowSpin(true) .X(this, &SPDD_TargetRow::GetTranslation, BoneIndex, EAxis::X) .OnXChanged(this, &SPDD_TargetRow::SetTranslation, BoneIndex, EAxis::X) .Y(this, &SPDD_TargetRow::GetTranslation, BoneIndex, EAxis::Y) .OnYChanged(this, &SPDD_TargetRow::SetTranslation, BoneIndex, EAxis::Y) .Z(this, &SPDD_TargetRow::GetTranslation, BoneIndex, EAxis::Z) .OnZChanged(this, &SPDD_TargetRow::SetTranslation, BoneIndex, EAxis::Z) ] + SWidgetSwitcher::Slot() [ SNew(SRotatorInputBox) .Roll(this, &SPDD_TargetRow::GetRotation, BoneIndex, EAxis::X) .OnRollChanged(this, &SPDD_TargetRow::SetRotation, BoneIndex, EAxis::X) .Pitch(this, &SPDD_TargetRow::GetRotation, BoneIndex, EAxis::Y) .OnPitchChanged(this, &SPDD_TargetRow::SetRotation, BoneIndex, EAxis::Y) .Yaw(this, &SPDD_TargetRow::GetRotation, BoneIndex, EAxis::Z) .OnYawChanged(this, &SPDD_TargetRow::SetRotation, BoneIndex, EAxis::Z) ] ] ] ]; } return RowWidget; } FPoseDriverTarget* SPDD_TargetRow::GetTarget() const { FPoseDriverTarget* Target = nullptr; UAnimGraphNode_PoseDriver* Driver = GetPoseDriverGraphNode(); if (Driver) { Target = &(Driver->Node.PoseTargets[GetTargetIndex()]); } return Target; } UAnimGraphNode_PoseDriver* SPDD_TargetRow::GetPoseDriverGraphNode() const { UAnimGraphNode_PoseDriver* Driver = nullptr; TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); if (PoseDriverDetails.IsValid()) { Driver = PoseDriverDetails->GetFirstSelectedPoseDriver(); } return Driver; } bool SPDD_TargetRow::IsEditingRotation() const { UAnimGraphNode_PoseDriver* Driver = GetPoseDriverGraphNode(); if (Driver) { return (Driver->Node.DriveSource == EPoseDriverSource::Rotation); } return false; } void SPDD_TargetRow::NotifyTargetChanged() { TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); if (PoseDriverDetails.IsValid()) { PoseDriverDetails->NodePropHandle->NotifyPostChange(EPropertyChangeType::ValueSet); // Will push change to preview node instance } } int32 SPDD_TargetRow::GetTargetIndex() const { TSharedPtr TargetInfo = TargetInfoPtr.Pin(); return TargetInfo->TargetIndex; } bool SPDD_TargetRow::IsOverrideEnabled() const { bool bOverrideEnabled = true; UAnimGraphNode_PoseDriver* Driver = GetPoseDriverGraphNode(); if (Driver) { bOverrideEnabled = Driver->Node.RBFParams.SolverType == ERBFSolverType::Additive; } return bOverrideEnabled; } float SPDD_TargetRow::GetTargetWeight() const { float OutWeight = 0.f; UAnimGraphNode_PoseDriver* Driver = GetPoseDriverGraphNode(); if (Driver) { FAnimNode_PoseDriver* PreviewNode = Driver->GetPreviewPoseDriverNode(); if (PreviewNode) { for (const FRBFOutputWeight& Weight : PreviewNode->OutputWeights) { if (Weight.TargetIndex == GetTargetIndex()) { OutWeight = Weight.TargetWeight; } } } } return OutWeight; } int32 SPDD_TargetRow::GetTransRotWidgetIndex() const { return IsEditingRotation() ? 1 : 0; } TOptional SPDD_TargetRow::GetTranslation(int32 BoneIndex, EAxis::Type Axis) const { float Translation = 0.f; const FPoseDriverTarget* Target = GetTarget(); if (Target && Target->BoneTransforms.IsValidIndex(BoneIndex)) { Translation = static_cast(Target->BoneTransforms[BoneIndex].TargetTranslation.GetComponentForAxis(Axis)); } return Translation; } TOptional SPDD_TargetRow::GetRotation(int32 BoneIndex, EAxis::Type Axis) const { float Rotation = 0.f; const FPoseDriverTarget* Target = GetTarget(); if (Target && Target->BoneTransforms.IsValidIndex(BoneIndex)) { Rotation = static_cast(Target->BoneTransforms[BoneIndex].TargetRotation.GetComponentForAxis(Axis)); } return Rotation; } TOptional SPDD_TargetRow::GetScale() const { const FPoseDriverTarget* Target = GetTarget(); return (Target) ? Target->TargetScale : 0.f; } bool SPDD_TargetRow::IsCustomCurveEnabled() const { FPoseDriverTarget* Target = GetTarget(); return (Target && Target->bApplyCustomCurve); } void SPDD_TargetRow::OnApplyCustomCurveChanged(const ECheckBoxState NewCheckState) { FPoseDriverTarget* Target = GetTarget(); if (Target) { Target->bApplyCustomCurve = (NewCheckState == ECheckBoxState::Checked); // As a convenience, if curve is empty, add linear mapping here if (Target->bApplyCustomCurve && Target->CustomCurve.GetNumKeys() == 0) { Target->CustomCurve.AddKey(0.f, 0.f); Target->CustomCurve.AddKey(1.f, 1.f); CurveEditor->ZoomToFitHorizontal(true); } // Push value to preview NotifyTargetChanged(); } } bool SPDD_TargetRow::IsHidden() const { FPoseDriverTarget* Target = GetTarget(); if (Target) { return Target->bIsHidden; } return false; } void SPDD_TargetRow::OnIsHiddenChanged(const ECheckBoxState NewCheckState) { FPoseDriverTarget* Target = GetTarget(); if (Target) { Target->bIsHidden= (NewCheckState == ECheckBoxState::Checked); NotifyTargetChanged(); } } FText SPDD_TargetRow::GetDistanceMethodAsText() const { const FPoseDriverTarget* Target = GetTarget(); if (Target) { return FText::FromString(*DistanceMethodOptions[(int32)Target->DistanceMethod]); } return FText(); } void SPDD_TargetRow::OnDistanceMethodChanged(TSharedPtr InItem, ESelectInfo::Type SelectionType) { FPoseDriverTarget* Target = GetTarget(); if (Target) { for (int32 i = 0; i < DistanceMethodOptions.Num(); i++) { if (InItem->Compare(*DistanceMethodOptions[i]) == 0) { Target->DistanceMethod = (ERBFDistanceMethod)i; NotifyTargetChanged(); return; } } } } FText SPDD_TargetRow::GetFunctionTypeAsText() const { const FPoseDriverTarget* Target = GetTarget(); if (Target) { return FText::FromString(*FunctionTypeOptions[(int32)Target->FunctionType]); } return FText(); } void SPDD_TargetRow::OnFunctionTypeChanged(TSharedPtr InItem, ESelectInfo::Type SelectionType) { FPoseDriverTarget* Target = GetTarget(); if (Target) { for (int32 i = 0; i < FunctionTypeOptions.Num(); i++) { if (InItem->Compare(*FunctionTypeOptions[i]) == 0) { Target->FunctionType = (ERBFFunctionType)i; NotifyTargetChanged(); return; } } } } FText SPDD_TargetRow::GetDrivenNameText() const { FPoseDriverTarget* Target = GetTarget(); return (Target) ? FText::FromName(Target->DrivenName) : FText::GetEmpty(); } void SPDD_TargetRow::OnDrivenNameChanged(TSharedPtr NewName, ESelectInfo::Type SelectInfo) { FPoseDriverTarget* Target = GetTarget(); if (Target && SelectInfo != ESelectInfo::Direct) { Target->DrivenName = FName(*NewName.Get()); NotifyTargetChanged(); } } TSharedRef SPDD_TargetRow::MakeDrivenNameWidget(TSharedPtr InItem) { return SNew(STextBlock) .Text(FText::FromString(*InItem)); } FText SPDD_TargetRow::GetTargetTitleText() const { FPoseDriverTarget* Target = GetTarget(); FString DrivenName = (Target) ? Target->DrivenName.ToString() : TEXT(""); FString Title = FString::Printf(TEXT("%d - %s"), GetTargetIndex(), *DrivenName); return FText::FromString(Title); } FText SPDD_TargetRow::GetTargetWeightText() const { FString Title = FString::Printf(TEXT("%2.1f"), GetTargetWeight() * 100.f); return FText::FromString(Title); } FSlateColor SPDD_TargetRow::GetWeightBarColor() const { UAnimGraphNode_PoseDriver* PoseDriver = GetPoseDriverGraphNode(); if (PoseDriver) { return PoseDriver->GetColorFromWeight(GetTargetWeight()); } return FLinearColor::Black; } void SPDD_TargetRow::SetTranslation(float NewTrans, int32 BoneIndex, EAxis::Type Axis) { FPoseDriverTarget* Target = GetTarget(); if (Target && Target->BoneTransforms.IsValidIndex(BoneIndex)) { Target->BoneTransforms[BoneIndex].TargetTranslation.SetComponentForAxis(Axis, NewTrans); } NotifyTargetChanged(); } void SPDD_TargetRow::SetRotation(float NewRot, int32 BoneIndex, EAxis::Type Axis) { FPoseDriverTarget* Target = GetTarget(); if (Target && Target->BoneTransforms.IsValidIndex(BoneIndex)) { Target->BoneTransforms[BoneIndex].TargetRotation.SetComponentForAxis(Axis, NewRot); } NotifyTargetChanged(); } void SPDD_TargetRow::SetScale(float NewScale) { FPoseDriverTarget* Target = GetTarget(); if (Target) { Target->TargetScale = NewScale; NotifyTargetChanged(); } } void SPDD_TargetRow::SetDrivenNameText(const FText& NewText, ETextCommit::Type CommitType) { FPoseDriverTarget* Target = GetTarget(); if (Target) { Target->DrivenName = FName(*NewText.ToString()); NotifyTargetChanged(); } } void SPDD_TargetRow::RemoveTarget() { TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); if (PoseDriverDetails.IsValid()) { PoseDriverDetails->RemoveTarget(GetTargetIndex()); // This will remove me } } void SPDD_TargetRow::SoloTargetStart() { TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); if (PoseDriverDetails.IsValid()) { PoseDriverDetails->SetSoloTarget(GetTargetIndex()); } } void SPDD_TargetRow::SoloTargetEnd() { TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); if (PoseDriverDetails.IsValid()) { PoseDriverDetails->SetSoloTarget(INDEX_NONE); } } bool SPDD_TargetRow::IsSoloTarget() const { TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); if (PoseDriverDetails.IsValid()) { return PoseDriverDetails->GetSoloTarget() == GetTargetIndex(); } return false; } void SPDD_TargetRow::ExpandTargetInfo() { ExpandArea->SetExpanded(true); // This fires OnTargetExpansionChanged which causes item to get selected as well, which is fine } void SPDD_TargetRow::OnTargetExpansionChanged(bool bExpanded) { TSharedPtr PoseDriverDetails = PoseDriverDetailsPtr.Pin(); if (PoseDriverDetails.IsValid()) { PoseDriverDetails->SelectTarget(GetTargetIndex(), false); } } ////// curve editor interface TArray SPDD_TargetRow::GetCurves() const { TArray Curves; FPoseDriverTarget* Target = GetTarget(); if (Target) { Curves.Add(&(Target->CustomCurve)); } return Curves; } TArray SPDD_TargetRow::GetCurves() { TArray Curves; FPoseDriverTarget* Target = GetTarget(); if (Target) { Curves.Add(&(Target->CustomCurve)); } return Curves; } void SPDD_TargetRow::ModifyOwner() { UAnimGraphNode_PoseDriver* PoseDriver = GetPoseDriverGraphNode(); if (PoseDriver) { PoseDriver->Modify(); } } TArray SPDD_TargetRow::GetOwners() const { TArray Owners; UAnimGraphNode_PoseDriver* PoseDriver = GetPoseDriverGraphNode(); if (PoseDriver) { Owners.Add(GetPoseDriverGraphNode()); } return Owners; } void SPDD_TargetRow::MakeTransactional() { UAnimGraphNode_PoseDriver* PoseDriver = GetPoseDriverGraphNode(); if (PoseDriver) { PoseDriver->SetFlags(PoseDriver->GetFlags() | RF_Transactional); } } bool SPDD_TargetRow::IsValidCurve(FRichCurveEditInfo CurveInfo) { FPoseDriverTarget* Target = GetTarget(); if (Target) { return CurveInfo.CurveToEdit == &(Target->CustomCurve); } else { return false; } } void SPDD_TargetRow::OnCurveChanged(const TArray& ChangedCurveEditInfos) { NotifyTargetChanged(); } ////////////////////////////////////////////////////////////////////////// TSharedRef FPoseDriverDetails::MakeInstance() { return MakeShareable(new FPoseDriverDetails); } TSharedRef FPoseDriverDetails::GetToolsMenuContent() { FMenuBuilder MenuBuilder(true, nullptr); MenuBuilder.AddMenuEntry( LOCTEXT("CopyFromPoseAsset", "Copy All From PoseAsset"), LOCTEXT("CopyFromPoseAssetTooltip", "Copy target positions from PoseAsset. Will overwrite any existing targets."), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FPoseDriverDetails::ClickedOnCopyFromPoseAsset), FCanExecuteAction::CreateRaw(this, &FPoseDriverDetails::CopyFromPoseAssetIsEnabled) ) ); MenuBuilder.AddMenuEntry( LOCTEXT("AutoTargetScale", "Auto Scale"), LOCTEXT("AutoTargetScaleTooltip", "Automatically set all Scale factors, based on distance to nearest neighbour targets."), FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &FPoseDriverDetails::ClickedOnAutoScaleFactors), FCanExecuteAction::CreateRaw(this, &FPoseDriverDetails::AutoScaleFactorsIsEnabled) ) ); return MenuBuilder.MakeWidget(); } FSlateColor FPoseDriverDetails::GetToolsForegroundColor() const { static const FName InvertedForegroundName("InvertedForeground"); static const FName DefaultForegroundName("DefaultForeground"); return ToolsButton->IsHovered() ? FAppStyle::GetSlateColor(InvertedForegroundName) : FAppStyle::GetSlateColor(DefaultForegroundName); } void FPoseDriverDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { IDetailCategoryBuilder& PoseTargetsCategory = DetailBuilder.EditCategory("PoseTargets"); // Get a property handle because we might need to to invoke NotifyPostChange NodePropHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UAnimGraphNode_PoseDriver, Node)); // Bind delegate to PoseAsset changing TSharedRef PoseAssetPropHandle = DetailBuilder.GetProperty("Node.PoseAsset"); PoseAssetPropHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FPoseDriverDetails::OnPoseAssetChanged)); // Bind delegate to source bones changing TSharedRef SourceBonesPropHandle = DetailBuilder.GetProperty("Node.SourceBones"); SourceBonesPropHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FPoseDriverDetails::OnSourceBonesChanged)); // Cache set of selected things SelectedObjectsList = DetailBuilder.GetSelectedObjects(); // Create list of driven names UpdateDrivenNameOptions(); TSharedPtr PoseTargetsProperty = DetailBuilder.GetProperty("Node.PoseTargets", UAnimGraphNode_PoseDriver::StaticClass()); // Update target infos when resetting the property PoseTargetsProperty->SetOnPropertyResetToDefault(FSimpleDelegate::CreateSP(this, &FPoseDriverDetails::UpdateTargetInfosList)); IDetailPropertyRow& PoseTargetsRow = PoseTargetsCategory.AddProperty(PoseTargetsProperty); PoseTargetsRow.ShowPropertyButtons(false); FDetailWidgetRow& PoseTargetRowWidget = PoseTargetsRow.CustomWidget(); TSharedRef PoseTargetsHeaderWidget = SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) .HAlign(HAlign_Right) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "RoundButton") .ForegroundColor(FAppStyle::GetSlateColor("DefaultForeground")) .ContentPadding(FMargin(2, 0)) .OnClicked(this, &FPoseDriverDetails::ClickedAddTarget) .HAlign(HAlign_Center) .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("AddTarget", "Add Target")) .ShadowOffset(FVector2D(1, 1)) ] ] ]; PoseTargetsCategory.HeaderContent(PoseTargetsHeaderWidget); static const FName DefaultForegroundName("DefaultForeground"); PoseTargetRowWidget.WholeRowContent() .HAlign(HAlign_Fill) [ SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(1) [ SAssignNew(TargetListWidget, SPDD_TargetListType) .ListItemsSource(&TargetInfos) .OnGenerateRow(this, &FPoseDriverDetails::GenerateTargetRow) .SelectionMode(ESelectionMode::SingleToggle) .OnSelectionChanged(this, &FPoseDriverDetails::OnTargetSelectionChanged) .HeaderRow ( SNew(SHeaderRow) .Visibility(EVisibility::Collapsed) + SHeaderRow::Column(ColumnId_Target) ) ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(FMargin(6, 0, 3, 0)) [ SNew(SCheckBox) .IsChecked_Lambda([this]() { return IsSoloDrivenOnly() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged(this, &FPoseDriverDetails::OnSoloDrivenOnlyChanged) .Padding(FMargin(4.0f, 0.0f)) .ToolTipText(LOCTEXT("SoloDrivenOnlyHelp", "Only solo the driven poses or curves and leave the source joint(s) in place.")) [ SNew(STextBlock) .Text(LOCTEXT("SoloDrivenOnly", "Solo Driven Pose/Curve Only")) ] ] + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SSpacer) ] + SHorizontalBox::Slot() .Padding(2, 2) .AutoWidth() [ SAssignNew(ToolsButton, SComboButton) .ContentPadding(3.f) .ForegroundColor(this, &FPoseDriverDetails::GetToolsForegroundColor) .ButtonStyle(FAppStyle::Get(), "ToggleButton") // Use the tool bar item style for this button .OnGetMenuContent(this, &FPoseDriverDetails::GetToolsMenuContent) .ButtonContent() [ SNew(STextBlock).Text(LOCTEXT("ViewButton", "Tools ")) ] ] ] ]; // Update target list from selected pose driver node UpdateTargetInfosList(); UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); PoseDriver->SelectedTargetChangeDelegate.AddSP(this, &FPoseDriverDetails::SelectedTargetChanged); } void FPoseDriverDetails::SelectedTargetChanged() { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { SelectTarget(PoseDriver->SelectedTargetIndex, true); } } TSharedRef FPoseDriverDetails::GenerateTargetRow(TSharedPtr InInfo, const TSharedRef& OwnerTable) { check(InInfo.IsValid()); return SNew(SPDD_TargetRow, OwnerTable) .TargetInfo(InInfo) .PoseDriverDetails(SharedThis(this)); } void FPoseDriverDetails::OnTargetSelectionChanged(TSharedPtr InInfo, ESelectInfo::Type SelectInfo) { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { if (InInfo.IsValid()) { PoseDriver->SelectedTargetIndex = InInfo->TargetIndex; } else { PoseDriver->SelectedTargetIndex = INDEX_NONE; } } } void FPoseDriverDetails::OnPoseAssetChanged() { UpdateDrivenNameOptions(); } void FPoseDriverDetails::OnSourceBonesChanged() { for (const TWeakObjectPtr& Object : SelectedObjectsList) { UAnimGraphNode_PoseDriver* PoseDriver = Cast(Object.Get()); if(PoseDriver) { PoseDriver->ReserveTargetTransforms(); } } UpdateTargetInfosList(); } void FPoseDriverDetails::OnSoloDrivenOnlyChanged(const ECheckBoxState NewCheckState) { bool bDrivenOnly = (NewCheckState == ECheckBoxState::Checked); UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver&& PoseDriver->Node.bSoloDrivenOnly != bDrivenOnly) { PoseDriver->Node.bSoloDrivenOnly = bDrivenOnly; // Set this as an interactive change to avoid triggering the "node needs recompile" // message, since it won't be needed anyway. NodePropHandle->NotifyPostChange(EPropertyChangeType::Interactive); } } UAnimGraphNode_PoseDriver* FPoseDriverDetails::GetFirstSelectedPoseDriver() const { for (const TWeakObjectPtr& Object : SelectedObjectsList) { UAnimGraphNode_PoseDriver* TestPoseDriver = Cast(Object.Get()); if (TestPoseDriver != nullptr && !TestPoseDriver->IsTemplate()) { return TestPoseDriver; } } return nullptr; } void FPoseDriverDetails::UpdateTargetInfosList() { TargetInfos.Empty(); if(SelectedObjectsList.Num() == 1) { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { for (int32 i = 0; i < PoseDriver->Node.PoseTargets.Num(); i++) { TargetInfos.Add( FPDD_TargetInfo::Make(i) ); } } } TargetListWidget->RequestListRefresh(); } void FPoseDriverDetails::UpdateDrivenNameOptions() { DrivenNameOptions.Empty(); UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { // None is always an option DrivenNameOptions.Add(MakeShareable(new FString())); // Compile list of all curves in Skeleton if (PoseDriver->Node.DriveOutput == EPoseDriverOutput::DriveCurves) { USkeleton* Skeleton = PoseDriver->GetAnimBlueprint()->TargetSkeleton; if (Skeleton) { TArray NameArray; Skeleton->GetCurveMetaDataNames(NameArray); NameArray.Sort(FNameLexicalLess()); for (const FName& CurveName : NameArray) { DrivenNameOptions.Add(MakeShareable(new FString(CurveName.ToString()))); } } } // Compile list of all poses in PoseAsset else { if (PoseDriver->Node.PoseAsset) { for (const FName& Name : PoseDriver->Node.PoseAsset->GetPoseFNames()) { DrivenNameOptions.Add(MakeShareable(new FString(Name.ToString()))); } } } } } void FPoseDriverDetails::ClickedOnCopyFromPoseAsset() { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { PoseDriver->CopyTargetsFromPoseAsset(); // Also update radius/scales float MaxDist; PoseDriver->AutoSetTargetScales(MaxDist); PoseDriver->Node.RBFParams.Radius = 0.5f * MaxDist; // reasonable default radius UpdateTargetInfosList(); NodePropHandle->NotifyPostChange(EPropertyChangeType::ValueSet); } } bool FPoseDriverDetails::CopyFromPoseAssetIsEnabled() const { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); return(PoseDriver && PoseDriver->Node.PoseAsset); } void FPoseDriverDetails::ClickedOnAutoScaleFactors() { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { float MaxDist; PoseDriver->AutoSetTargetScales(MaxDist); PoseDriver->Node.RBFParams.Radius = 0.5f * MaxDist; // reasonable default radius NodePropHandle->NotifyPostChange(EPropertyChangeType::ValueSet); } } bool FPoseDriverDetails::AutoScaleFactorsIsEnabled() const { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); return(PoseDriver && PoseDriver->Node.PoseTargets.Num() > 1); } FReply FPoseDriverDetails::ClickedAddTarget() { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { PoseDriver->AddNewTarget(); UpdateTargetInfosList(); NodePropHandle->NotifyPostChange(EPropertyChangeType::ArrayAdd); // will push changes to preview node instance } return FReply::Handled(); } void FPoseDriverDetails::RemoveTarget(int32 TargetIndex) { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver && PoseDriver->Node.PoseTargets.IsValidIndex(TargetIndex)) { PoseDriver->Node.PoseTargets.RemoveAt(TargetIndex); UpdateTargetInfosList(); NodePropHandle->NotifyPostChange(EPropertyChangeType::ArrayRemove); // will push changes to preview node instance } } void FPoseDriverDetails::SetSoloTarget(int32 TargetIndex) { UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { FAnimNode_PoseDriver& Node = PoseDriver->Node; if (PoseDriver->Node.PoseTargets.IsValidIndex(TargetIndex)) { Node.SoloTargetIndex = TargetIndex; } else { Node.SoloTargetIndex = INDEX_NONE; } // Set this as an interactive change to avoid triggering the "node needs recompile" // message, since it won't be needed anyway. NodePropHandle->NotifyPostChange(EPropertyChangeType::Interactive); } } int32 FPoseDriverDetails::GetSoloTarget() const { const UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); if (PoseDriver) { return PoseDriver->Node.SoloTargetIndex; } return INDEX_NONE; } bool FPoseDriverDetails::IsSoloDrivenOnly() const { const UAnimGraphNode_PoseDriver* PoseDriver = GetFirstSelectedPoseDriver(); return PoseDriver ? PoseDriver->Node.bSoloDrivenOnly : true; } void FPoseDriverDetails::SelectTarget(int32 TargetIndex, bool bExpandTarget) { if (TargetInfos.IsValidIndex(TargetIndex)) { TargetListWidget->SetSelection(TargetInfos[TargetIndex]); if (bExpandTarget) { TargetInfos[TargetIndex]->ExpandTargetDelegate.Broadcast(); } } } #undef LOCTEXT_NAMESPACE