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

259 lines
9.9 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PropertyEditorUtils.h"
#include "PropertyCustomizationHelpers.h"
#include "PropertyPathHelpers.h"
#include "Algo/ForEach.h"
#include "UObject/PropertyText.h"
namespace PropertyEditorUtils
{
void GetPropertyOptions(TArray<UObject*>& InOutContainers, FString& InOutPropertyPath,
TArray<TSharedPtr<FString>>& InOutOptions)
{
TArray<FString> OptionsStrings;
TArray<FText>* NullDisplayNames = nullptr;
GetPropertyOptions(InOutContainers, InOutPropertyPath, OptionsStrings, NullDisplayNames);
Algo::Transform(OptionsStrings, InOutOptions, [](const FString& InString) { return MakeShared<FString>(InString); });
}
struct FOptionsData
{
FString ValueString;
FText DisplayName;
friend uint32 GetTypeHash(const FOptionsData& Data)
{
// TODO: Is there a faster way to hash a FText?
return HashCombine(GetTypeHash(Data.ValueString), GetTypeHash(Data.DisplayName.ToString()));
}
bool operator==(const FOptionsData& Other) const
{
return ValueString == Other.ValueString && DisplayName.IdenticalTo(Other.DisplayName, ETextIdenticalModeFlags::LexicalCompareInvariants);
}
bool operator!=(const FOptionsData& Other) const
{
return !(this->operator==(Other));
}
};
void GetPropertyOptions(TArray<UObject*>& InOutContainers, FString& InOutPropertyPath,
TArray<FString>& InOutOptions, TArray<FText>* OutDisplayNames)
{
// Check for external function references
if (InOutPropertyPath.Contains(TEXT(".")))
{
InOutContainers.Empty();
UFunction* GetOptionsFunction = FindObject<UFunction>(nullptr, *InOutPropertyPath, true);
if (ensureMsgf(GetOptionsFunction && GetOptionsFunction->HasAnyFunctionFlags(EFunctionFlags::FUNC_Static), TEXT("Invalid GetOptions: %s"), *InOutPropertyPath))
{
UObject* GetOptionsCDO = GetOptionsFunction->GetOuterUClass()->GetDefaultObject();
GetOptionsFunction->GetName(InOutPropertyPath);
InOutContainers.Add(GetOptionsCDO);
}
}
if (InOutContainers.Num() > 0)
{
TArray<FOptionsData> OptionIntersection;
TSet<FOptionsData> OptionIntersectionSet;
// Loop carried containers
TArray<FOptionsData> LoopOptions;
TArray<FString> StringOptions;
TArray<FText> DisplayNameOptions;
TArray<FName> NameOptions;
TArray<FPropertyTextString> StringDisplayNameOptions;
TArray<FPropertyTextFName> FNameDisplayNameOptions;
// Need to find the intersection between all OptionsData.
// If there is no OutDisplayNames or a bound function to GetOptions doesn't provide DisplayNames, then don't provide
// display names either.
bool bUseDisplayNames = true;
for (UObject* Target : InOutContainers)
{
LoopOptions.Empty(LoopOptions.Num());
StringOptions.Empty(StringOptions.Num());
DisplayNameOptions.Empty(DisplayNameOptions.Num());
NameOptions.Empty(NameOptions.Num());
StringDisplayNameOptions.Empty(StringDisplayNameOptions.Num());
FNameDisplayNameOptions.Empty(FNameDisplayNameOptions.Num());
{
FEditorScriptExecutionGuard ScriptExecutionGuard;
// Test each signature of a function
FCachedPropertyPath Path(InOutPropertyPath);
// Handle function signature: "TArray<FString> GetOptions()"
if (PropertyPathHelpers::GetPropertyValue(Target, Path, StringOptions))
{
Algo::Transform(StringOptions, LoopOptions, [](const FString& InName) { return FOptionsData{ .ValueString = InName, .DisplayName = FText() }; });;
bUseDisplayNames = false;
}
// Handle function signature: "TArray<FText> GetOptions()"
else if (PropertyPathHelpers::GetPropertyValue(Target, Path, NameOptions))
{
Algo::Transform(NameOptions, LoopOptions, [](const FName& InName) { return FOptionsData{ .ValueString = InName.ToString(), .DisplayName = FText() }; });;
bUseDisplayNames = false;
}
else if (OutDisplayNames && bUseDisplayNames)
{
// Handle function signature: "TArray<FPropertyTextString> GetOptions()"
if (PropertyPathHelpers::GetPropertyValue(Target, Path, StringDisplayNameOptions))
{
Algo::Transform(StringDisplayNameOptions, LoopOptions, [](const FPropertyTextString& Pair) { return FOptionsData{ .ValueString = Pair.ValueString, .DisplayName = Pair.DisplayName }; });;
}
// Handle function signature: "TArray<FPropertyTextFName> GetOptions()"
else if (PropertyPathHelpers::GetPropertyValue(Target, Path, FNameDisplayNameOptions))
{
Algo::Transform(FNameDisplayNameOptions, LoopOptions, [](const FPropertyTextFName& Pair) { return FOptionsData{ .ValueString = Pair.ValueString.ToString(), .DisplayName = Pair.DisplayName }; });;
}
}
}
// If this is the first time there won't be any options.
if (OptionIntersection.Num() == 0)
{
OptionIntersection = LoopOptions;
OptionIntersectionSet = TSet<FOptionsData>(LoopOptions);
}
else
{
TSet<FOptionsData> LoopOptionsSet(LoopOptions);
OptionIntersectionSet = LoopOptionsSet.Intersect(OptionIntersectionSet);
OptionIntersection.RemoveAll([&OptionIntersectionSet](const FOptionsData& Option){ return !OptionIntersectionSet.Contains(Option); });
}
// If we're out of possible intersected options, we can stop.
if (OptionIntersection.Num() == 0)
{
break;
}
}
Algo::Transform(OptionIntersection, InOutOptions, [](const FOptionsData& Option) { return Option.ValueString; });
if (OutDisplayNames && bUseDisplayNames)
{
TArray<FText>& OutDisplayNamesRef = *OutDisplayNames;
Algo::Transform(OptionIntersection, OutDisplayNamesRef, [](const FOptionsData& Option) { return Option.DisplayName; });
}
}
}
void GetAllowedAndDisallowedClasses(const TArray<UObject*>& ObjectList, const FProperty& MetadataProperty, TArray<const UClass*>& AllowedClasses, TArray<const UClass*>& DisallowedClasses, bool bExactClass, const UClass* ObjectClass)
{
AllowedClasses = PropertyCustomizationHelpers::GetClassesFromMetadataString(MetadataProperty.GetOwnerProperty()->GetMetaData("AllowedClasses"));
DisallowedClasses = PropertyCustomizationHelpers::GetClassesFromMetadataString(MetadataProperty.GetOwnerProperty()->GetMetaData("DisallowedClasses"));
bool bMergeAllowedClasses = !AllowedClasses.IsEmpty();
if (MetadataProperty.GetOwnerProperty()->HasMetaData("GetAllowedClasses"))
{
const FString GetAllowedClassesFunctionName = MetadataProperty.GetOwnerProperty()->GetMetaData("GetAllowedClasses");
if (!GetAllowedClassesFunctionName.IsEmpty())
{
auto GetAllowedClasses = [&bMergeAllowedClasses, &AllowedClasses, &bExactClass, &DisallowedClasses, ObjectClass](UObject* InObject, const UFunction* InGetAllowedClassesFunction)-> bool
{
DECLARE_DELEGATE_RetVal(TArray<UClass*>, FGetAllowedClasses);
if (!bMergeAllowedClasses)
{
AllowedClasses.Append(FGetAllowedClasses::CreateUFunction(InObject, InGetAllowedClassesFunction->GetFName()).Execute());
if (AllowedClasses.IsEmpty())
{
// No allowed class means all classes are valid
return true;
}
bMergeAllowedClasses = true;
}
else
{
TArray<UClass*> MergedClasses = FGetAllowedClasses::CreateUFunction(InObject, InGetAllowedClassesFunction->GetFName()).Execute();
if (MergedClasses.IsEmpty())
{
// No allowed class means all classes are valid
return true;
}
TArray<const UClass*> CurrentAllowedClassFilters = MoveTemp(AllowedClasses);
ensure(AllowedClasses.IsEmpty());
for (const UClass* MergedClass : MergedClasses)
{
// Keep classes that match both allow list
for (const UClass* CurrentClass : CurrentAllowedClassFilters)
{
if (CurrentClass == MergedClass || (!bExactClass && CurrentClass->IsChildOf(MergedClass)))
{
AllowedClasses.Add(CurrentClass);
break;
}
if (!bExactClass && MergedClass->IsChildOf(CurrentClass))
{
AllowedClasses.Add(MergedClass);
break;
}
}
}
if (AllowedClasses.IsEmpty())
{
// An empty AllowedClasses array means that everything is allowed: in this case, forbid UObject
DisallowedClasses.Add(ObjectClass);
return false;
}
}
return true;
};
// First look for a library function assuming fully qualified path, e.g. /Script/ModuleName.ClassName:FunctionName
if(GetAllowedClassesFunctionName.Contains(TEXT(".")))
{
const UFunction* GetAllowedClassesFunction = FindObject<UFunction>(nullptr, *GetAllowedClassesFunctionName, true);
if (ensureMsgf(GetAllowedClassesFunction && GetAllowedClassesFunction->HasAnyFunctionFlags(EFunctionFlags::FUNC_Static), TEXT("Invalid GetAllowedClasses: %s"), *GetAllowedClassesFunctionName))
{
UObject* GetAllowedClassesCDO = GetAllowedClassesFunction->GetOwnerClass()->GetDefaultObject();
GetAllowedClasses(GetAllowedClassesCDO, GetAllowedClassesFunction);
}
}
else
{
// Otherwise interrogate the object list
for (UObject* Object : ObjectList)
{
const UFunction* GetAllowedClassesFunction = Object ? Object->FindFunction(*GetAllowedClassesFunctionName) : nullptr;
if (GetAllowedClassesFunction)
{
if(!GetAllowedClasses(Object, GetAllowedClassesFunction))
{
return;
}
}
}
}
}
}
if (MetadataProperty.GetOwnerProperty()->HasMetaData("GetDisallowedClasses"))
{
const FString GetDisallowedClassesFunctionName = MetadataProperty.GetOwnerProperty()->GetMetaData("GetDisallowedClasses");
if (!GetDisallowedClassesFunctionName.IsEmpty())
{
for (UObject* Object : ObjectList)
{
const UFunction* GetDisallowedClassesFunction = Object ? Object->FindFunction(*GetDisallowedClassesFunctionName) : nullptr;
if (GetDisallowedClassesFunction)
{
DECLARE_DELEGATE_RetVal(TArray<UClass*>, FGetDisallowedClasses);
DisallowedClasses.Append(FGetDisallowedClasses::CreateUFunction(Object, GetDisallowedClassesFunction->GetFName()).Execute());
}
}
}
}
}
}