// Copyright Epic Games, Inc. All Rights Reserved. #include "AnimNotifyDetails.h" #include "Animation/Skeleton.h" #include "Widgets/Text/STextBlock.h" #include "Animation/AnimTypes.h" #include "Animation/AnimationAsset.h" #include "DetailWidgetRow.h" #include "IDetailPropertyRow.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "Animation/AnimMontage.h" #include "Animation/EditorNotifyObject.h" #include "AssetSearchBoxUtilPersona.h" #include "IDetailGroup.h" #include "ObjectEditorUtils.h" #include "Animation/AnimNotifies/AnimNotify.h" #include "Widgets/Input/STextComboBox.h" #include "Animation/AnimNotifies/AnimNotifyState.h" namespace UE::AnimNotifyDetails::Private { constexpr TCHAR CategoryDelimiter = TEXT('|'); constexpr FStringView AnimNotifyCategory = TEXTVIEW("AnimNotify"); static const FName AnimNotifyCategoryName = FName(AnimNotifyCategory.Len(), AnimNotifyCategory.GetData()); static const FName AdvancedCategoryName = TEXT("Advanced"); /** * Splits a category name into its parent and leaf category names. Returns a pair of (Parent, Leaf) category names. * If there is no parent category, then the parent name will be empty */ static TPair SplitCategory(const FName CategoryName) { const FString CategoryString = CategoryName.ToString(); const FStringView CategoryView = CategoryString; int32 DelimiterLocation = 0; if (CategoryString.FindLastChar(CategoryDelimiter, DelimiterLocation)) { const FStringView ParentCategoryView = CategoryView.Left(DelimiterLocation); const FStringView LeafCategoryView = CategoryView.RightChop(DelimiterLocation + 1); const FName ParentCategoryName = FName(ParentCategoryView.Len(), ParentCategoryView.GetData()); const FName LeafCategoryName = FName(LeafCategoryView.Len(), LeafCategoryView.GetData()); return MakeTuple(ParentCategoryName, LeafCategoryName); } return MakeTuple(NAME_None, CategoryName); } /** * Strips the leading "Anim Notify" category from the category name, if there is any. * * Helps as a number of anim notifies were authored with the "Anim Notify" category but did not previously display * it correctly. This prevents such notifies from showing an extra category level */ static FName StripAnimNotifyPrefix(const FName CategoryName) { if (CategoryName.IsNone()) { return CategoryName; } const FString CategoryString = CategoryName.ToString(); const FStringView CategoryView = CategoryString; int32 DelimiterLocation = 0; if (CategoryString.StartsWith(AnimNotifyCategory)) { FStringView StrippedCategoryView = CategoryView.RightChop(AnimNotifyCategory.Len() + 1); if (!StrippedCategoryView.IsEmpty() && StrippedCategoryView[0] == CategoryDelimiter) { StrippedCategoryView = StrippedCategoryView.RightChop(1); } if (StrippedCategoryView.IsEmpty()) { return NAME_None; } return FName(StrippedCategoryView.Len(), StrippedCategoryView.GetData()); } return CategoryName; } /** * Adds a series of subgroups for the specified category name, with a new subgroup for each category separated by * a |. Appends the category to the subgroup map to avoid creating categories multiple times */ static IDetailGroup& FindOrAddSubgroup(IDetailCategoryBuilder& Category, const FName CategoryName, TMap& SubgroupMap) { if (IDetailGroup** ExistingGroup = SubgroupMap.Find(CategoryName)) { check(*ExistingGroup); return **ExistingGroup; } const TPair SplitCategoryName = SplitCategory(CategoryName); const FName ParentCategoryName = SplitCategoryName.Key; const FName LeafCategoryName = SplitCategoryName.Value; const FText DisplayName = FObjectEditorUtils::GetCategoryText(LeafCategoryName); IDetailGroup* Subgroup = nullptr; if (ParentCategoryName.IsNone()) { Subgroup = &Category.AddGroup(LeafCategoryName, DisplayName); } else { IDetailGroup& ParentGroup = FindOrAddSubgroup(Category, ParentCategoryName, SubgroupMap); Subgroup = &ParentGroup.AddGroup(LeafCategoryName, DisplayName); } check(Subgroup); SubgroupMap.Add(CategoryName, Subgroup); return *Subgroup; } /** * Adds subgroups for the specified property, but not the property itself */ static void AddSubgroupForProperty(IDetailCategoryBuilder& Category, const FProperty* Property, TMap& SubgroupMap) { if (Property) { const FName CategoryName = StripAnimNotifyPrefix(FObjectEditorUtils::GetCategoryFName(Property)); if (!CategoryName.IsNone()) { FindOrAddSubgroup(Category, CategoryName, SubgroupMap); } } } /** * Adds a subcategory to the specified category with the "Advanced" name. Uses a subgroup map and advanced subgroup * map to avoid creating duplicate categories */ static IDetailGroup& FindOrAddAdvancedCategory(FName CategoryName, TMap& SubgroupMap, TMap& AdvancedSubgroupMap) { if (IDetailGroup** ExistingAdvancedGroup = AdvancedSubgroupMap.Find(CategoryName)) { check(*ExistingAdvancedGroup); return **ExistingAdvancedGroup; } else { IDetailGroup** PropertyGroup = SubgroupMap.Find(CategoryName); check(PropertyGroup); check(*PropertyGroup); static FText AdvancedCategoryText; if (AdvancedCategoryText.IsEmpty()) { AdvancedCategoryText = FObjectEditorUtils::GetCategoryText(AdvancedCategoryName); } IDetailGroup& NewAdvancedGroup = (*PropertyGroup)->AddGroup(AdvancedCategoryName, AdvancedCategoryText); AdvancedSubgroupMap.Add(CategoryName, &NewAdvancedGroup); return NewAdvancedGroup; } } } TSharedRef FAnimNotifyDetails::MakeInstance() { return MakeShareable( new FAnimNotifyDetails ); } void FAnimNotifyDetails::CustomizeDetails( IDetailLayoutBuilder& DetailBuilder ) { const UClass* DetailObjectClass = nullptr; const UClass* BaseClass = DetailBuilder.GetBaseClass(); TArray> SelectedObjects; DetailBuilder.GetObjectsBeingCustomized(SelectedObjects); check(SelectedObjects.Num() > 0); UObject* CommonOuter = nullptr; for (const TWeakObjectPtr& WeakObject : SelectedObjects) { if (const UEditorNotifyObject* EditorObject = Cast(WeakObject.Get())) { UpdateSlotNames(EditorObject->AnimObject); // Find common outer object if (CommonOuter == nullptr) { CommonOuter = EditorObject->AnimObject; } else if (EditorObject->AnimObject && CommonOuter != EditorObject->AnimObject) { CommonOuter = nullptr; } } } TSharedRef EventHandle = DetailBuilder.GetProperty(TEXT("Event")); IDetailCategoryBuilder& EventCategory = DetailBuilder.EditCategory(TEXT("Category")); EventCategory.AddProperty(EventHandle).OverrideResetToDefault(FResetToDefaultOverride::Hide()); // Hide notify objects that aren't set UObject* NotifyObject = nullptr; UObject* NotifyStateObject = nullptr; FString NotifyClassName; TSharedPtr PropertyHandleToUse = nullptr; TSharedRef NotifyPropHandle = DetailBuilder.GetProperty(TEXT("Event.Notify")); TSharedRef NotifyStatePropHandle = DetailBuilder.GetProperty(TEXT("Event.NotifyStateClass")); const FPropertyAccess::Result NotifyPropertyAccessResult = NotifyPropHandle->GetValue(NotifyObject); const FPropertyAccess::Result NotifyStatePropertyAccessResult = NotifyStatePropHandle->GetValue(NotifyStateObject); // Don't want to edit the notify name here. DetailBuilder.HideProperty(TEXT("Event.NotifyName")); IDetailCategoryBuilder& AnimNotifyCategory = DetailBuilder.EditCategory( UE::AnimNotifyDetails::Private::AnimNotifyCategoryName, FText::GetEmpty(), ECategoryPriority::TypeSpecific); const bool bValidNotifyObjects = NotifyPropertyAccessResult != FPropertyAccess::Fail; const bool bValidNotifyStateClasses = NotifyStatePropertyAccessResult != FPropertyAccess::Fail; UObject* NotifyPtr = nullptr; DetailBuilder.HideProperty(TEXT("Event.Notify")); if (bValidNotifyStateClasses) { DetailBuilder.HideProperty(TEXT("Event.NotifyStateClass")); DetailBuilder.HideProperty(TEXT("Event.EndLink")); UClass* NotifyStateClass = nullptr; UAnimNotifyState* NotifyState = nullptr; // Find common notify state class, only valid if all of the selected objects are of the same notify type for (const TWeakObjectPtr& WeakObject : SelectedObjects) { if (const UEditorNotifyObject* EditorObject = Cast(WeakObject.Get())) { NotifyState = EditorObject->Event.NotifyStateClass.Get(); if (NotifyStateClass == nullptr) { if (NotifyState) { NotifyStateClass = NotifyState->GetClass(); } else { break; } } else if (NotifyState && NotifyState->GetClass() != NotifyStateClass) { NotifyStateClass = nullptr; break; } } } NotifyPtr = NotifyState; PropertyHandleToUse = NotifyStatePropHandle; DetailObjectClass = NotifyStateClass; } // In case there wasn't a valid AnimNotifyState class try to find an AnimNotify class instead if (bValidNotifyObjects && NotifyPtr == nullptr) { UClass* NotifyClass = nullptr; UAnimNotify* Notify = nullptr; // Find common notify class, only valid if all of the selected objects are of the same notify type for (const TWeakObjectPtr& WeakObject : SelectedObjects) { if (const UEditorNotifyObject* EditorObject = Cast(WeakObject.Get())) { Notify = EditorObject->Event.Notify.Get(); if (NotifyClass == nullptr) { if (Notify) { NotifyClass = Notify->GetClass(); } else { break; } } else if (Notify && Notify->GetClass() != NotifyClass) { NotifyClass = nullptr; break; } } } NotifyPtr = Notify; PropertyHandleToUse = NotifyPropHandle; DetailObjectClass = NotifyClass; } // This means we ended up in a situation where there is no valid notify or notify-state class - so why are we trying to customize a notify //check(NotifyPtr != nullptr); UAnimMontage* CurrentMontage = Cast(CommonOuter); // If we have a montage, and it has slots (which it should have) generate custom link properties if(CurrentMontage && CurrentMontage->SlotAnimTracks.Num() > 0) { CustomizeLinkProperties(DetailBuilder, EventHandle, CurrentMontage); } else { // No montage, hide link properties HideLinkProperties(DetailBuilder, EventHandle); } TMap SubgroupMap; TMap AdvancedSubgroupMap; TArray> PropertyHandles; TArray> AdvancedPropertyHandles; // Customizations do not run for instanced properties, so we have to resolve the properties and then // customize them here instead. if(PropertyHandleToUse->IsValidHandle()) { uint32 NumChildren = 0; PropertyHandleToUse->GetNumChildren(NumChildren); if(NumChildren > 0) { TSharedPtr BaseHandle = PropertyHandleToUse->GetChildHandle(0); DetailBuilder.HideProperty(PropertyHandleToUse); BaseHandle->GetNumChildren(NumChildren); DetailBuilder.HideProperty(BaseHandle); for(uint32 ChildIdx = 0 ; ChildIdx < NumChildren ; ++ChildIdx) { TSharedPtr NotifyProperty = BaseHandle->GetChildHandle(ChildIdx); FProperty* Prop = NotifyProperty->GetProperty(); if(Prop && !Prop->HasAnyPropertyFlags(CPF_DisableEditOnInstance)) { if(!CustomizeProperty(AnimNotifyCategory, NotifyPtr, NotifyProperty)) { // Add our subgroups first, so we can make sure they are sorted before the normal properties UE::AnimNotifyDetails::Private::AddSubgroupForProperty(AnimNotifyCategory, Prop, SubgroupMap); if (Prop->HasAnyPropertyFlags(CPF_AdvancedDisplay)) { AdvancedPropertyHandles.Add(NotifyProperty); } else { PropertyHandles.Add(NotifyProperty); } } } } } } for (TSharedPtr PropertyHandle : PropertyHandles) { check(PropertyHandle); FProperty* Property = PropertyHandle->GetProperty(); check(Property); const FName PropertyGroupName = UE::AnimNotifyDetails::Private::StripAnimNotifyPrefix(FObjectEditorUtils::GetCategoryFName(Property)); if (!PropertyGroupName.IsNone()) { IDetailGroup** PropertyGroup = SubgroupMap.Find(PropertyGroupName); check(PropertyGroup); check(*PropertyGroup); (*PropertyGroup)->AddPropertyRow(PropertyHandle.ToSharedRef()); } else { AnimNotifyCategory.AddProperty(PropertyHandle); } } // Iterate over all of the advanced properties last so we can their advanced categories as needed, to sort after the // normal properties for (TSharedPtr PropertyHandle : AdvancedPropertyHandles) { check(PropertyHandle); FProperty* Property = PropertyHandle->GetProperty(); check(Property); const FName PropertyGroupName = UE::AnimNotifyDetails::Private::StripAnimNotifyPrefix(FObjectEditorUtils::GetCategoryFName(Property)); if (!PropertyGroupName.IsNone()) { IDetailGroup& AdvancedCategory = UE::AnimNotifyDetails::Private::FindOrAddAdvancedCategory( PropertyGroupName, SubgroupMap, AdvancedSubgroupMap); AdvancedCategory.AddPropertyRow(PropertyHandle.ToSharedRef()); } else { // If we're just adding the property to the top level category, then AddProperty will automatically handle // setting whether it's advanced or not AnimNotifyCategory.AddProperty(PropertyHandle); } } struct FPropVisPair { const TCHAR* NotifyName; TAttribute Visibility; }; TriggerFilterModeHandle = DetailBuilder.GetProperty(TEXT("Event.NotifyFilterType")); FPropVisPair TriggerSettingNames[] = { { TEXT("Event.NotifyTriggerChance"), Cast(NotifyPtr) == nullptr ? EVisibility::Visible : EVisibility::Hidden } , { TEXT("Event.bTriggerOnDedicatedServer"), TAttribute(EVisibility::Visible) } , { TEXT("Event.bTriggerOnFollower"), TAttribute(EVisibility::Visible) } , { TEXT("Event.NotifyFilterType"), TAttribute(EVisibility::Visible) } , { TEXT("Event.NotifyFilterLOD"), TAttribute(this, &FAnimNotifyDetails::VisibilityForLODFilterMode) } }; IDetailCategoryBuilder& TriggerSettingCategory = DetailBuilder.EditCategory(TEXT("Trigger Settings"), FText::GetEmpty(), ECategoryPriority::TypeSpecific); for (FPropVisPair& NotifyPair : TriggerSettingNames) { TSharedRef NotifyPropertyHandle = DetailBuilder.GetProperty(NotifyPair.NotifyName); DetailBuilder.HideProperty(NotifyPropertyHandle); TriggerSettingCategory.AddProperty(NotifyPropertyHandle).Visibility(NotifyPair.Visibility); } } EVisibility FAnimNotifyDetails::VisibilityForLODFilterMode() const { uint8 FilterModeValue = 0; FPropertyAccess::Result Ret = TriggerFilterModeHandle.Get()->GetValue(FilterModeValue); if (Ret == FPropertyAccess::Result::Success) { return (FilterModeValue == ENotifyFilterType::LOD) ? EVisibility::Visible : EVisibility::Hidden; } return EVisibility::Hidden; //Hidden if we get fail or MultipleValues from the property } void FAnimNotifyDetails::AddBoneNameProperty(IDetailCategoryBuilder& CategoryBuilder, UObject* Notify, TSharedPtr Property) { int32 PropIndex = NameProperties.Num(); if(Notify && Property->IsValidHandle()) { NameProperties.Add(Property); // get all the possible suggestions for the bones and sockets. if(const UAnimationAsset* AnimAsset = Cast(Notify->GetOuter())) { if(const USkeleton* Skeleton = AnimAsset->GetSkeleton()) { CategoryBuilder.AddProperty(Property.ToSharedRef()) .CustomWidget() .NameContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0, 1, 0, 1)) [ SNew(STextBlock) .Text(Property->GetPropertyDisplayName()) .Font(FAppStyle::GetFontStyle(TEXT("PropertyWindow.NormalFont"))) ] ] .ValueContent() [ SNew(SAssetSearchBoxForBones, Skeleton, Property) .IncludeSocketsForSuggestions(true) .MustMatchPossibleSuggestions(false) .HintText(NSLOCTEXT("AnimNotifyDetails", "Hint Text", "Bone Name...")) .OnTextCommitted(this, &FAnimNotifyDetails::OnSearchBoxCommitted, PropIndex) ]; } } } } void FAnimNotifyDetails::AddCurveNameProperty(IDetailCategoryBuilder& CategoryBuilder, UObject* Notify, TSharedPtr Property) { int32 PropIndex = NameProperties.Num(); if(Notify && Property->IsValidHandle()) { NameProperties.Add(Property); if(const UAnimationAsset* AnimAsset = Cast(Notify->GetOuter())) { if(const USkeleton* Skeleton = AnimAsset->GetSkeleton()) { CategoryBuilder.AddProperty(Property.ToSharedRef()) .CustomWidget() .NameContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(2, 1, 0, 1)) [ SNew(STextBlock) .Text(Property->GetPropertyDisplayName()) .Font(FAppStyle::GetFontStyle(TEXT("PropertyWindow.NormalFont"))) ] ] .ValueContent() [ SNew(SAssetSearchBoxForCurves, Skeleton, Property) .IncludeSocketsForSuggestions(true) .MustMatchPossibleSuggestions(true) .HintText(NSLOCTEXT("AnimNotifyDetails", "Curve Name Hint Text", "Curve Name...")) .OnTextCommitted(this, &FAnimNotifyDetails::OnSearchBoxCommitted, PropIndex) ]; } } } } void FAnimNotifyDetails::OnSearchBoxCommitted(const FText& InSearchText, ETextCommit::Type CommitInfo, int32 PropertyIndex ) { NameProperties[PropertyIndex]->SetValue( InSearchText.ToString() ); } void FAnimNotifyDetails::ClearInstancedSelectionDropDown(IDetailCategoryBuilder& CategoryBuilder, TSharedRef PropHandle, bool bShowChildren /*= true*/) { IDetailPropertyRow& PropRow = CategoryBuilder.AddProperty(PropHandle); PropRow .OverrideResetToDefault(FResetToDefaultOverride::Hide()) .CustomWidget(bShowChildren) .NameContent() [ PropHandle->CreatePropertyNameWidget() ] .ValueContent() [ SNullWidget::NullWidget ]; } void FAnimNotifyDetails::CustomizeLinkProperties(IDetailLayoutBuilder& Builder, TSharedRef NotifyProperty, UAnimSequenceBase* AnimSequenceBase) { uint32 NumChildProperties = 0; NotifyProperty->GetNumChildren(NumChildProperties); if(NumChildProperties > 0) { IDetailCategoryBuilder& LinkCategory = Builder.EditCategory(TEXT("AnimLink")); for(uint32 ChildIdx = 0 ; ChildIdx < NumChildProperties ; ++ChildIdx) { TSharedPtr ChildHandle = NotifyProperty->GetChildHandle(ChildIdx); FString OuterFieldType = ChildHandle->GetProperty()->GetOwnerVariant().GetName(); if(ChildHandle->GetProperty()->GetName() == GET_MEMBER_NAME_CHECKED(FAnimNotifyEvent, EndLink).ToString() || OuterFieldType == FString(TEXT("AnimLinkableElement"))) { // If we get a slot index property replace it with a dropdown showing the names of the // slots, as the indices are hidden from the user. if(ChildHandle->GetProperty()->GetName() == TEXT("SlotIndex")) { int32 SlotIdx = INDEX_NONE; ChildHandle->GetValue(SlotIdx); TSharedPtr InitialItem = nullptr; if (SlotNameItems.IsValidIndex(SlotIdx)) { InitialItem = SlotNameItems[SlotIdx]; } else { InitialItem = MakeShared(TEXT("Multiple Values")); } LinkCategory.AddProperty(ChildHandle) .CustomWidget() .NameContent() [ ChildHandle->CreatePropertyNameWidget(NSLOCTEXT("NotifyDetails", "SlotIndexName", "Slot")) ] .ValueContent() [ SNew(STextComboBox) .OptionsSource(&SlotNameItems) .OnSelectionChanged(this, &FAnimNotifyDetails::OnSlotSelected, ChildHandle) .OnComboBoxOpening(this, &FAnimNotifyDetails::UpdateSlotNames, AnimSequenceBase) .InitiallySelectedItem(InitialItem) .Font(Builder.GetDetailFont()) ]; } else { LinkCategory.AddProperty(ChildHandle); } } } } } void FAnimNotifyDetails::HideLinkProperties(IDetailLayoutBuilder& Builder, TSharedRef NotifyProperty) { uint32 NumChildProperties = 0; NotifyProperty->GetNumChildren(NumChildProperties); if(NumChildProperties > 0) { for(uint32 ChildIdx = 0 ; ChildIdx < NumChildProperties ; ++ChildIdx) { TSharedPtr ChildHandle = NotifyProperty->GetChildHandle(ChildIdx); FString OuterFieldType = ChildHandle->GetProperty()->GetOwnerVariant().GetName(); if(ChildHandle->GetProperty()->GetName() == GET_MEMBER_NAME_CHECKED(FAnimNotifyEvent, EndLink).ToString() || OuterFieldType == FString(TEXT("AnimLinkableElement"))) { Builder.HideProperty(ChildHandle); } } } } bool FAnimNotifyDetails::CustomizeProperty(IDetailCategoryBuilder& CategoryBuilder, UObject* Notify, TSharedPtr Property) { TFunction)> FixBoneNamePropertyRecurse; FixBoneNamePropertyRecurse = [this, &FixBoneNamePropertyRecurse, &CategoryBuilder, &Notify](const TSharedPtr& InPropertyHandle) { const bool bHasExpandMeta = InPropertyHandle->GetBoolMetaData(TEXT("AnimNotifyExpand")); bool bParentIsObjectPtr = false; TSharedPtr ParentProp = InPropertyHandle->GetParentHandle(); if (ParentProp.IsValid() && ParentProp->IsValidHandle()) { bParentIsObjectPtr = (CastField(ParentProp->GetProperty()) != nullptr); } // Recurse into Object Ptrs or properties with AnimNotifyExpand if (bParentIsObjectPtr || bHasExpandMeta) { IDetailLayoutBuilder& LayoutBuilder = CategoryBuilder.GetParentLayout(); LayoutBuilder.HideProperty(InPropertyHandle); uint32 NumChildren = 0; InPropertyHandle->GetNumChildren(NumChildren); for (uint32 i = 0; i < NumChildren; ++i) { TSharedPtr ChildHandle = InPropertyHandle->GetChildHandle(i); FixBoneNamePropertyRecurse(ChildHandle); } } else if (InPropertyHandle->GetBoolMetaData(TEXT("AnimNotifyBoneName"))) { // Convert this property to a bone name property AddBoneNameProperty(CategoryBuilder, Notify, InPropertyHandle); } else { CategoryBuilder.AddProperty(InPropertyHandle); } }; if(Notify && Notify->GetClass() && Property->IsValidHandle()) { FString ClassName = Notify->GetClass()->GetName(); FString PropertyName = Property->GetProperty()->GetName(); bool bIsBoneName = Property->GetBoolMetaData(TEXT("AnimNotifyBoneName")); if(ClassName.Find(TEXT("AnimNotify_PlayParticleEffect")) != INDEX_NONE && PropertyName == TEXT("SocketName")) { AddBoneNameProperty(CategoryBuilder, Notify, Property); return true; } else if(ClassName.Find(TEXT("AnimNotifyState_TimedParticleEffect")) != INDEX_NONE && PropertyName == TEXT("SocketName")) { AddBoneNameProperty(CategoryBuilder, Notify, Property); return true; } else if(ClassName.Find(TEXT("AnimNotify_PlaySound")) != INDEX_NONE && PropertyName == TEXT("AttachName")) { AddBoneNameProperty(CategoryBuilder, Notify, Property); return true; } else if (ClassName.Find(TEXT("_SoundLibrary")) != INDEX_NONE && PropertyName == TEXT("SoundContext")) { CategoryBuilder.AddProperty(Property); FixBoneNamePropertyRecurse(Property); return true; } else if (ClassName.Find(TEXT("AnimNotifyState_Trail")) != INDEX_NONE) { if(PropertyName == TEXT("FirstSocketName") || PropertyName == TEXT("SecondSocketName")) { AddBoneNameProperty(CategoryBuilder, Notify, Property); return true; } else if(PropertyName == TEXT("WidthScaleCurve")) { AddCurveNameProperty(CategoryBuilder, Notify, Property); return true; } } else if (bIsBoneName) { AddBoneNameProperty(CategoryBuilder, Notify, Property); return true; } } return false; } void FAnimNotifyDetails::UpdateSlotNames(UAnimSequenceBase* AnimObject) { if(UAnimMontage* MontageObj = Cast(AnimObject)) { for(FSlotAnimationTrack& Slot : MontageObj->SlotAnimTracks) { if(!SlotNameItems.ContainsByPredicate([&Slot](TSharedPtr& Item){return Slot.SlotName.ToString() == *Item;})) { SlotNameItems.Add(MakeShareable(new FString(*Slot.SlotName.ToString()))); } } } } void FAnimNotifyDetails::OnSlotSelected(TSharedPtr SlotName, ESelectInfo::Type SelectInfo, TSharedPtr Property) { if(SelectInfo != ESelectInfo::Direct && Property->IsValidHandle()) { int32 NewIndex = SlotNameItems.Find(SlotName); if(NewIndex != INDEX_NONE) { Property->SetValue(NewIndex); } } }