// Copyright Epic Games, Inc. All Rights Reserved. #include "SAnimationModifiersTab.h" #include "Animation/AnimSequence.h" #include "Animation/Skeleton.h" #include "AnimationModifier.h" #include "AnimationModifierHelpers.h" #include "AnimationModifiersAssetUserData.h" #include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/IAssetRegistry.h" #include "ClassViewerModule.h" #include "Delegates/Delegate.h" #include "DetailsViewArgs.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Engine/Blueprint.h" #include "Engine/BlueprintGeneratedClass.h" #include "Framework/SlateDelegates.h" #include "HAL/Platform.h" #include "HAL/PlatformCrt.h" #include "HAL/PlatformMisc.h" #include "IDetailsView.h" #include "Interfaces/Interface_AssetUserData.h" #include "Internationalization/Internationalization.h" #include "Internationalization/Text.h" #include "Layout/Children.h" #include "Layout/Margin.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Misc/MessageDialog.h" #include "Misc/ScopedSlowTask.h" #include "Modules/ModuleManager.h" #include "PropertyEditorModule.h" #include "SModifierListview.h" #include "ScopedTransaction.h" #include "SlotBase.h" #include "Styling/AppStyle.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Templates/Casts.h" #include "Templates/SubclassOf.h" #include "Toolkits/AssetEditorToolkit.h" #include "Types/SlateEnums.h" #include "UObject/NameTypes.h" #include "UObject/Object.h" #include "UObject/WeakObjectPtr.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SMenuAnchor.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/SWidget.h" #include "Widgets/Text/STextBlock.h" class UClass; struct FGeometry; #define LOCTEXT_NAMESPACE "SAnimationModifiersTab" SAnimationModifiersTab::SAnimationModifiersTab() : Skeleton(nullptr), AnimationSequence(nullptr), AssetUserData(nullptr), bDirty(false) { } SAnimationModifiersTab::~SAnimationModifiersTab() { if (GEditor) { GEditor->UnregisterForUndo(this); GEditor->GetEditorSubsystem()->OnAssetOpenedInEditor().RemoveAll(this); } } void SAnimationModifiersTab::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (bDirty) { RetrieveModifierData(); ModifierListView->Refresh(); bDirty = false; } } void SAnimationModifiersTab::PostUndo(bool bSuccess) { Refresh(); } void SAnimationModifiersTab::PostRedo(bool bSuccess) { Refresh(); } void SAnimationModifiersTab::Construct(const FArguments& InArgs) { HostingApp = InArgs._InHostingApp; // Retrieve asset and modifier data RetrieveAnimationAsset(); RetrieveModifierData(); CreateInstanceDetailsView(); FOnGetContent GetContent = FOnGetContent::CreateLambda( [this]() { return FAnimationModifierHelpers::GetModifierPicker(FOnClassPicked::CreateRaw(this, &SAnimationModifiersTab::OnModifierPicked)); }); this->ChildSlot [ SNew(SOverlay) +SOverlay::Slot() [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SBorder) .Padding(2.0f) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(3.0f, 3.0f) .AutoWidth() [ SAssignNew(AddModifierCombobox, SComboButton) .OnGetMenuContent(GetContent) .ButtonContent() [ SNew(STextBlock) .Text(LOCTEXT("AddModifier", "Add Modifier")) ] ] + SHorizontalBox::Slot() .Padding(3.0f, 3.0f) .AutoWidth() [ SNew(SButton) .OnClicked(this, &SAnimationModifiersTab::OnApplyAllModifiersClicked) .ContentPadding(FMargin(5)) .Content() [ SNew(STextBlock) .Text(LOCTEXT("ApplyAllModifiers", "Apply All Modifiers")) ] ] ] ] + SVerticalBox::Slot() [ SNew(SBorder) .Padding(2.0f) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SSplitter) .Orientation(EOrientation::Orient_Vertical) + SSplitter::Slot() .Value(.5f) [ SNew(SBox) .Padding(2.0f) [ SAssignNew(ModifierListView, SModifierListView) .Items(&ModifierItems) .InstanceDetailsView(ModifierInstanceDetailsView) .OnApplyModifier(FOnModifierArray::CreateSP(this, &SAnimationModifiersTab::OnApplyModifier)) .OnCanRevertModifier(FOnCanModifierArray::CreateSP(this, &SAnimationModifiersTab::OnCanRevertModifier)) .OnRevertModifier(FOnModifierArray::CreateSP(this, &SAnimationModifiersTab::OnRevertModifier)) .OnRemoveModifier(FOnModifierArray::CreateSP(this, &SAnimationModifiersTab::OnRemoveModifier)) .OnOpenModifier(FOnSingleModifier::CreateSP(this, &SAnimationModifiersTab::OnOpenModifier)) .OnMoveUpModifier(FOnSingleModifier::CreateSP(this, &SAnimationModifiersTab::OnMoveModifierUp)) .OnMoveDownModifier(FOnSingleModifier::CreateSP(this, &SAnimationModifiersTab::OnMoveModifierDown)) ] ] + SSplitter::Slot() .Value(.5f) [ SNew(SBox) .Padding(2.0f) [ ModifierInstanceDetailsView->AsShared() ] ] ] ] ] ]; // Ensure that this tab is only enabled if we have a valid AssetUserData instance this->ChildSlot.GetWidget()->SetEnabled(TAttribute::Create([this]() { return AssetUserData != nullptr; })); // Register delegates if (GEditor) { GEditor->RegisterForUndo(this); } GEditor->GetEditorSubsystem()->OnAssetOpenedInEditor().AddSP(this, &SAnimationModifiersTab::OnAssetOpened); } void SAnimationModifiersTab::OnModifierPicked(UClass* PickedClass) { FScopedTransaction Transaction(LOCTEXT("AddModifierTransaction", "Adding Animation Modifier")); UObject* Outer = AssetUserData; UAnimationModifier* Processor = FAnimationModifierHelpers::CreateModifierInstance(Outer, PickedClass); AssetUserData->Modify(); AssetUserData->AddAnimationModifier(Processor); // Close the combo box AddModifierCombobox->SetIsOpen(false); // Refresh the UI Refresh(); } void SAnimationModifiersTab::RetrieveAnimationAsset() { TSharedPtr AssetEditor = HostingApp.Pin(); const TArray* EditedObjects = AssetEditor->GetObjectsCurrentlyBeingEdited(); AssetUserData = nullptr; if (EditedObjects) { // Try and find an AnimSequence or Skeleton asset in the currently being edited objects, and retrieve or add ModfiersAssetUserDat for (UObject* Object : (*EditedObjects)) { if (Object->IsA()) { AnimationSequence = Cast(Object); AssetUserData = FAnimationModifierHelpers::RetrieveOrCreateModifierUserData(AnimationSequence); break; } else if (Object->IsA()) { Skeleton = Cast(Object); AssetUserData = FAnimationModifierHelpers::RetrieveOrCreateModifierUserData(Skeleton); break; } } } } void SAnimationModifiersTab::CreateInstanceDetailsView() { // Create a property view FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bAllowSearch = false; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsViewArgs.bHideSelectionTip = true; DetailsViewArgs.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Automatic; DetailsViewArgs.bShowOptions = false; ModifierInstanceDetailsView = EditModule.CreateDetailView(DetailsViewArgs); ModifierInstanceDetailsView->SetDisableCustomDetailLayouts(true); } FReply SAnimationModifiersTab::OnApplyAllModifiersClicked() { FScopedTransaction Transaction(LOCTEXT("ApplyAllModifiersTransaction", "Applying All Animation Modifier(s)")); const TArray& ModifierInstances = AssetUserData->GetAnimationModifierInstances(); ApplyModifiers(ModifierInstances); return FReply::Handled(); } void SAnimationModifiersTab::OnApplyModifier(const TArray>& Instances) { TArray ModifierInstances; for (TWeakObjectPtr InstancePtr : Instances) { checkf(InstancePtr.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); UAnimationModifier* Instance = InstancePtr.Get(); ModifierInstances.Add(Instance); } FScopedTransaction Transaction(LOCTEXT("ApplyModifiersTransaction", "Applying Animation Modifier(s)")); ApplyModifiers(ModifierInstances); Refresh(); } void SAnimationModifiersTab::FindAnimSequencesForSkeleton(TArray& ReferencedAnimSequences) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(FName("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); // Search for referencing packages to the currently open skeleton TArray Referencers; AssetRegistry.GetReferencers(Skeleton->GetOuter()->GetFName(), Referencers); for (const FAssetIdentifier& Identifier : Referencers) { TArray Assets; AssetRegistry.GetAssetsByPackageName(Identifier.PackageName, Assets); for (const FAssetData& Asset : Assets) { // Only add assets whos class is of UAnimSequence if (Asset.IsInstanceOf(UAnimSequence::StaticClass())) { ReferencedAnimSequences.Add(CastChecked(Asset.GetAsset())); } } } } void SAnimationModifiersTab::OnRevertModifier(const TArray>& Instances) { TArray ModifierInstances; for (TWeakObjectPtr InstancePtr : Instances) { checkf(InstancePtr.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); UAnimationModifier* Instance = InstancePtr.Get(); ModifierInstances.Add(Instance); } FScopedTransaction Transaction(LOCTEXT("RevertModifiersTransaction", "Reverting Animation Modifier(s)")); RevertModifiers(ModifierInstances); Refresh(); } bool SAnimationModifiersTab::OnCanRevertModifier(const TArray>& Instances) { bool bCanRevert = false; if (AnimationSequence) { for (const TWeakObjectPtr& InstancePtr : Instances) { checkf(InstancePtr.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); const UAnimationModifier* Modifier = InstancePtr.Get(); // At least one instance has to be revert-able if (Modifier->CanRevert(AnimationSequence)) { bCanRevert = true; break; } } } else if (Skeleton) { // Check for modifiers applied on previous version // Where asset registry tags were not written for (const TWeakObjectPtr& Modifier : Instances) { if (Modifier->HasLegacyPreviousAppliedModifierOnSkeleton()) { bCanRevert = true; return true; } } FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(FName("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); TSet Modifiers; Algo::Transform(Instances, Modifiers, [](const TWeakObjectPtr& Ptr) { return Ptr.Get()->GetName(); }); FARFilter Filter; Filter.ClassPaths.Add(UAnimSequence::StaticClass()->GetClassPathName()); Filter.TagsAndValues.Add(FName{"Skeleton"}) = FAssetData(Skeleton).GetExportTextName(); AssetRegistry.EnumerateAssets(Filter, [&](const FAssetData& Asset) { FString Tag = Asset.GetTagValueRef(UAnimationModifier::AnimationModifiersTag); FAnimationModifierHelpers::EnumerateAnimationModifierTags(Tag, [&](FStringView Name, FGuid Revision) { bCanRevert = Modifiers.ContainsByHash(GetTypeHash(Name), Name); return !bCanRevert; // Break if bCanRevert is already true }); return !bCanRevert; // Break if bCanRevert is already true }); } return bCanRevert; } void SAnimationModifiersTab::OnRemoveModifier(const TArray>& Instances) { const bool bShouldRevert = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("RemoveAndRevertPopupText", "Should the Modifiers be reverted before removing them?"), LOCTEXT("RemoveAndRevertPopupTitle", "Revert before Removing")) == EAppReturnType::Yes; FScopedTransaction Transaction(LOCTEXT("RemoveModifiersTransaction", "Removing Animation Modifier(s)")); AssetUserData->Modify(); if (bShouldRevert) { TArray ModifierInstances; for (TWeakObjectPtr InstancePtr : Instances) { checkf(InstancePtr.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); UAnimationModifier* Instance = InstancePtr.Get(); ModifierInstances.Add(Instance); } RevertModifiers(ModifierInstances); } for (TWeakObjectPtr InstancePtr : Instances) { checkf(InstancePtr.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); UAnimationModifier* Instance = InstancePtr.Get(); AssetUserData->Modify(); AssetUserData->RemoveAnimationModifierInstance(Instance); } Refresh(); } void SAnimationModifiersTab::OnOpenModifier(const TWeakObjectPtr& Instance) { checkf(Instance.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); const UAnimationModifier* ModifierInstance = Instance.Get(); const UBlueprintGeneratedClass* BPGeneratedClass = Cast(ModifierInstance->GetClass()); if (BPGeneratedClass && BPGeneratedClass->ClassGeneratedBy) { UBlueprint* Blueprint = Cast(BPGeneratedClass->ClassGeneratedBy); if (Blueprint) { GEditor->GetEditorSubsystem()->OpenEditorForAsset(Blueprint); } } } void SAnimationModifiersTab::OnMoveModifierUp(const TWeakObjectPtr& Instance) { FScopedTransaction Transaction(LOCTEXT("MoveModifierUpTransaction", "Moving Animation Modifier Up")); checkf(Instance.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); AssetUserData->Modify(); AssetUserData->ChangeAnimationModifierIndex(Instance.Get(), -1); Refresh(); } void SAnimationModifiersTab::OnMoveModifierDown(const TWeakObjectPtr& Instance) { FScopedTransaction Transaction(LOCTEXT("MoveModifierDownTransaction", "Moving Animation Modifier Down")); checkf(Instance.IsValid(), TEXT("Invalid weak object ptr to modifier instance")); AssetUserData->Modify(); AssetUserData->ChangeAnimationModifierIndex(Instance.Get(), 1); Refresh(); } void SAnimationModifiersTab::Refresh() { bDirty = true; } void SAnimationModifiersTab::OnBlueprintCompiled(UBlueprint* Blueprint) { if (Blueprint) { Refresh(); } } void SAnimationModifiersTab::OnAssetOpened(UObject* Object, IAssetEditorInstance* Instance) { RetrieveAnimationAsset(); RetrieveModifierData(); ModifierListView->Refresh(); } void SAnimationModifiersTab::ApplyModifiers(const TArray& Modifiers) { bool bApply = true; TArray AnimSequences; if (AnimationSequence != nullptr) { AnimSequences.Add(AnimationSequence); } else if (Skeleton != nullptr) { // Double check with the user for applying all modifiers to referenced animation sequences for the skeleton bApply = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("ApplyingSkeletonModifierPopupText", "Are you sure you want to apply the modifiers to all animation sequences referenced by the current skeleton?"), LOCTEXT("ApplyingSkeletonModifierPopupTitle", "Are you sure?")) == EAppReturnType::Yes; if (bApply) { FindAnimSequencesForSkeleton(AnimSequences); } } if (bApply) { UE::Anim::FApplyModifiersScope Scope; FScopedSlowTask SlowTaskModifier(Modifiers.Num(), LOCTEXT("SlowTaskApplyingModifier_Modifier_Start", "Applying Animation Modifier(s) to Animation Sequence(s)...") ); SlowTaskModifier.MakeDialog(); for (UAnimationModifier* Instance : Modifiers) { checkf(Instance, TEXT("Invalid modifier instance")); SlowTaskModifier.EnterProgressFrame(1.0f, FText::Format(LOCTEXT("SlowTaskApplyingModifier_Modifier_Update", "Applying Animation Modifier {0}..."), Instance->GetClass()->GetDisplayNameText())); bool AppliedOnAny = false; FScopedSlowTask SlowTaskSequences(AnimSequences.Num(), LOCTEXT("SlowTaskApplyingModifier_Sequence_Start", "Applying Animation Modifier to Animation Sequence(s)...")); SlowTaskSequences.MakeDialog(); for (UAnimSequence* AnimSequence : AnimSequences) { FText AnimSequenceName = FText::FromString(IsValid(AnimSequence) ? AnimSequence->GetName() : TEXT("")); SlowTaskSequences.EnterProgressFrame(1.0f, FText::Format(LOCTEXT("SlowTaskApplyingModifier_Sequence_Update", "Applying Animation Modifier to Animation Sequence {0}..."), AnimSequenceName)); ensure(!(Skeleton != nullptr) || AnimSequence->GetSkeleton() == Skeleton); Instance->ApplyToAnimationSequence(AnimSequence); AppliedOnAny = AppliedOnAny || Instance->CanRevert(AnimSequence); } if (AppliedOnAny && Skeleton) { Instance->RemoveLegacyPreviousAppliedModifierOnSkeleton(Skeleton); } } } } void SAnimationModifiersTab::RevertModifiers(const TArray& Modifiers) { bool bRevert = true; TArray AnimSequences; if (AnimationSequence != nullptr) { AnimSequences.Add(AnimationSequence); } else if (Skeleton != nullptr) { // Double check with the user for reverting all modifiers from referenced animation sequences for the skeleton bRevert = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("RevertingSkeletonModifierPopupText", "Are you sure you want to revert the modifiers from all animation sequences referenced by the current skeleton?"), LOCTEXT("RevertingSkeletonModifierPopupTitle", "Are you sure?")) == EAppReturnType::Yes; if ( bRevert) { FindAnimSequencesForSkeleton(AnimSequences); } } if (bRevert) { for (UAnimationModifier* Instance : Modifiers) { checkf(Instance, TEXT("Invalid modifier instance")); for (UAnimSequence* AnimSequence : AnimSequences) { ensure(!(Skeleton != nullptr) || AnimSequence->GetSkeleton() == Skeleton); Instance->RevertFromAnimationSequence(AnimSequence); } if (Skeleton) { // Revert can not fail, thus we can always mark reverted Instance->RemoveLegacyPreviousAppliedModifierOnSkeleton(Skeleton); } } } } void SAnimationModifiersTab::RetrieveModifierData() { ResetModifierData(); if (AssetUserData) { const TArray& ModifierInstances = AssetUserData->GetAnimationModifierInstances(); for (int32 ModifierIndex = 0; ModifierIndex < ModifierInstances.Num(); ++ModifierIndex) { UAnimationModifier* Modifier = ModifierInstances[ModifierIndex]; checkf(Modifier != nullptr, TEXT("Invalid modifier ptr entry")); FModifierListviewItem* Item = new FModifierListviewItem(); Item->Instance = Modifier; Item->Class = Modifier->GetClass(); Item->Index = ModifierIndex; Item->OuterClass = AssetUserData->GetOuter()->GetClass(); Item->OutOfDate = AnimationSequence && !Modifier->IsLatestRevisionApplied(AnimationSequence); ModifierItems.Add(ModifierListviewItem(Item)); // Register a delegate for when a BP is compiled, this so we can refresh the UI and prevent issues with invalid instance data if (Item->Class->ClassGeneratedBy) { checkf(Item->Class->ClassGeneratedBy != nullptr, TEXT("Invalid ClassGeneratedBy value")); UBlueprint* Blueprint = CastChecked(Item->Class->ClassGeneratedBy); Blueprint->OnCompiled().AddSP(this, &SAnimationModifiersTab::OnBlueprintCompiled); DelegateRegisteredBlueprints.Add(Blueprint); } } // Refresh OutOfDate data for modifiers on Skeleton if (Skeleton) { TArray ModifiersToCheck; // Modifiers that are up to date (until current search progress) TArray NextModifiersToCheck; // Double buffer for ModifiersToCheck, store the UpToDate modifiers on this animation ModifiersToCheck.Reserve(ModifierItems.Num()); // Handle backward compatibility // Modifier applied in previous version will not have the applied version stored in asset registry tags // Exclude them in asset registry search and use the legacy OutOfDate check for (TSharedPtr& Item : ModifierItems) { ensure(!Item->OutOfDate); if (Item->Instance->HasLegacyPreviousAppliedModifierOnSkeleton()) { Item->OutOfDate = !Item->Instance->IsLatestRevisionApplied(Skeleton); } else { ModifiersToCheck.Add(Item.Get()); } } if (ModifiersToCheck.IsEmpty()) { return; } NextModifiersToCheck.Reserve(ModifiersToCheck.Num()); // Modifier.OutOfDate = Any(Animation in Skeleton.Animations, Animation=>Modifier.OutOfDateOn(Animation)) FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(FName("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); FARFilter Filter; Filter.ClassPaths.Add(UAnimSequence::StaticClass()->GetClassPathName()); Filter.TagsAndValues.Add(FName{"Skeleton"}) = FAssetData(Skeleton).GetExportTextName(); AssetRegistry.EnumerateAssets(Filter, [&](const FAssetData& Asset) { FString Tag = Asset.GetTagValueRef(UAnimationModifier::AnimationModifiersTag); // First mark all modifiers to check as out of date // Since modifiers omitted from the tag were not applied and thus out of date for (FModifierListviewItem* Item : ModifiersToCheck) { Item->OutOfDate = true; } // Enumerate the current asset's tag // To check the revision of each applied modifier // UpToDate modifiers will be add to NextModifiersToCheck to continue the search in next Asset NextModifiersToCheck.Empty(); FAnimationModifierHelpers::EnumerateAnimationModifierTags(Tag, [&](FStringView Name, FGuid Revision) { FName ModifierName {Name}; auto CompareModifierListViewItemName = [ModifierName](const FModifierListviewItem* Item) { return Item->Instance->GetFName() == ModifierName; }; if (FModifierListviewItem** PtrItem = ModifiersToCheck.FindByPredicate(CompareModifierListViewItemName)) { FModifierListviewItem* Item = *PtrItem; Item->OutOfDate = Item->Instance->GetLatestRevisionGuid() != Revision; if (!Item->OutOfDate) { NextModifiersToCheck.Add(Item); // Only need to continue check modifiers that is update to date } } return true; }); // Swap the double buffer Swap(ModifiersToCheck, NextModifiersToCheck); // Break if all modifiers are out of date return !ModifiersToCheck.IsEmpty(); }); } } } void SAnimationModifiersTab::ResetModifierData() { const int32 NumProcessors = (AssetUserData != nullptr) ? AssetUserData->GetAnimationModifierInstances().Num() : 0; DelegateRegisteredBlueprints.Empty(NumProcessors); ModifierItems.Empty(NumProcessors); for (UBlueprint* Blueprint : DelegateRegisteredBlueprints) { Blueprint->OnCompiled().RemoveAll(this); } } #undef LOCTEXT_NAMESPACE //"SAnimationModifiersTab"