Files
UnrealEngine/Engine/Source/Editor/DetailCustomizations/Private/ComponentReferenceCustomization.cpp
2025-05-18 13:04:45 +08:00

707 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ComponentReferenceCustomization.h"
#include "ActorPickerMode.h"
#include "Brushes/SlateNoResource.h"
#include "Components/ActorComponent.h"
#include "Components/SceneComponent.h"
#include "Containers/UnrealString.h"
#include "Delegates/Delegate.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Engine/EngineTypes.h"
#include "Engine/LevelScriptActor.h"
#include "Fonts/SlateFontInfo.h"
#include "GameFramework/Actor.h"
#include "HAL/PlatformCrt.h"
#include "IDetailChildrenBuilder.h"
#include "IDetailPropertyRow.h"
#include "Internationalization/Internationalization.h"
#include "Kismet2/ComponentEditorUtils.h"
#include "Layout/BasicLayoutWidgetSlot.h"
#include "Layout/Margin.h"
#include "Math/Color.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Attribute.h"
#include "PropertyCustomizationHelpers.h"
#include "PropertyHandle.h"
#include "SlotBase.h"
#include "Styling/AppStyle.h"
#include "Styling/SlateColor.h"
#include "Styling/SlateIconFinder.h"
#include "Templates/Casts.h"
#include "Types/SlateEnums.h"
#include "UObject/Class.h"
#include "UObject/Field.h"
#include "UObject/GarbageCollection.h"
#include "UObject/NameTypes.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/ObjectPtr.h"
#include "UObject/PropertyPortFlags.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/UObjectIterator.h"
#include "UObject/UnrealType.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Layout/SWidgetSwitcher.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SNullWidget.h"
#include "Widgets/Text/STextBlock.h"
static const FName NAME_AllowAnyActor = "AllowAnyActor";
static const FName NAME_AllowedClasses = "AllowedClasses";
static const FName NAME_DisallowedClasses = "DisallowedClasses";
static const FName NAME_UseComponentPicker = "UseComponentPicker";
#define LOCTEXT_NAMESPACE "ComponentReferenceCustomization"
TSharedRef<IPropertyTypeCustomization> FComponentReferenceCustomization::MakeInstance()
{
return MakeShareable( new FComponentReferenceCustomization);
}
void FComponentReferenceCustomization::CustomizeHeader(TSharedRef<IPropertyHandle> InPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
PropertyHandle = InPropertyHandle;
CachedComponent.Reset();
CachedFirstOuterActor.Reset();
CachedPropertyAccess = FPropertyAccess::Fail;
bAllowClear = false;
bAllowAnyActor = false;
bUseComponentPicker = PropertyHandle->HasMetaData(NAME_UseComponentPicker);
bIsSoftReference = false;
if (bUseComponentPicker)
{
FProperty* Property = InPropertyHandle->GetProperty();
check(CastField<FStructProperty>(Property) &&
(FComponentReference::StaticStruct() == CastFieldChecked<const FStructProperty>(Property)->Struct ||
FSoftComponentReference::StaticStruct() == CastFieldChecked<const FStructProperty>(Property)->Struct));
bAllowClear = !(InPropertyHandle->GetMetaDataProperty()->PropertyFlags & CPF_NoClear);
bAllowAnyActor = InPropertyHandle->HasMetaData(NAME_AllowAnyActor);
bIsSoftReference = FSoftComponentReference::StaticStruct() == CastFieldChecked<const FStructProperty>(Property)->Struct;
BuildClassFilters();
BuildComboBox();
InPropertyHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FComponentReferenceCustomization::OnPropertyValueChanged));
UpdateCachedValues(/*bResetValueIfInvalid*/ false);
HeaderRow.NameContent()
[
InPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
ComponentComboButton.ToSharedRef()
]
.IsEnabled(MakeAttributeSP(this, &FComponentReferenceCustomization::CanEdit));
}
else
{
HeaderRow.NameContent()
[
InPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
InPropertyHandle->CreatePropertyValueWidget()
]
.IsEnabled(MakeAttributeSP(this, &FComponentReferenceCustomization::CanEdit));
}
}
void FComponentReferenceCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
uint32 NumberOfChild;
if (InStructPropertyHandle->GetNumChildren(NumberOfChild) == FPropertyAccess::Success)
{
for (uint32 Index = 0; Index < NumberOfChild; ++Index)
{
TSharedRef<IPropertyHandle> ChildPropertyHandle = InStructPropertyHandle->GetChildHandle(Index).ToSharedRef();
if (bUseComponentPicker)
{
ChildPropertyHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FComponentReferenceCustomization::OnPropertyValueChanged));
StructBuilder.AddProperty(ChildPropertyHandle)
.ShowPropertyButtons(true)
.IsEnabled(MakeAttributeSP(this, &FComponentReferenceCustomization::CanEditChildren));
}
else
{
StructBuilder.AddProperty(ChildPropertyHandle)
.ShowPropertyButtons(true)
.IsEnabled(MakeAttributeSP(this, &FComponentReferenceCustomization::CanEditChildren));
}
}
}
}
void FComponentReferenceCustomization::BuildClassFilters()
{
auto AddToClassFilters = [this](const UClass* Class, TArray<const UClass*>& ActorList, TArray<const UClass*>& ComponentList)
{
if (bAllowAnyActor && Class->IsChildOf(AActor::StaticClass()))
{
ActorList.Add(Class);
}
else if (Class->IsChildOf(UActorComponent::StaticClass()))
{
ComponentList.Add(Class);
}
};
auto ParseClassFilters = [this, AddToClassFilters](const FString& MetaDataString, TArray<const UClass*>& ActorList, TArray<const UClass*>& ComponentList)
{
if (!MetaDataString.IsEmpty())
{
TArray<FString> ClassFilterNames;
MetaDataString.ParseIntoArrayWS(ClassFilterNames, TEXT(","), true);
for (const FString& ClassName : ClassFilterNames)
{
UClass* Class = UClass::TryFindTypeSlow<UClass>(ClassName);
if (!Class)
{
Class = LoadObject<UClass>(nullptr, *ClassName);
}
if (Class)
{
// If the class is an interface, expand it to be all classes in memory that implement the class.
if (Class->HasAnyClassFlags(CLASS_Interface))
{
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* const ClassWithInterface = (*ClassIt);
if (ClassWithInterface->ImplementsInterface(Class))
{
AddToClassFilters(ClassWithInterface, ActorList, ComponentList);
}
}
}
else
{
AddToClassFilters(Class, ActorList, ComponentList);
}
}
}
}
};
// Account for the allowed classes specified in the property metadata
const FString& AllowedClassesFilterString = PropertyHandle->GetMetaData(NAME_AllowedClasses);
ParseClassFilters(AllowedClassesFilterString, AllowedActorClassFilters, AllowedComponentClassFilters);
const FString& DisallowedClassesFilterString = PropertyHandle->GetMetaData(NAME_DisallowedClasses);
ParseClassFilters(DisallowedClassesFilterString, DisallowedActorClassFilters, DisallowedComponentClassFilters);
}
void FComponentReferenceCustomization::BuildComboBox()
{
TAttribute<bool> IsEnabledAttribute(this, &FComponentReferenceCustomization::CanEdit);
TAttribute<FText> TooltipAttribute;
if (PropertyHandle->GetMetaDataProperty()->HasAnyPropertyFlags(CPF_EditConst | CPF_DisableEditOnTemplate))
{
TArray<UObject*> ObjectList;
PropertyHandle->GetOuterObjects(ObjectList);
// If there is no objects, that means we must have a struct asset managing this property
if (ObjectList.Num() == 0)
{
IsEnabledAttribute.Set(false);
TooltipAttribute.Set(LOCTEXT("VariableHasDisableEditOnTemplate", "Editing this value in structure's defaults is not allowed"));
}
else
{
// Go through all the found objects and see if any are a CDO, we can't set an actor in a CDO default.
for (UObject* Obj : ObjectList)
{
if (Obj->IsTemplate() && !Obj->IsA<ALevelScriptActor>())
{
IsEnabledAttribute.Set(false);
TooltipAttribute.Set(LOCTEXT("VariableHasDisableEditOnTemplateTooltip", "Editing this value in a Class Default Object is not allowed"));
break;
}
}
}
}
TSharedRef<SVerticalBox> ObjectContent = SNew(SVerticalBox);
if (bAllowAnyActor)
{
ObjectContent->AddSlot()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(this, &FComponentReferenceCustomization::GetActorIcon)
]
+ SHorizontalBox::Slot()
.FillWidth(1)
.VAlign(VAlign_Center)
[
// Show the name of the asset or actor
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(this, &FComponentReferenceCustomization::OnGetActorName)
]
];
}
ObjectContent->AddSlot()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(this, &FComponentReferenceCustomization::GetComponentIcon)
]
+ SHorizontalBox::Slot()
.FillWidth(1)
.VAlign(VAlign_Center)
[
// Show the name of the asset or actor
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(this, &FComponentReferenceCustomization::OnGetComponentName)
]
];
TSharedRef<SHorizontalBox> ComboButtonContent = SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(this, &FComponentReferenceCustomization::GetStatusIcon)
]
+ SHorizontalBox::Slot()
.FillWidth(1)
.VAlign(VAlign_Center)
[
ObjectContent
];
ComponentComboButton = SNew(SComboButton)
.ToolTipText(TooltipAttribute)
.ButtonStyle(FAppStyle::Get(), "PropertyEditor.AssetComboStyle")
.ForegroundColor(FAppStyle::GetColor("PropertyEditor.AssetName.ColorAndOpacity"))
.OnGetMenuContent(this, &FComponentReferenceCustomization::OnGetMenuContent)
.OnMenuOpenChanged(this, &FComponentReferenceCustomization::OnMenuOpenChanged)
.IsEnabled(IsEnabledAttribute)
.ContentPadding(2.0f)
.ButtonContent()
[
SNew(SWidgetSwitcher)
.WidgetIndex(this, &FComponentReferenceCustomization::OnGetComboContentWidgetIndex)
+ SWidgetSwitcher::Slot()
[
SNew(STextBlock)
.Text(LOCTEXT("MultipleValuesText", "<multiple values>"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
+ SWidgetSwitcher::Slot()
[
ComboButtonContent
]
];
}
AActor* FComponentReferenceCustomization::GetFirstOuterActor() const
{
TArray<UObject*> ObjectList;
PropertyHandle->GetOuterObjects(ObjectList);
for (UObject* Obj : ObjectList)
{
while (Obj)
{
if (AActor* Actor = Cast<AActor>(Obj))
{
return Actor;
}
if (UActorComponent* Component = Cast<UActorComponent>(Obj))
{
if (Component->GetOwner())
{
return Component->GetOwner();
}
}
Obj = Obj->GetOuter();
}
}
return nullptr;
}
void FComponentReferenceCustomization::SetValue(const FComponentReference& Value)
{
ComponentComboButton->SetIsOpen(false);
const bool bIsEmpty = Value == FComponentReference();
const bool bAllowedToSetBasedOnFilter = IsComponentReferenceValid(Value);
if (bIsEmpty || bAllowedToSetBasedOnFilter)
{
FString TextValue;
if (bIsSoftReference)
{
FSoftComponentReference SoftValue;
if (Value.OtherActor.IsValid())
{
SoftValue.OtherActor = Value.OtherActor.Get();
SoftValue.ComponentProperty = Value.ComponentProperty;
SoftValue.PathToComponent = Value.PathToComponent;
}
CastFieldChecked<const FStructProperty>(PropertyHandle->GetProperty())->Struct->ExportText(TextValue, &SoftValue, &SoftValue, nullptr, EPropertyPortFlags::PPF_None, nullptr);
ensure(PropertyHandle->SetValueFromFormattedString(TextValue) == FPropertyAccess::Result::Success);
}
else
{
CastFieldChecked<const FStructProperty>(PropertyHandle->GetProperty())->Struct->ExportText(TextValue, &Value, &Value, nullptr, EPropertyPortFlags::PPF_None, nullptr);
ensure(PropertyHandle->SetValueFromFormattedString(TextValue) == FPropertyAccess::Result::Success);
}
}
}
FPropertyAccess::Result FComponentReferenceCustomization::GetValue(FComponentReference& OutValue) const
{
// Potentially accessing the value while garbage collecting or saving the package could trigger a crash.
// so we fail to get the value when that is occurring.
if (GIsSavingPackage || IsGarbageCollecting())
{
return FPropertyAccess::Fail;
}
FPropertyAccess::Result Result = FPropertyAccess::Fail;
if (PropertyHandle.IsValid() && PropertyHandle->IsValidHandle())
{
TArray<void*> RawData;
PropertyHandle->AccessRawData(RawData);
UActorComponent* CurrentComponent = nullptr;
AActor* CurrentActor = CachedFirstOuterActor.Get();
for (const void* RawPtr : RawData)
{
if (RawPtr)
{
FComponentReference ThisReference;
if (bIsSoftReference)
{
FSoftComponentReference SoftReference = *reinterpret_cast<const FSoftComponentReference*>(RawPtr);
if (SoftReference.OtherActor.IsValid())
{
ThisReference.OtherActor = SoftReference.OtherActor.Get();
ThisReference.ComponentProperty = SoftReference.ComponentProperty;
ThisReference.PathToComponent = SoftReference.PathToComponent;
}
}
else
{
ThisReference = *reinterpret_cast<const FComponentReference*>(RawPtr);
}
if (Result == FPropertyAccess::Success)
{
if (ThisReference.GetComponent(CurrentActor) != CurrentComponent)
{
Result = FPropertyAccess::MultipleValues;
break;
}
}
else
{
OutValue = ThisReference;
CurrentComponent = OutValue.GetComponent(CurrentActor);
Result = FPropertyAccess::Success;
}
}
else if (Result == FPropertyAccess::Success)
{
Result = FPropertyAccess::MultipleValues;
break;
}
}
}
return Result;
}
bool FComponentReferenceCustomization::IsComponentReferenceValid(const FComponentReference& Value) const
{
if (!bAllowAnyActor && Value.OtherActor.IsValid())
{
return false;
}
AActor* CachedActor = CachedFirstOuterActor.Get();
if (UActorComponent* NewComponent = Value.GetComponent(CachedActor))
{
if (!IsFilteredComponent(NewComponent))
{
return false;
}
if (bAllowAnyActor)
{
if (NewComponent->GetOwner() == nullptr)
{
return false;
}
TArray<UObject*> ObjectList;
PropertyHandle->GetOuterObjects(ObjectList);
// Is the Outer object in the same world/level
for (UObject* Obj : ObjectList)
{
AActor* Actor = Cast<AActor>(Obj);
if (Actor == nullptr)
{
if (UActorComponent* ActorComponent = Cast<UActorComponent>(Obj))
{
Actor = ActorComponent->GetOwner();
}
}
if (Actor)
{
if (NewComponent->GetOwner()->GetLevel() != Actor->GetLevel())
{
return false;
}
}
}
}
}
return true;
}
void FComponentReferenceCustomization::OnPropertyValueChanged()
{
UpdateCachedValues(/*bResetValueIfInvalid*/ true);
}
void FComponentReferenceCustomization::UpdateCachedValues(bool bResetValueIfInvalid)
{
CachedComponent.Reset();
CachedFirstOuterActor = GetFirstOuterActor();
FComponentReference TmpComponentReference;
CachedPropertyAccess = GetValue(TmpComponentReference);
if (CachedPropertyAccess != FPropertyAccess::Success)
{
return;
}
// If the reference is null, we don't want to update CachedComponent (the further code would
// otherwise try to set it to the root component of our actor)
if (TmpComponentReference.PathToComponent.IsEmpty()
&& TmpComponentReference.ComponentProperty == NAME_None
&& TmpComponentReference.OverrideComponent.IsExplicitlyNull())
{
return;
}
CachedComponent = TmpComponentReference.GetComponent(CachedFirstOuterActor.Get());
if (!IsComponentReferenceValid(TmpComponentReference))
{
CachedComponent.Reset();
if (bResetValueIfInvalid && !(TmpComponentReference == FComponentReference()))
{
SetValue(FComponentReference());
}
}
}
int32 FComponentReferenceCustomization::OnGetComboContentWidgetIndex() const
{
switch (CachedPropertyAccess)
{
case FPropertyAccess::MultipleValues: return 0;
case FPropertyAccess::Success:
default:
return 1;
}
}
bool FComponentReferenceCustomization::CanEdit() const
{
return PropertyHandle.IsValid() ? !PropertyHandle->IsEditConst() : true;
}
bool FComponentReferenceCustomization::CanEditChildren() const
{
return CanEdit() && (!bUseComponentPicker || !CachedFirstOuterActor.IsValid());
}
const FSlateBrush* FComponentReferenceCustomization::GetActorIcon() const
{
if (UActorComponent* Component = CachedComponent.Get())
{
if (AActor* Owner = Component->GetOwner())
{
return FSlateIconFinder::FindIconBrushForClass(Owner->GetClass());
}
}
return FSlateIconFinder::FindIconBrushForClass(AActor::StaticClass());;
}
FText FComponentReferenceCustomization::OnGetActorName() const
{
if (UActorComponent* Component = CachedComponent.Get())
{
if (AActor* Owner = Component->GetOwner())
{
return FText::AsCultureInvariant(Owner->GetActorLabel());
}
}
return LOCTEXT("NoActor", "None");
}
const FSlateBrush* FComponentReferenceCustomization::GetComponentIcon() const
{
if (const UActorComponent* ActorComponent = CachedComponent.Get())
{
return FSlateIconFinder::FindIconBrushForClass(ActorComponent->GetClass());
}
return FSlateIconFinder::FindIconBrushForClass(UActorComponent::StaticClass());
}
FText FComponentReferenceCustomization::OnGetComponentName() const
{
if (CachedPropertyAccess == FPropertyAccess::Success)
{
if (UActorComponent* ActorComponent = CachedComponent.Get())
{
const FName ComponentName = FComponentEditorUtils::FindVariableNameGivenComponentInstance(ActorComponent);
const bool bIsArrayVariable = !ComponentName.IsNone() && ActorComponent->GetOwner() != nullptr && FindFProperty<FArrayProperty>(ActorComponent->GetOwner()->GetClass(), ComponentName);
if (!ComponentName.IsNone() && !bIsArrayVariable)
{
return FText::FromName(ComponentName);
}
return FText::AsCultureInvariant(ActorComponent->GetName());
}
}
else if (CachedPropertyAccess == FPropertyAccess::MultipleValues)
{
return LOCTEXT("MultipleValues", "Multiple Values");
}
return LOCTEXT("NoComponent", "None");
}
const FSlateBrush* FComponentReferenceCustomization::GetStatusIcon() const
{
static FSlateNoResource EmptyBrush = FSlateNoResource();
if (CachedPropertyAccess == FPropertyAccess::Fail)
{
return FAppStyle::GetBrush("Icons.Error");
}
return &EmptyBrush;
}
TSharedRef<SWidget> FComponentReferenceCustomization::OnGetMenuContent()
{
UActorComponent* InitialComponent = CachedComponent.Get();
return PropertyCustomizationHelpers::MakeComponentPickerWithMenu(InitialComponent
, bAllowClear
, FOnShouldFilterActor::CreateSP(this, &FComponentReferenceCustomization::IsFilteredActor)
, FOnShouldFilterComponent::CreateSP(this, &FComponentReferenceCustomization::IsFilteredComponent)
, FOnComponentSelected::CreateSP(this, &FComponentReferenceCustomization::OnComponentSelected)
, FSimpleDelegate::CreateSP(this, &FComponentReferenceCustomization::CloseComboButton));
}
void FComponentReferenceCustomization::OnMenuOpenChanged(bool bOpen)
{
if (!bOpen)
{
ComponentComboButton->SetMenuContent(SNullWidget::NullWidget);
}
}
bool FComponentReferenceCustomization::IsFilteredActor(const AActor* const Actor) const
{
return bAllowAnyActor || Actor == CachedFirstOuterActor.Get();
}
bool FComponentReferenceCustomization::IsFilteredComponent(const UActorComponent* const Component) const
{
const USceneComponent* SceneComp = Cast<USceneComponent>(Component);
const USceneComponent* ParentSceneComp = SceneComp != nullptr ? SceneComp->GetAttachParent() : nullptr;
const AActor* OuterActor = CachedFirstOuterActor.Get();
return Component->GetOwner()
&& (bAllowAnyActor || Component->GetOwner() == CachedFirstOuterActor.Get())
&& (!bAllowAnyActor || (OuterActor && Component->GetOwner()->GetLevel() == OuterActor->GetLevel()))
&& FComponentEditorUtils::CanEditComponentInstance(Component, SceneComp, false)
&& IsFilteredObject(Component, AllowedComponentClassFilters, DisallowedComponentClassFilters)
&& IsFilteredObject(Component->GetOwner(), AllowedActorClassFilters, DisallowedActorClassFilters);
}
bool FComponentReferenceCustomization::IsFilteredObject(const UObject* const Object, const TArray<const UClass*>& AllowedFilters, const TArray<const UClass*>& DisallowedFilters)
{
bool bAllowedToSetBasedOnFilter = true;
const UClass* ObjectClass = Object->GetClass();
if (AllowedFilters.Num() > 0)
{
bAllowedToSetBasedOnFilter = false;
for (const UClass* AllowedClass : AllowedFilters)
{
const bool bAllowedClassIsInterface = AllowedClass->HasAnyClassFlags(CLASS_Interface);
if (ObjectClass->IsChildOf(AllowedClass) || (bAllowedClassIsInterface && ObjectClass->ImplementsInterface(AllowedClass)))
{
bAllowedToSetBasedOnFilter = true;
break;
}
}
}
if (DisallowedFilters.Num() > 0 && bAllowedToSetBasedOnFilter)
{
for (const UClass* DisallowedClass : DisallowedFilters)
{
const bool bDisallowedClassIsInterface = DisallowedClass->HasAnyClassFlags(CLASS_Interface);
if (ObjectClass->IsChildOf(DisallowedClass) || (bDisallowedClassIsInterface && ObjectClass->ImplementsInterface(DisallowedClass)))
{
bAllowedToSetBasedOnFilter = false;
break;
}
}
}
return bAllowedToSetBasedOnFilter;
}
void FComponentReferenceCustomization::OnComponentSelected(const UActorComponent* InComponent)
{
ComponentComboButton->SetIsOpen(false);
FComponentReference ComponentReference = FComponentEditorUtils::MakeComponentReference(CachedFirstOuterActor.Get(), InComponent);
SetValue(ComponentReference);
}
void FComponentReferenceCustomization::CloseComboButton()
{
ComponentComboButton->SetIsOpen(false);
}
#undef LOCTEXT_NAMESPACE