// Copyright Epic Games, Inc. All Rights Reserved. #include "BoneProxyDetailsCustomization.h" #include "BoneProxy.h" #include "AnimPreviewInstance.h" #include "PropertyHandle.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "IDetailGroup.h" #include "IDetailPropertyRow.h" #include "Animation/DebugSkelMeshComponent.h" #include "ScopedTransaction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SSegmentedControl.h" #include "Widgets/Layout/SBox.h" #include "Algo/Transform.h" #include "SAdvancedTransformInputBox.h" #if WITH_EDITOR #include "HAL/PlatformApplicationMisc.h" #endif #define LOCTEXT_NAMESPACE "FBoneProxyDetailsCustomization" namespace BoneProxyCustomizationConstants { static const float ItemWidth = 125.0f; } static FText GetTransformFieldText(bool* bValuePtr, FText Label) { return *bValuePtr ? FText::Format(LOCTEXT("Local", "Local {0}"), Label) : FText::Format(LOCTEXT("World", "World {0}"), Label); } static void OnSetRelativeTransform(bool* bValuePtr) { *bValuePtr = true; } static void OnSetWorldTransform(bool* bValuePtr) { *bValuePtr = false; } static bool IsRelativeTransformChecked(bool* bValuePtr) { return *bValuePtr; } static bool IsWorldTransformChecked(bool* bValuePtr) { return !*bValuePtr; } static TSharedRef BuildTransformFieldLabel(bool* bValuePtr, const FText& Label, bool bMultiSelected) { if (bMultiSelected) { return SNew(STextBlock) .Text(Label) .Font(IDetailLayoutBuilder::GetDetailFont()); } else { FMenuBuilder MenuBuilder(true, nullptr, nullptr); FUIAction SetRelativeLocationAction ( FExecuteAction::CreateStatic(&OnSetRelativeTransform, bValuePtr), FCanExecuteAction(), FIsActionChecked::CreateStatic(&IsRelativeTransformChecked, bValuePtr) ); FUIAction SetWorldLocationAction ( FExecuteAction::CreateStatic(&OnSetWorldTransform, bValuePtr), FCanExecuteAction(), FIsActionChecked::CreateStatic(&IsWorldTransformChecked, bValuePtr) ); MenuBuilder.BeginSection( TEXT("TransformType"), FText::Format( LOCTEXT("TransformType", "{0} Type"), Label ) ); MenuBuilder.AddMenuEntry ( FText::Format( LOCTEXT( "LocalLabel", "Local"), Label ), FText::Format( LOCTEXT( "LocalLabel_ToolTip", "{0} is relative to its parent"), Label ), FSlateIcon(), SetRelativeLocationAction, NAME_None, EUserInterfaceActionType::RadioButton ); MenuBuilder.AddMenuEntry ( FText::Format( LOCTEXT( "WorldLabel", "World"), Label ), FText::Format( LOCTEXT( "WorldLabel_ToolTip", "{0} is relative to the world"), Label ), FSlateIcon(), SetWorldLocationAction, NAME_None, EUserInterfaceActionType::RadioButton ); MenuBuilder.EndSection(); return SNew(SHorizontalBox) +SHorizontalBox::Slot() .HAlign(HAlign_Left) [ SNew(SComboButton) .ContentPadding( 0.f ) .ButtonStyle( FAppStyle::Get(), "NoBorder" ) .ForegroundColor( FSlateColor::UseForeground() ) .MenuContent() [ MenuBuilder.MakeWidget() ] .ButtonContent() [ SNew( SBox ) .Padding( FMargin( 0.0f, 0.0f, 2.0f, 0.0f ) ) [ SNew(STextBlock) .Text_Static(&GetTransformFieldText, bValuePtr, Label) .Font(IDetailLayoutBuilder::GetDetailFont()) ] ] ]; } } namespace { class FBoneProxyDetailsErrorPipe : public FOutputDevice { public: int32 NumErrors; FBoneProxyDetailsErrorPipe() : FOutputDevice() , NumErrors(0) { } virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category) override { NumErrors++; } }; template bool GetDataFromContent(const FString& Content, DataType& OutData) { FBoneProxyDetailsErrorPipe ErrorPipe; static UScriptStruct* DataStruct = TBaseStructure::Get(); DataStruct->ImportText(*Content, &OutData, nullptr, PPF_None, &ErrorPipe, DataStruct->GetName(), true); return (ErrorPipe.NumErrors == 0); } } void FBoneProxyDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { TArray> Objects; DetailBuilder.GetObjectsBeingCustomized(Objects); BoneProxies.Empty(); Algo::TransformIf(Objects, BoneProxies, [](TWeakObjectPtr InItem) { return InItem.IsValid() && InItem.Get()->IsA(); }, [](TWeakObjectPtr InItem) { return CastChecked(InItem.Get()); }); TArrayView BoneProxiesView(BoneProxies); UBoneProxy* FirstBoneProxy = CastChecked(Objects[0].Get()); bool bIsEditingEnabled = true; if (UDebugSkelMeshComponent* Component = FirstBoneProxy->SkelMeshComponent.Get()) { bIsEditingEnabled = (Component->AnimScriptInstance == Component->PreviewInstance); } if (bIsEditingEnabled) { bool bHasEditable = false; for (const UBoneProxy* BoneProxy : BoneProxies) { if (BoneProxy->bIsTransformEditable) { bHasEditable = true; break; } } bIsEditingEnabled = bHasEditable; } DetailBuilder.HideCategory(TEXT("Transform")); DetailBuilder.HideCategory(TEXT("Reference Transform")); DetailBuilder.HideCategory(TEXT("Mesh Relative Transform")); DetailBuilder.EditCategory(TEXT("Bone")).SetSortOrder(1); IDetailCategoryBuilder& CategoryBuilder = DetailBuilder.EditCategory(TEXT("Transforms")); CategoryBuilder.SetSortOrder(2); const TArray ButtonLabels = { LOCTEXT("BoneTransform", "Bone"), LOCTEXT("ReferenceTransform", "Reference"), LOCTEXT("MeshTransform", "Mesh Relative") }; const TArray ButtonTooltips = { LOCTEXT("BoneTransformTooltip", "The transform of the bone"), LOCTEXT("ReferenceTransformTooltip", "The reference transform of a bone (original)"), LOCTEXT("MeshTransformTooltip", "The relative transform of the mesh") }; const TArray TransformTypes = { UBoneProxy::TransformType_Bone, UBoneProxy::TransformType_Reference, UBoneProxy::TransformType_Mesh }; static TAttribute> VisibleTransform = TransformTypes; TSharedPtr> TransformChoiceWidget = SSegmentedControl::Create( TransformTypes, ButtonLabels, ButtonTooltips, VisibleTransform ); // use a static shared ref so that all views retain these settings static TSharedRef> VisibleTransforms = MakeShareable(new TArray({ UBoneProxy::TransformType_Bone, UBoneProxy::TransformType_Reference, UBoneProxy::TransformType_Mesh})); CategoryBuilder.AddCustomRow(FText::FromString(TEXT("TransformType"))) .ValueContent() .MinDesiredWidth(375.f) .MaxDesiredWidth(375.f) .HAlign(HAlign_Left) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ TransformChoiceWidget.ToSharedRef() ] ]; SAdvancedTransformInputBox::FArguments TransformWidgetArgs = SAdvancedTransformInputBox::FArguments() .DisplayRelativeWorld(true) .AllowEditRotationRepresentation(false) .DisplayScaleLock(true) .Font(IDetailLayoutBuilder::GetDetailFont()) .UseQuaternionForRotation(false) .OnGetIsComponentRelative_Lambda( [BoneProxiesView](ESlateTransformComponent::Type InComponent) { switch(InComponent) { case ESlateTransformComponent::Location: return BoneProxiesView[0]->bLocalLocation; case ESlateTransformComponent::Rotation: return BoneProxiesView[0]->bLocalRotation; case ESlateTransformComponent::Scale: return BoneProxiesView[0]->bLocalScale; } return true; }) .OnIsComponentRelativeChanged_Lambda( [BoneProxiesView](ESlateTransformComponent::Type InComponent, bool bIsRelative) { for(UBoneProxy* BoneProxy : BoneProxiesView) { switch(InComponent) { case ESlateTransformComponent::Location: { BoneProxy->bLocalLocation = bIsRelative; break; } case ESlateTransformComponent::Rotation: { BoneProxy->bLocalRotation = bIsRelative; break; } case ESlateTransformComponent::Scale: { BoneProxy->bLocalScale = bIsRelative; break; } } } }); TArray> Properties; Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, Location))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, Rotation))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, Scale))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, ReferenceLocation))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, ReferenceRotation))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, ReferenceScale))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, MeshLocation))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, MeshRotation))); Properties.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UBoneProxy, MeshScale))); int32 PropertyIndex = 0; for(int32 TransformIndex=0;TransformIndex<3;TransformIndex++) { // only the first transform can be edited const UBoneProxy::ETransformType TransformType = (UBoneProxy::ETransformType)TransformIndex; bIsEditingEnabled = TransformType == UBoneProxy::TransformType_Bone ? bIsEditingEnabled : false; TransformWidgetArgs .IsEnabled(bIsEditingEnabled) .DisplayRelativeWorld(bIsEditingEnabled) .DisplayScaleLock(bIsEditingEnabled) .OnGetNumericValue_Static(&UBoneProxy::GetMultiNumericValue, TransformType, BoneProxiesView) .OnCopyToClipboard_Lambda([TransformType, BoneProxiesView](ESlateTransformComponent::Type InComponent) { if(BoneProxiesView.Num() == 0) { return; } if(TransformType == UBoneProxy::TransformType_Bone) { FString Content; UBoneProxy* BoneProxy = BoneProxiesView[0]; switch(InComponent) { case ESlateTransformComponent::Location: { const FVector Data = BoneProxy->Location; TBaseStructure::Get()->ExportText(Content, &Data, &Data, nullptr, PPF_None, nullptr); break; } case ESlateTransformComponent::Rotation: { const FRotator Data = BoneProxy->Rotation; TBaseStructure::Get()->ExportText(Content, &Data, &Data, nullptr, PPF_None, nullptr); break; } case ESlateTransformComponent::Scale: { const FVector Data = BoneProxy->Scale; TBaseStructure::Get()->ExportText(Content, &Data, &Data, nullptr, PPF_None, nullptr); break; } case ESlateTransformComponent::Max: default: { const FEulerTransform Data(BoneProxy->Location, BoneProxy->Rotation, BoneProxy->Scale); TBaseStructure::Get()->ExportText(Content, &Data, &Data, nullptr, PPF_None, nullptr); break; } } if(!Content.IsEmpty()) { FPlatformApplicationMisc::ClipboardCopy(*Content); } } }) .OnPasteFromClipboard_Lambda([TransformType, BoneProxiesView](ESlateTransformComponent::Type InComponent) { FString Content; FPlatformApplicationMisc::ClipboardPaste(Content); if(Content.IsEmpty() || TransformType != UBoneProxy::TransformType_Bone) { return; } FScopedTransaction Transaction(LOCTEXT("PasteTransform", "Paste Transform")); static const FName LocationPropertyName = GET_MEMBER_NAME_CHECKED(UBoneProxy, Location); static const FName RotationPropertyName = GET_MEMBER_NAME_CHECKED(UBoneProxy, Rotation); static const FName ScalePropertyName = GET_MEMBER_NAME_CHECKED(UBoneProxy, Scale); for (UBoneProxy* BoneProxy : BoneProxiesView) { if (!BoneProxy->bIsTransformEditable) { continue; // Skip non editable bones } if (const UDebugSkelMeshComponent* Component = BoneProxy->SkelMeshComponent.Get()) { Component->PreviewInstance->SetFlags(RF_Transactional); Component->PreviewInstance->Modify(); switch(InComponent) { case ESlateTransformComponent::Location: { FVector Data = BoneProxy->Location; if (GetDataFromContent(Content, Data)) { BoneProxy->OnPreEditChange(LocationPropertyName); BoneProxy->Location = Data; BoneProxy->OnPostEditChangeProperty(LocationPropertyName); } break; } case ESlateTransformComponent::Rotation: { FRotator Data = BoneProxy->Rotation; if (GetDataFromContent(Content, Data)) { BoneProxy->OnPreEditChange(RotationPropertyName); BoneProxy->Rotation = Data; BoneProxy->OnPostEditChangeProperty(RotationPropertyName); } break; } case ESlateTransformComponent::Scale: { FVector Data = BoneProxy->Scale; if (GetDataFromContent(Content, Data)) { BoneProxy->OnPreEditChange(ScalePropertyName); BoneProxy->Scale = Data; BoneProxy->OnPostEditChangeProperty(ScalePropertyName); } break; } case ESlateTransformComponent::Max: default: { FEulerTransform Data = FEulerTransform::Identity; if (GetDataFromContent(Content, Data)) { BoneProxy->OnPreEditChange(LocationPropertyName); BoneProxy->Location = Data.GetLocation(); BoneProxy->OnPostEditChangeProperty(LocationPropertyName); BoneProxy->OnPreEditChange(RotationPropertyName); BoneProxy->Rotation = Data.Rotator(); BoneProxy->OnPostEditChangeProperty(RotationPropertyName); BoneProxy->OnPreEditChange(ScalePropertyName); BoneProxy->Scale = Data.GetScale3D(); BoneProxy->OnPostEditChangeProperty(ScalePropertyName); } break; } } } } }) .DiffersFromDefault_Lambda([TransformType, BoneProxiesView](ESlateTransformComponent::Type InComponent) -> bool { for(UBoneProxy* BoneProxy : BoneProxiesView) { if(BoneProxy->DiffersFromDefault(InComponent, TransformType)) { return true; } } return false; }) .OnResetToDefault_Lambda([TransformType, BoneProxiesView](ESlateTransformComponent::Type InComponent) { FScopedTransaction Transaction(LOCTEXT("ResetToDefault", "Reset to Default")); for(UBoneProxy* BoneProxy : BoneProxiesView) { BoneProxy->ResetToDefault(InComponent, TransformType); } }); TransformWidgetArgs.Visibility_Lambda([TransformChoiceWidget, TransformType]() -> EVisibility { return TransformChoiceWidget->HasValue(TransformType) ? EVisibility::Visible : EVisibility::Collapsed; }); if(bIsEditingEnabled) { constexpr bool bNonTransactional = false; constexpr bool bTransactional = true; TransformWidgetArgs.OnBeginSliderMovement_Static(&UBoneProxy::OnSliderMovementStateChanged, 0.0, UBoneProxy::ESliderMovementState::Begin, BoneProxiesView); TransformWidgetArgs.OnEndSliderMovement_Static(&UBoneProxy::OnSliderMovementStateChanged, UBoneProxy::ESliderMovementState::End, BoneProxiesView); TransformWidgetArgs.OnNumericValueChanged_Static(&UBoneProxy::OnMultiNumericValueChanged, ETextCommit::Default, bNonTransactional, TransformType, BoneProxiesView); TransformWidgetArgs.OnNumericValueCommitted_Static (&UBoneProxy::OnMultiNumericValueChanged, bTransactional, TransformType, BoneProxiesView); } SAdvancedTransformInputBox::ConstructGroupedTransformRows( CategoryBuilder, ButtonLabels[TransformIndex], ButtonTooltips[TransformIndex], TransformWidgetArgs); } } bool FBoneProxyDetailsCustomization::IsResetLocationVisible(TSharedPtr InPropertyHandle, TArrayView InBoneProxies) { for (UBoneProxy* BoneProxy : InBoneProxies) { if (UDebugSkelMeshComponent* Component = BoneProxy->SkelMeshComponent.Get()) { if (FAnimNode_ModifyBone* ModifyBone = Component->PreviewInstance->FindModifiedBone(BoneProxy->BoneName)) { if (ModifyBone->Translation != FVector::ZeroVector && BoneProxy->bIsTransformEditable) { return true; } } } } return false; } bool FBoneProxyDetailsCustomization::IsResetRotationVisible(TSharedPtr InPropertyHandle, TArrayView InBoneProxies) { for (UBoneProxy* BoneProxy : InBoneProxies) { if (UDebugSkelMeshComponent* Component = BoneProxy->SkelMeshComponent.Get()) { if (FAnimNode_ModifyBone* ModifyBone = Component->PreviewInstance->FindModifiedBone(BoneProxy->BoneName)) { if (ModifyBone->Rotation != FRotator::ZeroRotator && BoneProxy->bIsTransformEditable) { return true; } } } } return false; } bool FBoneProxyDetailsCustomization::IsResetScaleVisible(TSharedPtr InPropertyHandle, TArrayView InBoneProxies) { for (UBoneProxy* BoneProxy : InBoneProxies) { if (UDebugSkelMeshComponent* Component = BoneProxy->SkelMeshComponent.Get()) { if (FAnimNode_ModifyBone* ModifyBone = Component->PreviewInstance->FindModifiedBone(BoneProxy->BoneName)) { if (ModifyBone->Scale != FVector(1.0f) && BoneProxy->bIsTransformEditable) { return true; } } } } return false; } void FBoneProxyDetailsCustomization::HandleResetLocation(TSharedPtr InPropertyHandle, TArrayView InBoneProxies) { FScopedTransaction Transaction(LOCTEXT("ResetLocation", "Reset Location")); for (UBoneProxy* BoneProxy : InBoneProxies) { if (UDebugSkelMeshComponent* Component = BoneProxy->SkelMeshComponent.Get()) { BoneProxy->Modify(); Component->PreviewInstance->Modify(); FAnimNode_ModifyBone& ModifyBone = Component->PreviewInstance->ModifyBone(BoneProxy->BoneName); ModifyBone.Translation = FVector::ZeroVector; RemoveUnnecessaryModifications(Component, ModifyBone); } } } void FBoneProxyDetailsCustomization::HandleResetRotation(TSharedPtr InPropertyHandle, TArrayView InBoneProxies) { FScopedTransaction Transaction(LOCTEXT("ResetRotation", "Reset Rotation")); for (UBoneProxy* BoneProxy : InBoneProxies) { if (UDebugSkelMeshComponent* Component = BoneProxy->SkelMeshComponent.Get()) { BoneProxy->Modify(); Component->PreviewInstance->Modify(); FAnimNode_ModifyBone& ModifyBone = Component->PreviewInstance->ModifyBone(BoneProxy->BoneName); ModifyBone.Rotation = FRotator::ZeroRotator; RemoveUnnecessaryModifications(Component, ModifyBone); } } } void FBoneProxyDetailsCustomization::HandleResetScale(TSharedPtr InPropertyHandle, TArrayView InBoneProxies) { FScopedTransaction Transaction(LOCTEXT("ResetScale", "Reset Scale")); for (UBoneProxy* BoneProxy : InBoneProxies) { if (UDebugSkelMeshComponent* Component = BoneProxy->SkelMeshComponent.Get()) { BoneProxy->Modify(); Component->PreviewInstance->Modify(); FAnimNode_ModifyBone& ModifyBone = Component->PreviewInstance->ModifyBone(BoneProxy->BoneName); ModifyBone.Scale = FVector(1.0f); RemoveUnnecessaryModifications(Component, ModifyBone); } } } void FBoneProxyDetailsCustomization::RemoveUnnecessaryModifications(UDebugSkelMeshComponent* Component, FAnimNode_ModifyBone& ModifyBone) { if (ModifyBone.Translation == FVector::ZeroVector && ModifyBone.Rotation == FRotator::ZeroRotator && ModifyBone.Scale == FVector(1.0f)) { Component->PreviewInstance->RemoveBoneModification(ModifyBone.BoneToModify.BoneName); } } #undef LOCTEXT_NAMESPACE