// 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& InOutContainers, FString& InOutPropertyPath, TArray>& InOutOptions) { TArray OptionsStrings; TArray* NullDisplayNames = nullptr; GetPropertyOptions(InOutContainers, InOutPropertyPath, OptionsStrings, NullDisplayNames); Algo::Transform(OptionsStrings, InOutOptions, [](const FString& InString) { return MakeShared(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& InOutContainers, FString& InOutPropertyPath, TArray& InOutOptions, TArray* OutDisplayNames) { // Check for external function references if (InOutPropertyPath.Contains(TEXT("."))) { InOutContainers.Empty(); UFunction* GetOptionsFunction = FindObject(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 OptionIntersection; TSet OptionIntersectionSet; // Loop carried containers TArray LoopOptions; TArray StringOptions; TArray DisplayNameOptions; TArray NameOptions; TArray StringDisplayNameOptions; TArray 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 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 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 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 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(LoopOptions); } else { TSet 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& OutDisplayNamesRef = *OutDisplayNames; Algo::Transform(OptionIntersection, OutDisplayNamesRef, [](const FOptionsData& Option) { return Option.DisplayName; }); } } } void GetAllowedAndDisallowedClasses(const TArray& ObjectList, const FProperty& MetadataProperty, TArray& AllowedClasses, TArray& 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, 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 MergedClasses = FGetAllowedClasses::CreateUFunction(InObject, InGetAllowedClassesFunction->GetFName()).Execute(); if (MergedClasses.IsEmpty()) { // No allowed class means all classes are valid return true; } TArray 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(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, FGetDisallowedClasses); DisallowedClasses.Append(FGetDisallowedClasses::CreateUFunction(Object, GetDisallowedClassesFunction->GetFName()).Execute()); } } } } } }