// Copyright Epic Games, Inc. All Rights Reserved. #include "PropertyBagDetails.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "IDetailChildrenBuilder.h" #include "IDetailGroup.h" #include "IPropertyUtilities.h" #include "PropertyBagDragDropHandler.h" #include "ScopedTransaction.h" #include "SDraggableBox.h" // Custom widget for drag and drop sections #include "StructUtilsMetadata.h" #include "STypeSelector.h" // Custom widget for pill type selector #include "Framework/MultiBox/MultiBoxBuilder.h" #include "StructUtils/UserDefinedStruct.h" #include "Templates/ValueOrError.h" #include "UObject/EnumProperty.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(PropertyBagDetails) #define LOCTEXT_NAMESPACE "StructUtilsEditor" //////////////////////////////////// namespace UE::StructUtils { /** Sets property descriptor based on a Blueprint pin type. */ void SetPropertyDescFromPin(FPropertyBagPropertyDesc& Desc, const FEdGraphPinType& PinType) { const UPropertyBagSchema* Schema = GetDefault(); check(Schema); // remove any existing containers Desc.ContainerTypes.Reset(); // Fill Container types, if any switch (PinType.ContainerType) { case EPinContainerType::Array: Desc.ContainerTypes.Add(EPropertyBagContainerType::Array); break; case EPinContainerType::Set: Desc.ContainerTypes.Add(EPropertyBagContainerType::Set); break; case EPinContainerType::Map: ensureMsgf(false, TEXT("Unsuported container type [Map] ")); break; default: break; } // Value type if (PinType.PinCategory == UEdGraphSchema_K2::PC_Boolean) { Desc.ValueType = EPropertyBagPropertyType::Bool; Desc.ValueTypeObject = nullptr; } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Byte) { if (UEnum* Enum = Cast(PinType.PinSubCategoryObject)) { Desc.ValueType = EPropertyBagPropertyType::Enum; Desc.ValueTypeObject = PinType.PinSubCategoryObject.Get(); } else { Desc.ValueType = EPropertyBagPropertyType::Byte; Desc.ValueTypeObject = nullptr; } } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Int) { Desc.ValueType = EPropertyBagPropertyType::Int32; Desc.ValueTypeObject = nullptr; } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Int64) { Desc.ValueType = EPropertyBagPropertyType::Int64; Desc.ValueTypeObject = nullptr; } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Real) { if (PinType.PinSubCategory == UEdGraphSchema_K2::PC_Float) { Desc.ValueType = EPropertyBagPropertyType::Float; Desc.ValueTypeObject = nullptr; } else if (PinType.PinSubCategory == UEdGraphSchema_K2::PC_Double) { Desc.ValueType = EPropertyBagPropertyType::Double; Desc.ValueTypeObject = nullptr; } } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Name) { Desc.ValueType = EPropertyBagPropertyType::Name; Desc.ValueTypeObject = nullptr; } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_String) { Desc.ValueType = EPropertyBagPropertyType::String; Desc.ValueTypeObject = nullptr; } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Text) { Desc.ValueType = EPropertyBagPropertyType::Text; Desc.ValueTypeObject = nullptr; } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Enum) { Desc.ValueType = EPropertyBagPropertyType::Enum; Desc.ValueTypeObject = PinType.PinSubCategoryObject.Get(); } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) { Desc.ValueType = EPropertyBagPropertyType::Struct; Desc.ValueTypeObject = PinType.PinSubCategoryObject.Get(); } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Object) { Desc.ValueType = EPropertyBagPropertyType::Object; Desc.ValueTypeObject = PinType.PinSubCategoryObject.Get(); } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject) { Desc.ValueType = EPropertyBagPropertyType::SoftObject; Desc.ValueTypeObject = PinType.PinSubCategoryObject.Get(); } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Class) { Desc.ValueType = EPropertyBagPropertyType::Class; Desc.ValueTypeObject = PinType.PinSubCategoryObject.Get(); } else if (PinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass) { Desc.ValueType = EPropertyBagPropertyType::SoftClass; Desc.ValueTypeObject = PinType.PinSubCategoryObject.Get(); } else { ensureMsgf(false, TEXT("Unhandled pin category %s"), *PinType.PinCategory.ToString()); } } /** @return Blueprint pin type from property descriptor. */ FEdGraphPinType GetPropertyDescAsPin(const FPropertyBagPropertyDesc& Desc) { UEnum* PropertyTypeEnum = StaticEnum(); check(PropertyTypeEnum); const UPropertyBagSchema* Schema = GetDefault(); check(Schema); FEdGraphPinType PinType; PinType.PinSubCategory = NAME_None; // Container type //@todo: Handle nested containers in property selection. const EPropertyBagContainerType ContainerType = Desc.ContainerTypes.GetFirstContainerType(); switch (ContainerType) { case EPropertyBagContainerType::Array: PinType.ContainerType = EPinContainerType::Array; break; case EPropertyBagContainerType::Set: PinType.ContainerType = EPinContainerType::Set; break; default: PinType.ContainerType = EPinContainerType::None; } // Value type switch (Desc.ValueType) { case EPropertyBagPropertyType::Bool: PinType.PinCategory = UEdGraphSchema_K2::PC_Boolean; break; case EPropertyBagPropertyType::Byte: PinType.PinCategory = UEdGraphSchema_K2::PC_Byte; break; case EPropertyBagPropertyType::Int32: PinType.PinCategory = UEdGraphSchema_K2::PC_Int; break; case EPropertyBagPropertyType::Int64: PinType.PinCategory = UEdGraphSchema_K2::PC_Int64; break; case EPropertyBagPropertyType::Float: PinType.PinCategory = UEdGraphSchema_K2::PC_Real; PinType.PinSubCategory = UEdGraphSchema_K2::PC_Float; break; case EPropertyBagPropertyType::Double: PinType.PinCategory = UEdGraphSchema_K2::PC_Real; PinType.PinSubCategory = UEdGraphSchema_K2::PC_Double; break; case EPropertyBagPropertyType::Name: PinType.PinCategory = UEdGraphSchema_K2::PC_Name; break; case EPropertyBagPropertyType::String: PinType.PinCategory = UEdGraphSchema_K2::PC_String; break; case EPropertyBagPropertyType::Text: PinType.PinCategory = UEdGraphSchema_K2::PC_Text; break; case EPropertyBagPropertyType::Enum: // @todo: some pin coloring is not correct due to this (byte-as-enum vs enum). PinType.PinCategory = UEdGraphSchema_K2::PC_Enum; PinType.PinSubCategoryObject = const_cast(Desc.ValueTypeObject.Get()); break; case EPropertyBagPropertyType::Struct: PinType.PinCategory = UEdGraphSchema_K2::PC_Struct; PinType.PinSubCategoryObject = const_cast(Desc.ValueTypeObject.Get()); break; case EPropertyBagPropertyType::Object: PinType.PinCategory = UEdGraphSchema_K2::PC_Object; PinType.PinSubCategoryObject = const_cast(Desc.ValueTypeObject.Get()); break; case EPropertyBagPropertyType::SoftObject: PinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject; PinType.PinSubCategoryObject = const_cast(Desc.ValueTypeObject.Get()); break; case EPropertyBagPropertyType::Class: PinType.PinCategory = UEdGraphSchema_K2::PC_Class; PinType.PinSubCategoryObject = const_cast(Desc.ValueTypeObject.Get()); break; case EPropertyBagPropertyType::SoftClass: PinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass; PinType.PinSubCategoryObject = const_cast(Desc.ValueTypeObject.Get()); break; case EPropertyBagPropertyType::UInt32: // Warning : Type only partially supported (Blueprint does not support unsigned type) PinType.PinCategory = UEdGraphSchema_K2::PC_Int; break; case EPropertyBagPropertyType::UInt64: // Warning : Type only partially supported (Blueprint does not support unsigned type) PinType.PinCategory = UEdGraphSchema_K2::PC_Int64; break; default: ensureMsgf(false, TEXT("Unhandled value type %s"), *UEnum::GetValueAsString(Desc.ValueType)); break; } return PinType; } namespace Private { /** @return true property handle holds struct property of type T. */ template bool IsScriptStruct(const TSharedPtr& PropertyHandle) { if (!PropertyHandle) { return false; } FStructProperty* StructProperty = CastField(PropertyHandle->GetProperty()); return StructProperty && StructProperty->Struct->IsA(TBaseStructure::Get()->GetClass()); } /** @return true if the property is one of the known missing types. */ bool HasMissingType(const TSharedPtr& PropertyHandle) { if (!PropertyHandle) { return false; } // Handles Struct if (FStructProperty* StructProperty = CastField(PropertyHandle->GetProperty())) { return StructProperty->Struct == FPropertyBagMissingStruct::StaticStruct(); } // Handles Object, SoftObject, Class, SoftClass. if (FObjectPropertyBase* ObjectProperty = CastField(PropertyHandle->GetProperty())) { return ObjectProperty->PropertyClass == UPropertyBagMissingObject::StaticClass(); } // Handles Enum if (FEnumProperty* EnumProperty = CastField(PropertyHandle->GetProperty())) { return EnumProperty->GetEnum() == StaticEnum(); } return false; } /** @return property bag struct common to all edited properties. */ const UPropertyBag* GetCommonBagStruct(TSharedPtr StructProperty) { const UPropertyBag* CommonBagStruct = nullptr; if (ensure(IsScriptStruct(StructProperty))) { StructProperty->EnumerateConstRawData([&CommonBagStruct](const void* RawData, const int32 /*DataIndex*/, const int32 /*NumDatas*/) { if (RawData) { const FInstancedPropertyBag* Bag = static_cast(RawData); const UPropertyBag* BagStruct = Bag->GetPropertyBagStruct(); if (CommonBagStruct && CommonBagStruct != BagStruct) { // Multiple struct types on the sources - show nothing set CommonBagStruct = nullptr; return false; } CommonBagStruct = BagStruct; } return true; }); } return CommonBagStruct; } /** @return property descriptors of the property bag struct common to all edited properties. */ TArray GetCommonPropertyDescs(const TSharedPtr& StructProperty) { TArray PropertyDescs; if (const UPropertyBag* BagStruct = GetCommonBagStruct(StructProperty)) { PropertyDescs = BagStruct->GetPropertyDescs(); } return PropertyDescs; } /** Creates new property bag struct and sets all properties to use it, migrating over old values. */ void SetPropertyDescs(const TSharedPtr& StructProperty, const TConstArrayView PropertyDescs) { if (ensure(IsScriptStruct(StructProperty))) { // Create new bag struct const UPropertyBag* NewBagStruct = UPropertyBag::GetOrCreateFromDescs(PropertyDescs); // Migrate structs to the new type, copying values over. StructProperty->EnumerateRawData([&NewBagStruct](void* RawData, const int32 /*DataIndex*/, const int32 /*NumDatas*/) { if (RawData) { if (FInstancedPropertyBag* Bag = static_cast(RawData)) { Bag->MigrateToNewBagStruct(NewBagStruct); } } return true; }); } } FName GetPropertyNameSafe(const TSharedPtr& PropertyHandle) { const FProperty* Property = PropertyHandle ? PropertyHandle->GetProperty() : nullptr; if (Property != nullptr) { return Property->GetFName(); } return FName(); } /** @return true of the property name is not used yet by the property bag structure common to all edited properties. */ bool IsUniqueName(const FName NewName, const FName OldName, const TSharedPtr& StructProperty) { if (NewName == OldName) { return false; } if (!StructProperty || !StructProperty->IsValidHandle()) { return false; } bool bFound = false; if (ensure(IsScriptStruct(StructProperty))) { StructProperty->EnumerateConstRawData([&bFound, NewName](const void* RawData, const int32 /*DataIndex*/, const int32 /*NumDatas*/) { if (const FInstancedPropertyBag* Bag = static_cast(RawData)) { if (const UPropertyBag* BagStruct = Bag->GetPropertyBagStruct()) { const bool bContains = BagStruct->GetPropertyDescs().ContainsByPredicate([NewName](const FPropertyBagPropertyDesc& Desc) { return Desc.Name == NewName; }); if (bContains) { bFound = true; return false; // Stop iterating } } } return true; }); } return !bFound; } template void ApplyChangesToPropertyDescs(const FText& SessionName, const TSharedPtr& StructProperty, const TSharedPtr& PropUtils, TFunc&& Function) { if (!StructProperty || !PropUtils) { return; } FScopedTransaction Transaction(SessionName); TArray PropertyDescs = GetCommonPropertyDescs(StructProperty); StructProperty->NotifyPreChange(); Function(PropertyDescs); SetPropertyDescs(StructProperty, PropertyDescs); StructProperty->NotifyPostChange(EPropertyChangeType::ValueSet); StructProperty->NotifyFinishedChangingProperties(); if (PropUtils) { PropUtils->ForceRefresh(); } } template void ApplyChangesToSinglePropertyDesc(const FText& SessionName, const TSharedPtr PropertyHandle, const TSharedPtr& StructProperty, const TSharedPtr& PropUtils, TFunc Function) { ApplyChangesToPropertyDescs(SessionName, StructProperty, PropUtils, [Function = std::move(Function), Property = PropertyHandle->GetProperty()](TArray& PropertyDescs) { if (FPropertyBagPropertyDesc* Desc = PropertyDescs.FindByPredicate([Property](const FPropertyBagPropertyDesc& OutDesc) { return OutDesc.CachedProperty == Property; })) { Function(*Desc); } }); } template void ApplyChangesToSinglePropertyDesc(const FText& SessionName, const FPropertyBagPropertyDesc& PropertyDesc, const TSharedPtr& StructProperty, const TSharedPtr& PropUtils, TFunc Function) { ApplyChangesToPropertyDescs(SessionName, StructProperty, PropUtils, [Function = std::move(Function), &PropertyDesc](TArray& PropertyDescs) { if (FPropertyBagPropertyDesc* Desc = PropertyDescs.FindByPredicate([&PropertyDesc](const FPropertyBagPropertyDesc& OutDesc) { return OutDesc == PropertyDesc; })) { Function(*Desc); } }); } bool CanHaveMemberVariableOfType(const FEdGraphPinType& PinType) { if (PinType.PinCategory == UEdGraphSchema_K2::PC_Exec || PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard || PinType.PinCategory == UEdGraphSchema_K2::PC_MCDelegate || PinType.PinCategory == UEdGraphSchema_K2::PC_Delegate || PinType.PinCategory == UEdGraphSchema_K2::PC_Interface) { return false; } return true; } bool FindUserFunction(TSharedPtr InStructProperty, FName InFuncMetadataName, UFunction*& OutFunc, UObject*& OutTarget) { FProperty* MetadataProperty = InStructProperty->GetMetaDataProperty(); OutFunc = nullptr; OutTarget = nullptr; if (!MetadataProperty || !MetadataProperty->HasMetaData(InFuncMetadataName)) { return false; } FString FunctionName = MetadataProperty->GetMetaData(InFuncMetadataName); if (FunctionName.IsEmpty()) { return false; } TArray OutObjects; InStructProperty->GetOuterObjects(OutObjects); // Check for external function references, taken from GetOptions if (FunctionName.Contains(TEXT("."))) { OutFunc = FindObject(nullptr, *FunctionName, true); if (ensureMsgf(OutFunc && OutFunc->HasAnyFunctionFlags(EFunctionFlags::FUNC_Static), TEXT("[%s] Didn't find function %s or expected it to be static"), *InFuncMetadataName.ToString(), *FunctionName)) { UObject* GetOptionsCDO = OutFunc->GetOuterUClass()->GetDefaultObject(); OutTarget = GetOptionsCDO; } } else if (OutObjects.Num() > 0) { OutTarget = OutObjects[0]; OutFunc = OutTarget->GetClass() ? OutTarget->GetClass()->FindFunctionByName(*FunctionName) : nullptr; } // Only support native functions if (!ensureMsgf(OutFunc && OutFunc->IsNative(), TEXT("[%s] Didn't find function %s or expected it to be native"), *InFuncMetadataName.ToString(), *FunctionName)) { OutFunc = nullptr; OutTarget = nullptr; } return OutTarget != nullptr && OutFunc != nullptr; } FText GetAccessSpecifierNameFromFlags(const EPropertyFlags Flags) { // TODO: Support 'protected'. For now treat protected and private the same. if (!!(Flags & (CPF_NativeAccessSpecifierPrivate | CPF_NativeAccessSpecifierProtected))) { return LOCTEXT("AccessSpecifierPrivate", "Private"); } else // Public flag or not, should be treated as public. { return LOCTEXT("AccessSpecifierPublic", "Public"); } } // UFunction calling helpers. // Use our "own" hardcoded reflection system for types used in UFunctions calls in this file. template struct TypeName { static const TCHAR* Get() = delete; }; #define DEFINE_TYPENAME(InType) template<> struct TypeName { static const TCHAR *Get() { return TEXT(#InType); }}; DEFINE_TYPENAME(bool) DEFINE_TYPENAME(FGuid) DEFINE_TYPENAME(FName) DEFINE_TYPENAME(FEdGraphPinType) #undef DEFINE_TYPENAME // Wrapper around a param that store an address (const_cast for const ptr, be careful of that) // a string identifiying the underlying cpp type and if the input value is const, mark it const. struct FFuncParam { void* Value = nullptr; const TCHAR* CppType; bool bIsConst = false; template static FFuncParam Make(T& Value) { FFuncParam Result; Result.Value = &Value; Result.CppType = TypeName::Get(); return Result; } template static FFuncParam Make(const T& Value) { FFuncParam Result; Result.Value = &const_cast(Value); Result.CppType = TypeName::Get(); Result.bIsConst = true; return Result; } }; // Validate that the function pass as parameter has signature ReturnType(ArgsTypes...) template bool ValidateFunctionSignature(UFunction* InFunc) { if (!InFunc) { return false; } constexpr int32 NumParms = std::is_same_v ? sizeof...(ArgsTypes) : (sizeof...(ArgsTypes) + 1); if (NumParms != InFunc->NumParms) { return false; } const TCHAR* ArgsCppTypes[NumParms] = { TypeName::Get() ... }; // If we have a return type, put it at the end. UFunction will have the return type after InArgs in the field iterator. if constexpr (!std::is_same_v) { ArgsCppTypes[NumParms - 1] = TypeName::Get(); } else { // Otherwise, check that the function doesn't have a return param if (InFunc->GetReturnProperty() != nullptr) { return false; } } int32 ArgCppTypesIndex = 0; for (TFieldIterator It(InFunc); It && It->HasAnyPropertyFlags(EPropertyFlags::CPF_Parm); ++It) { const FString PropertyCppType = It->GetCPPType(); if (PropertyCppType != ArgsCppTypes[ArgCppTypesIndex]) { return false; } // Also making sure that the last param is a return param, if we have a return value if constexpr (!std::is_same_v) { if (ArgCppTypesIndex == NumParms - 1 && !It->HasAnyPropertyFlags(EPropertyFlags::CPF_ReturnParm)) { return false; } } ArgCppTypesIndex++; } return true; } template TValueOrError CallFunc(UObject* InTargetObject, UFunction* InFunc, ArgsTypes&& ...InArgs) { if (!InTargetObject || !InFunc) { return MakeError(); } constexpr int32 NumParms = std::is_same_v ? sizeof...(ArgsTypes) : (sizeof...(ArgsTypes) + 1); if (NumParms != InFunc->NumParms) { return MakeError(); } FFuncParam InParams[NumParms] = { FFuncParam::Make(std::forward(InArgs)) ... }; auto Invoke = [InTargetObject, InFunc, &InParams, NumParms](ReturnType* OutResult) -> bool { // Validate that the function has a return property if the return type is not void. if (std::is_same_v != (InFunc->GetReturnProperty() == nullptr)) { return false; } // Allocating our "stack" for the function call on the stack (will be freed when this function is exited) uint8* StackMemory = (uint8*)FMemory_Alloca(InFunc->ParmsSize); FMemory::Memzero(StackMemory, InFunc->ParmsSize); if constexpr (!std::is_same_v) { check(OutResult != nullptr); InParams[NumParms - 1] = FFuncParam::Make(*OutResult); } bool bValid = true; int32 ParamIndex = 0; // Initializing our "stack" with our parameters. Use the property system to make sure more complex types // are constructed before being set. for (TFieldIterator It(InFunc); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { FProperty* LocalProp = *It; if (!LocalProp->HasAnyPropertyFlags(CPF_ZeroConstructor)) { LocalProp->InitializeValue_InContainer(StackMemory); } if (bValid) { if (ParamIndex >= NumParms) { bValid = false; continue; } FFuncParam& Param = InParams[ParamIndex++]; if (LocalProp->GetCPPType() != Param.CppType) { bValid = false; continue; } LocalProp->SetValue_InContainer(StackMemory, Param.Value); } } if (bValid) { FFrame Stack(InTargetObject, InFunc, StackMemory, nullptr, InFunc->ChildProperties); InFunc->Invoke(InTargetObject, Stack, OutResult); } ParamIndex = 0; // Copy back all non-const out params (that is not the return param, this one is already set by the invoke call) // from the stack, also making sure that the constructed types are destroyed accordingly. for (TFieldIterator It(InFunc); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { FProperty* LocalProp = *It; FFuncParam& Param = InParams[ParamIndex++]; if (LocalProp->HasAnyPropertyFlags(CPF_OutParm) && !LocalProp->HasAnyPropertyFlags(CPF_ReturnParm) && !Param.bIsConst) { LocalProp->GetValue_InContainer(StackMemory, Param.Value); } LocalProp->DestroyValue_InContainer(StackMemory); } return bValid; }; if constexpr (std::is_same_v) { if (Invoke(nullptr)) { return MakeValue(); } else { return MakeError(); } } else { ReturnType OutResult{}; if (Invoke(&OutResult)) { return MakeValue(std::move(OutResult)); } else { return MakeError(); } } } /** Checks if the value for a source property in a source struct has the same value that the target property in the target struct. */ bool ArePropertiesIdentical( const FPropertyBagPropertyDesc* InSourcePropertyDesc, const FInstancedPropertyBag& InSourceInstance, const FPropertyBagPropertyDesc* InTargetPropertyDesc, const FInstancedPropertyBag& InTargetInstance) { if (!InSourceInstance.IsValid() || !InTargetInstance.IsValid() || !InSourcePropertyDesc || !InSourcePropertyDesc->CachedProperty || !InTargetPropertyDesc || !InTargetPropertyDesc->CachedProperty) { return false; } if (!InSourcePropertyDesc->CompatibleType(*InTargetPropertyDesc)) { return false; } const uint8* SourceValueAddress = InSourceInstance.GetValue().GetMemory() + InSourcePropertyDesc->CachedProperty->GetOffset_ForInternal(); const uint8* TargetValueAddress = InTargetInstance.GetValue().GetMemory() + InTargetPropertyDesc->CachedProperty->GetOffset_ForInternal(); return InSourcePropertyDesc->CachedProperty->Identical(SourceValueAddress, TargetValueAddress); } /** Copy the value for a source property in a source struct to the target property in the target struct. */ void CopyPropertyValue(const FPropertyBagPropertyDesc* InSourcePropertyDesc, const FInstancedPropertyBag& InSourceInstance, const FPropertyBagPropertyDesc* InTargetPropertyDesc, FInstancedPropertyBag& InTargetInstance) { if (!InSourceInstance.IsValid() || !InTargetInstance.IsValid() || !InSourcePropertyDesc || !InSourcePropertyDesc->CachedProperty || !InTargetPropertyDesc || !InTargetPropertyDesc->CachedProperty) { return; } // Can't copy if they are not compatible. if (!InSourcePropertyDesc->CompatibleType(*InTargetPropertyDesc)) { return; } const uint8* SourceValueAddress = InSourceInstance.GetValue().GetMemory() + InSourcePropertyDesc->CachedProperty->GetOffset_ForInternal(); uint8* TargetValueAddress = InTargetInstance.GetMutableValue().GetMemory() + InTargetPropertyDesc->CachedProperty->GetOffset_ForInternal(); InSourcePropertyDesc->CachedProperty->CopyCompleteValue(TargetValueAddress, SourceValueAddress); } void GetFilteredVariableTypeTree(const TSharedPtr& BagStructProperty, TArray>& TypeTree, const ETypeTreeFilter TypeTreeFilter) { // The type selector popup might outlive this details view, so bag struct property can be invalid here. if (!BagStructProperty || !BagStructProperty->IsValidHandle()) { return; } UFunction* IsPinTypeAcceptedFunc = nullptr; UObject* IsPinTypeAcceptedTarget = nullptr; if (FindUserFunction(BagStructProperty, Metadata::IsPinTypeAcceptedName, IsPinTypeAcceptedFunc, IsPinTypeAcceptedTarget)) { check(IsPinTypeAcceptedFunc && IsPinTypeAcceptedTarget); // We need to make sure the signature matches perfectly: bool(FEdGraphPinType, bool) bool bFuncIsValid = ValidateFunctionSignature(IsPinTypeAcceptedFunc); if (!ensureMsgf(bFuncIsValid, TEXT("[%s] Function %s does not have the right signature."), *Metadata::IsPinTypeAcceptedName.ToString(), *IsPinTypeAcceptedFunc->GetName())) { return; } } auto IsPinTypeAccepted = [IsPinTypeAcceptedFunc, IsPinTypeAcceptedTarget](const FEdGraphPinType& InPinType, bool bInIsChild) -> bool { if (IsPinTypeAcceptedFunc && IsPinTypeAcceptedTarget) { const TValueOrError bIsValid = CallFunc(IsPinTypeAcceptedTarget, IsPinTypeAcceptedFunc, InPinType, bInIsChild); return bIsValid.HasValue() && bIsValid.GetValue(); } else { return true; } }; check(GetDefault()); TArray> TempTypeTree; GetDefault()->GetVariableTypeTree(TempTypeTree, TypeTreeFilter); // Filter for (TSharedPtr& PinType : TempTypeTree) { if (!PinType.IsValid() || !IsPinTypeAccepted(PinType->GetPinType(/*bForceLoadSubCategoryObject*/false), /*bInIsChild=*/ false)) { continue; } for (int32 ChildIndex = 0; ChildIndex < PinType->Children.Num();) { TSharedPtr Child = PinType->Children[ChildIndex]; if (Child.IsValid()) { const FEdGraphPinType& ChildPinType = Child->GetPinType(/*bForceLoadSubCategoryObject*/false); if (!CanHaveMemberVariableOfType(ChildPinType) || !IsPinTypeAccepted(ChildPinType, /*bInIsChild=*/ true)) { PinType->Children.RemoveAt(ChildIndex); continue; } } ++ChildIndex; } TypeTree.Add(PinType); } } bool CanDeleteProperty(const TSharedPtr& InStructProperty, const TSharedPtr& ChildPropertyHandle) { if (!InStructProperty || !InStructProperty->IsValidHandle() || !ChildPropertyHandle || !ChildPropertyHandle->IsValidHandle()) { return false; } // Extra check provided by the user to cancel a remove action. Useful to provide the user a possibility to cancel the action if // the given property is in use elsewhere. UFunction* CanRemovePropertyFunc = nullptr; UObject* CanRemovePropertyTarget = nullptr; if (FindUserFunction(InStructProperty, Metadata::CanRemovePropertyName, CanRemovePropertyFunc, CanRemovePropertyTarget)) { check(CanRemovePropertyFunc && CanRemovePropertyTarget); FName PropertyName = ChildPropertyHandle->GetProperty()->GetFName(); const UPropertyBag* PropertyBag = GetCommonBagStruct(InStructProperty); const FPropertyBagPropertyDesc* PropertyDesc = PropertyBag ? PropertyBag->FindPropertyDescByName(PropertyName) : nullptr; if (!PropertyDesc) { return false; } // We need to make sure the signature matches perfectly: bool(FGuid, FName)s bool bFuncIsValid = UE::StructUtils::Private::ValidateFunctionSignature(CanRemovePropertyFunc); if (!ensureMsgf(bFuncIsValid, TEXT("[%s] Function %s does not have the right signature."), *UE::StructUtils::Metadata::CanRemovePropertyName.ToString(), *CanRemovePropertyFunc->GetName())) { return false; } const TValueOrError bCanRemove = UE::StructUtils::Private::CallFunc(CanRemovePropertyTarget, CanRemovePropertyFunc, PropertyDesc->ID, PropertyDesc->Name); if (bCanRemove.HasError() || !bCanRemove.GetValue()) { return false; } } return true; } void DeleteProperty(TSharedPtr InStructProperty, TSharedPtr ChildPropertyHandle, const TSharedPtr& PropUtils) { if (!InStructProperty || !InStructProperty->IsValidHandle() || !ChildPropertyHandle || !ChildPropertyHandle->IsValidHandle()) { return; } if (!CanDeleteProperty(InStructProperty, ChildPropertyHandle)) { return; } ApplyChangesToPropertyDescs( FText::Format(LOCTEXT("OnPropertyDeleted", "Deleted property: {0}"), ChildPropertyHandle->GetPropertyDisplayName()), InStructProperty, PropUtils, [&ChildPropertyHandle](TArray& PropertyDescs) { const FProperty* Property = ChildPropertyHandle ? ChildPropertyHandle->GetProperty() : nullptr; PropertyDescs.RemoveAll([Property](const FPropertyBagPropertyDesc& Desc) { return Desc.CachedProperty == Property; }); }); } FEdGraphPinType GetPinInfo(const TSharedPtr& ChildPropertyHandle, const TSharedPtr& InBagStructProperty) { // The SPinTypeSelector popup might outlive this details view, so bag struct property can be invalid here. if (!InBagStructProperty || !InBagStructProperty->IsValidHandle() || !ChildPropertyHandle || !ChildPropertyHandle->IsValidHandle()) { return FEdGraphPinType(); } TArray PropertyDescs = Private::GetCommonPropertyDescs(InBagStructProperty); const FProperty* Property = ChildPropertyHandle->GetProperty(); if (FPropertyBagPropertyDesc* Desc = PropertyDescs.FindByPredicate([Property](const FPropertyBagPropertyDesc& Desc){ return Desc.CachedProperty == Property; })) { return GetPropertyDescAsPin(*Desc); } return FEdGraphPinType(); } void PinInfoChanged(TSharedPtr ChildPropertyHandle, const TSharedPtr& InBagStructProperty, const TSharedPtr& InPropUtils, const FEdGraphPinType& PinType) { // The SPinTypeSelector popup might outlive this details view, so bag struct property can be invalid here. if (!InBagStructProperty || !InBagStructProperty->IsValidHandle() || !ChildPropertyHandle || !ChildPropertyHandle->IsValidHandle()) { return; } Private::ApplyChangesToPropertyDescs( FText::Format(LOCTEXT("OnPropertyTypeChanged", "Changed property type: {0}"), ChildPropertyHandle->GetPropertyDisplayName()), InBagStructProperty, InPropUtils, [&PinType, &ChildPropertyHandle](TArray& PropertyDescs) { // Find and change struct type const FProperty* Property = ChildPropertyHandle ? ChildPropertyHandle->GetProperty() : nullptr; if (FPropertyBagPropertyDesc* Desc = PropertyDescs.FindByPredicate([Property](const FPropertyBagPropertyDesc& Desc){ return Desc.CachedProperty == Property; })) { SetPropertyDescFromPin(*Desc, PinType); } }); } } // UE::StructUtils::Private TSharedRef CreateTypeSelectionWidget(TSharedPtr ChildPropertyHandle, const TSharedPtr& InBagStructProperty, const TSharedPtr& InPropUtils, SPinTypeSelector::ESelectorType SelectorType, const bool bAllowContainers) { return SNew(SBox) .HAlign(HAlign_Right) .Padding(FMargin(4, 0)) [ SNew(STypeSelector, FGetPinTypeTree::CreateLambda( [BagStructProperty = InBagStructProperty](TArray>& TypeTree, const ETypeTreeFilter TypeTreeFilter) { UE::StructUtils::Private::GetFilteredVariableTypeTree(BagStructProperty, TypeTree, TypeTreeFilter); })) .TargetPinType_Lambda([ChildPropertyHandle = ChildPropertyHandle, BagStructProperty = InBagStructProperty]() { return Private::GetPinInfo(ChildPropertyHandle, BagStructProperty); }) .OnPinTypeChanged_Lambda([ChildPropertyHandle = ChildPropertyHandle, BagStructProperty = InBagStructProperty, PropUtils = InPropUtils](const FEdGraphPinType& PinType) { return Private::PinInfoChanged(ChildPropertyHandle, BagStructProperty, PropUtils, PinType); }) .Schema(GetDefault()) .bAllowContainers(bAllowContainers) .SelectorType(SelectorType) .TypeTreeFilter(ETypeTreeFilter::None) .Font(IDetailLayoutBuilder::GetDetailFont()) ]; } namespace Constants { static constexpr int32 MaxCategoryLength = 70; // Special case for categories. Alphanumeric, but including spaces and `|` for nested categories. static constexpr TCHAR InvalidCategoryCharacters[] = TEXT("\"',/.:&!?~\\\n\r\t@#(){}[]<>=;^%$`*+-"); } } // UE::StructUtils //----------------------------------------------------------------// // FPropertyBagInstanceDataDetails // - StructProperty is FInstancedPropertyBag // - ChildPropertyHandle a child property of the FInstancedPropertyBag::Value (FInstancedStruct) //----------------------------------------------------------------// /** Primary constructor. Values passed by parameter struct. */ FPropertyBagInstanceDataDetails::FPropertyBagInstanceDataDetails(const FPropertyBagInstanceDataDetails::FConstructParams& ConstructParams) : FInstancedStructDataDetails(ConstructParams.BagStructProperty.IsValid() ? ConstructParams.BagStructProperty->GetChildHandle(TEXT("Value")) : nullptr) , BagStructProperty(ConstructParams.BagStructProperty) , PropUtils(ConstructParams.PropUtils) , bAllowContainers(ConstructParams.bAllowContainers) , ChildRowFeatures(ConstructParams.ChildRowFeatures) PRAGMA_DISABLE_DEPRECATION_WARNINGS , bFixedLayout(false) , bAllowArrays(true) PRAGMA_ENABLE_DEPRECATION_WARNINGS { ensure(UE::StructUtils::Private::IsScriptStruct(BagStructProperty)); ensure(PropUtils != nullptr); } /** For backwards compatibility. */ FPropertyBagInstanceDataDetails::FPropertyBagInstanceDataDetails(TSharedPtr InStructProperty, const TSharedPtr& InPropUtils, const bool bInFixedLayout, const bool bInAllowContainers) : FInstancedStructDataDetails(InStructProperty.IsValid() ? InStructProperty->GetChildHandle(TEXT("Value")) : nullptr) , BagStructProperty(InStructProperty) , PropUtils(InPropUtils) , bAllowContainers(bInAllowContainers) , ChildRowFeatures(bInFixedLayout ? EPropertyBagChildRowFeatures::Fixed : EPropertyBagChildRowFeatures::Default) PRAGMA_DISABLE_DEPRECATION_WARNINGS , bFixedLayout(bInFixedLayout) // For backwards compatibility , bAllowArrays(bInAllowContainers) // For backwards compatibility PRAGMA_ENABLE_DEPRECATION_WARNINGS { ensure(UE::StructUtils::Private::IsScriptStruct(BagStructProperty)); ensure(PropUtils != nullptr); } void FPropertyBagInstanceDataDetails::OnGroupRowAdded(IDetailGroup& GroupRow, int32 Level, const FString& Category) const { using namespace UE::StructUtils; FDetailWidgetRow& FolderRow = GroupRow.HeaderRow(); TWeakPtr WeakSelf = SharedThis(this); FString FullCategoryName = GroupRow.GetGroupName().ToString(); /*** DRAG AND DROP HANDLER ***/ if (EnumHasAllFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::DragAndDrop | EPropertyBagChildRowFeatures::Menu_Categories)) { TSharedPtr DragDropHandler = MakeShared(); DragDropHandler->BindCanAcceptDragDrop( FCanAcceptPropertyBagDetailsRowDropOp::CreateLambda( [FullCategoryName](const TSharedPtr& DropOp, EItemDropZone DropZone) -> TOptional { if (!DropOp.IsValid() || DropZone != EItemDropZone::OntoItem) { DropOp->SetDecoration(EPropertyBagDropState::Invalid); return TOptional(); } TOptional DecorationOverride; if (Metadata::AreCategoriesEnabled(DropOp->PropertyDesc) && Metadata::GetCategory(DropOp->PropertyDesc).Equals(FullCategoryName)) { const FSlateBrush* Brush = FAppStyle::Get().GetBrush("Graph.ConnectorFeedback.OKWarn"); DecorationOverride.Emplace(LOCTEXT("OnSameCategoryDragDropDecoratorMessage", "Already in this category"), Brush); DropOp->SetDecoration(EPropertyBagDropState::SourceIsTarget, std::move(DecorationOverride)); return TOptional(); } const FSlateBrush* Brush = FAppStyle::Get().GetBrush("Graph.ConnectorFeedback.OK"); DecorationOverride.Emplace(LOCTEXT("OnNewCategoryDragDropDecoratorMessage", "Move to this category"), Brush); DropOp->SetDecoration(EPropertyBagDropState::Valid, std::move(DecorationOverride)); return DropZone; })); DragDropHandler->BindOnHandleDragDrop( FOnPropertyBagDetailsRowDropOp::CreateLambda( [WeakSelf, FullCategoryName, BagStructProperty = BagStructProperty, PropUtils = PropUtils](const FPropertyBagPropertyDesc& DroppedPropertyDesc, const EItemDropZone DropZone) -> FReply { if (ensure(DroppedPropertyDesc.CachedProperty && DropZone == EItemDropZone::OntoItem)) { const TSharedPtr DetailsSP = WeakSelf.Pin(); const UPropertyBag* ChildBagStruct = DetailsSP ? Private::GetCommonBagStruct(DetailsSP->BagStructProperty) : nullptr; // Validate these properties are still part of the bag. if (!ChildBagStruct || !ChildBagStruct->FindPropertyDescByProperty(DroppedPropertyDesc.CachedProperty)) { return FReply::Unhandled(); } Private::ApplyChangesToSinglePropertyDesc( LOCTEXT("DragToChangeCategory", "Change property category"), DroppedPropertyDesc, BagStructProperty, PropUtils, [&DroppedPropertyDesc, &FullCategoryName](FPropertyBagPropertyDesc& Desc) { Metadata::SetCategory(Desc, FullCategoryName); }); return FReply::Handled(); } return FReply::Unhandled(); })); // Add the drag and drop handler as a target for the folder row. FolderRow.DragDropHandler(std::move(DragDropHandler)); } PRAGMA_DISABLE_DEPRECATION_WARNINGS const bool bIsFixed = bFixedLayout || (ChildRowFeatures == EPropertyBagChildRowFeatures::Fixed); PRAGMA_ENABLE_DEPRECATION_WARNINGS /*** EDITABLE NAME BLOCK ***/ const TSharedPtr EditableInlineNameWidget = SNew(SInlineEditableTextBlock) .MultiLine(false) .OverflowPolicy(ETextOverflowPolicy::Ellipsis) .Font(IDetailLayoutBuilder::GetDetailFontBold()) .Text(FText::FromString(Category)) .OnVerifyTextChanged_Lambda([](const FText& InText, FText& OutErrorMessage) { if (InText.IsEmpty()) { OutErrorMessage = LOCTEXT("InlineEmptyCategoryName", "Name is empty"); return false; } else if (InText.ToString().Len() > Constants::MaxCategoryLength) { OutErrorMessage = LOCTEXT("InlineInvalidCategoryLength", "Too many characters"); return false; } else if (!FName::IsValidXName(InText.ToString(), Constants::InvalidCategoryCharacters)) { OutErrorMessage = LOCTEXT("InlineInvalidCategoryName", "Invalid character(s)"); return false; } return true; }) .OnTextCommitted_Lambda([FullCategoryName, Category = Category, BagStructProperty = BagStructProperty, PropUtils = PropUtils](const FText& InNewText, const ETextCommit::Type InCommitType) { if (InCommitType == ETextCommit::OnEnter || InCommitType == ETextCommit::OnUserMovedFocus) { using namespace UE::StructUtils; Private::ApplyChangesToPropertyDescs( LOCTEXT("InlineRenameCategory", "Rename category"), BagStructProperty, PropUtils, [&InNewText, OldCategory = FullCategoryName, &Category](TArray& PropertyDescs) { FString NewCategory = OldCategory; NewCategory.ReplaceInline(*Category, *InNewText.ToString(), ESearchCase::CaseSensitive); for (FPropertyBagPropertyDesc& Desc : PropertyDescs) { if (Metadata::AreCategoriesEnabled(Desc)) { FString DescCategory = Metadata::GetCategory(Desc); if (DescCategory.StartsWith(OldCategory)) { DescCategory.ReplaceInline(*OldCategory, *NewCategory); Metadata::SetCategory(Desc, DescCategory); } } } }); } }) .IsReadOnly(bIsFixed || !EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::Renaming)); /*** CATEGORY NAME AND BUTTONS ***/ const TSharedPtr NameContent = SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) .Content() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() .Padding(1, 0) [ EditableInlineNameWidget.ToSharedRef() ] + SHorizontalBox::Slot() .FillWidth(1) [ SNew(SSpacer).Size(1) ] + SHorizontalBox::Slot() .HAlign(HAlign_Right) .AutoWidth() [ SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText(LOCTEXT("DeleteCategory", "Delete this category.")) .OnClicked_Lambda([GroupName = GroupRow.GetGroupName(), BagStructProperty = BagStructProperty, PropUtils = PropUtils]() { Private::ApplyChangesToPropertyDescs( LOCTEXT("OnCategoryDeleted", "Delete category"), BagStructProperty, PropUtils, [GroupName](TArray& PropertyDescs) { for (FPropertyBagPropertyDesc& Desc : PropertyDescs) { if (Metadata::GetCategory(Desc).Equals(GroupName.ToString())) { Metadata::RemoveCategory(Desc); } } }); return FReply::Handled(); }) .ButtonStyle(FAppStyle::Get(), "SimpleButton") [ SNew(SImage) .DesiredSizeOverride(FVector2D(16)) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FAppStyle::Get().GetBrush("Icons.Delete")) ] ] ]; // Mirrors PropertyEditorConstants::GetRowBackgroundColor, which is private. NameContent->SetBorderBackgroundColor(TAttribute::CreateLambda([NameContent, Level]() { int32 ColorIndex = 0; int32 Increment = 1; for (int i = 0; i < Level + 1; ++i) { ColorIndex += Increment; if (ColorIndex == 0 || ColorIndex == 3) { Increment = -Increment; } } static constexpr uint8 ColorOffsets[] = { 0, 4, (4 + 2), (6 + 4), (10 + 6) }; const FSlateColor BaseSlateColor = NameContent->IsHovered() ? FAppStyle::Get().GetSlateColor("Colors.Header") : FAppStyle::Get().GetSlateColor("Colors.Panel"); const FColor BaseColor = BaseSlateColor.GetSpecifiedColor().ToFColor(true); const FColor ColorWithOffset( BaseColor.R + ColorOffsets[ColorIndex], BaseColor.G + ColorOffsets[ColorIndex], BaseColor.B + ColorOffsets[ColorIndex]); return FSlateColor(FLinearColor::FromSRGBColor(ColorWithOffset)); })); FolderRow .ShouldAutoExpand(true) .WholeRowContent() .HAlign(HAlign_Fill) [ NameContent.ToSharedRef() ]; } void FPropertyBagInstanceDataDetails::OnChildRowAdded(IDetailPropertyRow& ChildRow) { TSharedPtr NameWidget; TSharedPtr PropertyValueWidget; FDetailWidgetRow DetailWidgetRow; ChildRow.GetDefaultWidgets(NameWidget, PropertyValueWidget, DetailWidgetRow); TSharedPtr ChildPropertyHandle = ChildRow.GetPropertyHandle(); check(ChildPropertyHandle); TWeakPtr WeakSelf = SharedThis(this); const UPropertyBag* BagStruct = BagStructProperty ? UE::StructUtils::Private::GetCommonBagStruct(BagStructProperty) : nullptr; const FProperty* ChildProperty = ChildPropertyHandle->GetProperty(); const FPropertyBagPropertyDesc* PropertyDesc = BagStruct ? BagStruct->FindPropertyDescByProperty(ChildProperty) : nullptr; PRAGMA_DISABLE_DEPRECATION_WARNINGS const bool bIsFixed = bFixedLayout || ChildRowFeatures == EPropertyBagChildRowFeatures::Fixed; PRAGMA_ENABLE_DEPRECATION_WARNINGS // Validate data and check if it's editable if (ChildProperty->HasMetaData(UE::StructUtils::Metadata::HideInDetailPanelsName)) { ChildRow.Visibility(EVisibility::Collapsed); return; } bool bEditable = BagStructProperty->IsEditable(); /*** WARNINGS FOR PROPERTY ISSUES ***/ FText WarningOnProperty; // This message will supplement a warning icon on the details view child row, which will show if not empty. if (!ensure(PropertyDesc) || PropertyDesc->ContainerTypes.Num() > 1) { // The property editing for nested containers is not supported. WarningOnProperty = LOCTEXT("NestedContainersWarning", "This property type (nested container) is not supported in the property bag UI."); } else if ((PropertyDesc->ValueType == EPropertyBagPropertyType::UInt32 || PropertyDesc->ValueType == EPropertyBagPropertyType::UInt64) && !bIsFixed) { // Warn that the unsigned types cannot be set via the type selection. WarningOnProperty = LOCTEXT("UnsignedTypesWarning", "Unsigned types are not supported through the property type selection. If you change the type, you will not be able to change it back."); } else if (UE::StructUtils::Private::HasMissingType(ChildPropertyHandle)) { WarningOnProperty = LOCTEXT("MissingTypeWarning", "The property is missing type. The Struct, Enum, or Object may have been removed."); } else if (!FInstancedPropertyBag::IsPropertyNameValid(UE::StructUtils::Private::GetPropertyNameSafe(ChildPropertyHandle))) { WarningOnProperty = LOCTEXT("InvalidNameWarning", "The property's name contains invalid characters. Dynamically named properties with invalid characters may be rejected in future releases."); } /*** OVERRIDE RESET TO DEFAULT ACTION FOR BAG OVERRIDES ***/ if (HasPropertyOverrides()) { TAttribute EditConditionValue = TAttribute::CreateLambda( [WeakSelf, ChildPropertyHandle]() -> bool { if (const TSharedPtr Self = WeakSelf.Pin()) { return Self->IsPropertyOverridden(ChildPropertyHandle) == EPropertyOverrideState::Yes; } return true; }); FOnBooleanValueChanged OnEditConditionChanged = FOnBooleanValueChanged::CreateLambda([WeakSelf, ChildPropertyHandle](bool bNewValue) { if (const TSharedPtr Self = WeakSelf.Pin()) { Self->SetPropertyOverride(ChildPropertyHandle, bNewValue); } }); ChildRow.EditCondition(std::move(EditConditionValue), std::move(OnEditConditionChanged)); FIsResetToDefaultVisible IsResetVisible = FIsResetToDefaultVisible::CreateLambda([WeakSelf](TSharedPtr PropertyHandle) { if (const TSharedPtr Self = WeakSelf.Pin()) { return !Self->IsDefaultValue(PropertyHandle); } return false; }); FResetToDefaultHandler ResetHandler = FResetToDefaultHandler::CreateLambda([WeakSelf](TSharedPtr PropertyHandle) { if (const TSharedPtr Self = WeakSelf.Pin()) { Self->ResetToDefault(PropertyHandle); } }); FResetToDefaultOverride ResetOverride = FResetToDefaultOverride::Create(IsResetVisible, ResetHandler); ChildRow.OverrideResetToDefault(ResetOverride); } if (!bIsFixed) { /*** BUILD PROPERTY NAME WIDGET ***/ TSharedRef PropertyDetailsWidget = SNew(SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Right) .Padding(3, 0) .AutoWidth() [ SNew(SBox) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .ToolTipText(WarningOnProperty) .Visibility_Lambda([WarningOnProperty]() { return WarningOnProperty.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible; }) .DesiredSizeOverride(FVector2D(12)) .ColorAndOpacity(FLinearColor(1.f, 0.8f, 0.f, 1.f)) .Image(FAppStyle::GetBrush("Icons.Error")) ] ]; if (EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::CompactTypeSelector)) { PropertyDetailsWidget->AddSlot() .HAlign(HAlign_Left) .Padding(1, 0) .AutoWidth() [ UE::StructUtils::CreateTypeSelectionWidget(ChildPropertyHandle, BagStructProperty, PropUtils, SPinTypeSelector::ESelectorType::Compact, bAllowContainers) ]; } /*** EDITABLE NAME BLOCK ***/ TSharedPtr EditableInlineNameWidget = SNew(SInlineEditableTextBlock) .IsReadOnly(!EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::Renaming)) .Font(IDetailLayoutBuilder::GetDetailFont()) .MultiLine(false) .OverflowPolicy(ETextOverflowPolicy::Ellipsis) .Text_Lambda([ChildPropertyHandle]() { const bool bIsBool = ChildPropertyHandle && ChildPropertyHandle->GetPropertyClass() == FBoolProperty::StaticClass(); const FName PropertyName = UE::StructUtils::Private::GetPropertyNameSafe(ChildPropertyHandle); return FText::FromString(FName::NameToDisplayString(PropertyName.ToString(), bIsBool)); }) .OnVerifyTextChanged_Lambda([BagStructProperty = BagStructProperty, ChildPropertyHandle](const FText& InText, FText& OutErrorMessage) { if (InText.IsEmpty()) { OutErrorMessage = LOCTEXT("InlineEmptyPropertyName", "Name is empty"); return false; } // Check for invalid characters upon renaming. if (!FInstancedPropertyBag::IsPropertyNameValid(InText.ToString())) { OutErrorMessage = LOCTEXT("InlineInvalidPropertyName", "Invalid character(s)"); return false; } const FName OldName = UE::StructUtils::Private::GetPropertyNameSafe(ChildPropertyHandle); // Bypass if the name is the exact same. if (InText.ToString().Equals(OldName.ToString())) { return true; } // Sanitize out any other characters that we allowed for convenience but are not valid, like spaces. const FName NewName = FInstancedPropertyBag::SanitizePropertyName(InText.ToString()); // Bypass if sanitized name is the same. if (NewName == OldName) { return true; } if (!UE::StructUtils::Private::IsUniqueName(NewName, OldName, BagStructProperty)) { OutErrorMessage = LOCTEXT("InlinePropertyUniqueName", "Property must have unique name"); return false; } // Name is OK. return true; }) .OnTextCommitted_Lambda([BagStructProperty = BagStructProperty, PropUtils = PropUtils, ChildPropertyHandle](const FText& InNewText, ETextCommit::Type InCommitType) { if (InCommitType == ETextCommit::OnCleared) { return; } const FName NewName = FInstancedPropertyBag::SanitizePropertyName(InNewText.ToString()); const FName OldName = UE::StructUtils::Private::GetPropertyNameSafe(ChildPropertyHandle); if (!ensureMsgf(UE::StructUtils::Private::IsUniqueName(NewName, OldName, BagStructProperty), TEXT("Should have already been addressed in OnVerifyTextChanged."))) { return; } UE::StructUtils::Private::ApplyChangesToPropertyDescs( FText::Format(LOCTEXT("OnPropertyNameChanged", "Change property name: {0} -> {1}"), FText::FromName(OldName), FText::FromName(NewName)), BagStructProperty, PropUtils, [&NewName, &ChildPropertyHandle](TArray& PropertyDescs) { const FProperty* Property = ChildPropertyHandle->GetProperty(); if (FPropertyBagPropertyDesc* Desc = PropertyDescs.FindByPredicate([Property](const FPropertyBagPropertyDesc& Desc) { return Desc.CachedProperty == Property; })) { Desc->Name = NewName; } }); }); /*** CURRENT UI AS IT BECOMES DEPRECATED ***/ // Deprecated in 5.6 - the combo button on the name widget will be removed in favor of the new drop-down menu. if (EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::Deprecated)) { // Add the widget to the property bar. PropertyDetailsWidget->AddSlot() .HAlign(HAlign_Left) .AutoWidth() .Padding(0, 0, 3, 0) [ SNew(SBox) .Content() [ SNew(SComboButton) .MenuContent() [ PRAGMA_DISABLE_DEPRECATION_WARNINGS OnPropertyNameContent(ChildPropertyHandle, EditableInlineNameWidget) PRAGMA_ENABLE_DEPRECATION_WARNINGS ] .ContentPadding(FMargin(0, 0, 2, 0)) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ButtonContent() [ EditableInlineNameWidget.ToSharedRef() ] ] ]; } else // No deprecated combo box. Just add the name. { PropertyDetailsWidget->AddSlot() .HAlign(HAlign_Left) .AutoWidth() .Padding(0, 0, 3, 0) [ EditableInlineNameWidget.ToSharedRef() ]; } // Extendable spacer between the name and the drop-down PropertyDetailsWidget->AddSlot() .FillWidth(1) [ SNew(SSpacer) .Size(1) ]; /*** ACCESS SPECIFIER BUTTON ***/ if (EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::AccessSpecifierButton)) { using namespace UE::StructUtils::Metadata; PropertyDetailsWidget->AddSlot() .HAlign(HAlign_Right) .VAlign(VAlign_Center) .AutoWidth() .Padding(1, 0) [ SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText(LOCTEXT("SetAccessSpecifier", "Set the access specifier on the property to Public or Private.")) .OnClicked_Lambda([ChildPropertyHandle, BagStructProperty = BagStructProperty, PropUtils = PropUtils]() { FProperty* Property = ChildPropertyHandle->GetProperty(); UE::StructUtils::Private::ApplyChangesToPropertyDescs( LOCTEXT("OnPropertyAccessSpecifierChanged", "Set access specifier."), BagStructProperty, PropUtils, [Property, BagStructProperty](TArray& PropertyDescs) { if (FPropertyBagPropertyDesc* Desc = PropertyDescs.FindByPredicate([Property, BagStructProperty](const FPropertyBagPropertyDesc& Desc) { return Desc.CachedProperty == Property; })) { BagStructProperty->NotifyPreChange(); const bool bIsPrivate = Property->HasAnyPropertyFlags(CPF_NativeAccessSpecifierPrivate | CPF_NativeAccessSpecifierProtected); Desc->PropertyFlags &= ~CPF_NativeAccessSpecifiers; if (bIsPrivate) { Desc->PropertyFlags |= CPF_NativeAccessSpecifierPublic; } else { Desc->PropertyFlags |= CPF_NativeAccessSpecifierPrivate; } BagStructProperty->NotifyPostChange(EPropertyChangeType::ValueSet); BagStructProperty->NotifyFinishedChangingProperties(); } }); return FReply::Handled(); }) .ButtonStyle(FAppStyle::Get(), "SimpleButton") [ SNew(SImage) .DesiredSizeOverride(FVector2D(16)) .ColorAndOpacity(FSlateColor::UseForeground()) .Image_Lambda([ChildPropertyHandle]() { check(ChildPropertyHandle); if (const FProperty* Property = ChildPropertyHandle->GetProperty()) { // For now, treat protected as private. TODO: Add toggle for protected. if (Property->HasAnyPropertyFlags(CPF_NativeAccessSpecifierPrivate | CPF_NativeAccessSpecifierProtected)) { return FAppStyle::Get().GetBrush("Icons.Visible"); } } return FAppStyle::Get().GetBrush("Icons.Hidden"); }) ] ]; } /*** DROP-DOWN MENU OPTIONS ***/ // Check drop-down is enabled and at least one option as well. if (EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::DropDownMenuButton) && EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::AllMenuOptions)) { static constexpr bool bInShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bInShouldCloseWindowAfterMenuSelection, nullptr); if (EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::Menu_TypeSelector)) { MenuBuilder.BeginSection(/*InExtensionHook=*/NAME_None, LOCTEXT("DropDownMenuSectionTypeSelector", "Type")); MenuBuilder.AddWidget( UE::StructUtils::CreateTypeSelectionWidget( ChildPropertyHandle, BagStructProperty, PropUtils, SPinTypeSelector::ESelectorType::Full, bAllowContainers), /*InLabel=*/FText::GetEmpty()); MenuBuilder.EndSection(); } const bool bMenuRenameEnabled = EnumHasAllFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::Renaming | EPropertyBagChildRowFeatures::Menu_Rename); const bool bMenuDeleteEnabled = EnumHasAllFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::Deletion | EPropertyBagChildRowFeatures::Menu_Delete); if (bMenuRenameEnabled | bMenuDeleteEnabled) { MenuBuilder.BeginSection(/*InExtensionHook=*/NAME_None, LOCTEXT("DropDownMenuSectionGeneral", "General")); // Must have property renaming enabled or the editable inline widget will be invalid. if (bMenuRenameEnabled) { MenuBuilder.AddMenuEntry( LOCTEXT("DropDownMenuRenameProperty", "Rename property"), LOCTEXT("DropDownMenuRenamePropertyToolTip", "Enable the inline property renaming."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Edit"), FUIAction(FExecuteAction::CreateLambda([NameWidget = EditableInlineNameWidget]() { if (NameWidget) { NameWidget->EnterEditingMode(); } }))); } if (bMenuDeleteEnabled) { MenuBuilder.AddMenuEntry( LOCTEXT("DropDownMenuRemoveProperty", "Remove property"), LOCTEXT("DropDownMenuRemovePropertyToolTip", "Delete the property."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete"), FUIAction(FExecuteAction::CreateLambda([BagStructProperty = BagStructProperty, PropUtils = PropUtils, ChildPropertyHandle]() { UE::StructUtils::Private::DeleteProperty(BagStructProperty, ChildPropertyHandle, PropUtils); })) ); } MenuBuilder.EndSection(); } // The property's category (grouping) can be edited here. if (EnumHasAllFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::Categories | EPropertyBagChildRowFeatures::Menu_Categories)) { MenuBuilder.BeginSection(/*InExtensionHook=*/NAME_None, LOCTEXT("DropDownMenuSectionCategory", "Category")); if (ChildPropertyHandle && ChildPropertyHandle->HasMetaData(UE::StructUtils::Metadata::CategoryName)) { MenuBuilder.AddMenuEntry( LOCTEXT("DropDownMenuClearCategory", "Clear category"), LOCTEXT("DropDownMenuClearCategoryToolTip", "Remove the property from its current category."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete"), FUIAction(FExecuteAction::CreateLambda([ChildPropertyHandle, StructProperty = BagStructProperty, PropUtils = PropUtils]() { UE::StructUtils::Private::ApplyChangesToSinglePropertyDesc( LOCTEXT("DropDownMenuOnCategoryCleared", "Clear property category"), ChildPropertyHandle, StructProperty, PropUtils, [](FPropertyBagPropertyDesc& Desc) { UE::StructUtils::Metadata::RemoveCategory(Desc); }); }))); } // TODO: AddVerifiedEditableText seems to bypass the MenuBuilder Section, so this was added temporarily to force the section to exist. MenuBuilder.AddWidget(SNullWidget::NullWidget, FText::GetEmpty(), false, false); MenuBuilder.AddVerifiedEditableText( LOCTEXT("DropDownMenuSubMenuCategoryName", "Category"), LOCTEXT("DropDownMenuSubMenuCategoryTooltip", "Edit this value to change the category of this property. Subcategories can be created with the '|' character."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.FolderOpen"), TAttribute::CreateLambda([ChildPropertyHandle]() { FText GroupLabel = FText::FromString(""); if (ChildPropertyHandle && ChildPropertyHandle->HasMetaData(UE::StructUtils::Metadata::CategoryName)) { GroupLabel = FText::FromString(ChildPropertyHandle->GetMetaData(UE::StructUtils::Metadata::CategoryName)); } return GroupLabel; }), FOnVerifyTextChanged::CreateLambda([](const FText& InText, FText& OutMessage) { if (InText.ToString().Len() > UE::StructUtils::Constants::MaxCategoryLength) { OutMessage = LOCTEXT("DropDownMenuInvalidCategoryName", "Invalid category name"); return false; } else { return true; } }), FOnTextCommitted::CreateLambda([StructProperty = BagStructProperty, PropUtils = PropUtils, ChildPropertyHandle](const FText& CommittedText, const ETextCommit::Type CommitType) { if (CommitType == ETextCommit::OnEnter || CommitType == ETextCommit::OnUserMovedFocus) { UE::StructUtils::Private::ApplyChangesToSinglePropertyDesc( LOCTEXT("DropDownMenuOnCategoryEdited", "Edit property category"), ChildPropertyHandle, StructProperty, PropUtils, [&CommittedText](FPropertyBagPropertyDesc& Desc) { UE::StructUtils::Metadata::SetCategory(Desc, CommittedText.ToString()); }); } }), FOnTextChanged(), !bEditable); MenuBuilder.EndSection(); } /*** DROP-DOWN ARROW MENU ***/ PropertyDetailsWidget->AddSlot() .HAlign(HAlign_Right) .AutoWidth() .Padding(0, 0, 5, 0) [ SNew(SBox) .Content() [ SNew(SComboButton) .MenuContent() [ MenuBuilder.MakeWidget() ] .HasDownArrow(true) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ButtonContent() [ SNullWidget::NullWidget ] ] ]; } /*** DRAG AND DROP HANDLER ***/ if (EnumHasAnyFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::DragAndDrop) && PropertyDesc != nullptr) { TSharedPtr DragDropHandler = MakeShared(*PropertyDesc); // Can accept drag and drop check for if this is a valid drop DragDropHandler->BindCanAcceptDragDrop( FCanAcceptPropertyBagDetailsRowDropOp::CreateLambda( [PropertyDesc](const TSharedPtr& DropOp, EItemDropZone DropZone) -> TOptional { if (!PropertyDesc || !DropOp.IsValid()) { DropOp->SetDecoration(EPropertyBagDropState::Invalid); return TOptional(); } if (DropZone == EItemDropZone::OntoItem && PropertyDesc->ID != DropOp->PropertyDesc.ID) { DropOp->SetDecoration(EPropertyBagDropState::Invalid); return TOptional(); } // No effect to drop in these cases. Either source == target, or moving source above/below target puts source in same location. if (*PropertyDesc == DropOp->PropertyDesc || (DropZone == EItemDropZone::AboveItem && DropOp->PropertyDesc.GetCachedIndex() == PropertyDesc->GetCachedIndex() - 1) || (DropZone == EItemDropZone::BelowItem && DropOp->PropertyDesc.GetCachedIndex() == PropertyDesc->GetCachedIndex() + 1)) { DropOp->SetDecoration(EPropertyBagDropState::SourceIsTarget); return TOptional(); } DropOp->SetDecoration(EPropertyBagDropState::Valid); return DropZone; })); DragDropHandler->BindOnHandleDragDrop( FOnPropertyBagDetailsRowDropOp::CreateLambda( [WeakSelf, PropertyDesc = *PropertyDesc, BagStructProperty = BagStructProperty, PropUtils = PropUtils](const FPropertyBagPropertyDesc& DroppedPropertyDesc, EItemDropZone DropZone) -> FReply { using namespace UE::StructUtils; const TSharedPtr DetailsSP = WeakSelf.Pin(); const UPropertyBag* ChildBagStruct = DetailsSP ? Private::GetCommonBagStruct(DetailsSP->BagStructProperty) : nullptr; // Validate these properties are still part of the bag. if (!ChildBagStruct || !ChildBagStruct->FindPropertyDescByProperty(PropertyDesc.CachedProperty) || !ChildBagStruct->FindPropertyDescByProperty(DroppedPropertyDesc.CachedProperty)) { return FReply::Unhandled(); } EPropertyBagAlterationResult Result = EPropertyBagAlterationResult::InternalError; DetailsSP->BagStructProperty->EnumerateRawData([DropZone, &PropertyDesc, &DroppedPropertyDesc, &Result](void* RawData, const int32 /*DataIndex*/, const int32 /*NumData*/) { if (FInstancedPropertyBag* PropertyBag = RawData ? static_cast(RawData) : nullptr) { Result = PropertyBag->ReorderProperty(DroppedPropertyDesc.Name, PropertyDesc.Name, DropZone == EItemDropZone::AboveItem); } return true; }); if (Result == EPropertyBagAlterationResult::Success) { Private::ApplyChangesToSinglePropertyDesc( LOCTEXT("DragDropReorderProperties", "Reordered properties"), DroppedPropertyDesc, BagStructProperty, PropUtils, [PropertyDesc](FPropertyBagPropertyDesc& Desc) { Metadata::SetCategory(Desc, Metadata::GetCategory(PropertyDesc)); }); return FReply::Handled(); } else { return FReply::Unhandled(); } })); // Bind the drag and drop handler for receiving. ChildRow.DragDropHandler(DragDropHandler); // Add draggability for the name widget. NameWidget = SNew(StructUtilsEditor::SDraggableBox) .DragDropHandler(DragDropHandler) .RequireDirectHover(true) .Content() [ PropertyDetailsWidget ]; // Add draggability for the value widget, maximizing draggable space, but not at the cost of the value widget. PropertyValueWidget = SNew(StructUtilsEditor::SDraggableBox) .DragDropHandler(DragDropHandler) .RequireDirectHover(true) .Content() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Left) .AutoWidth() [ PropertyValueWidget.ToSharedRef() ] + SHorizontalBox::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) .FillWidth(1) [ SNullWidget::NullWidget ] ]; } else // Update the name widget with our new property details composition. { NameWidget = PropertyDetailsWidget; } } /*** FINAL WIDGET ***/ ChildRow .IsEnabled(bEditable) .CustomWidget(/*bShowChildren=*/true) .NameContent() .HAlign(HAlign_Fill) [ NameWidget.ToSharedRef() ] .ValueContent() .HAlign(HAlign_Fill) [ PropertyValueWidget.ToSharedRef() ]; } FPropertyBagInstanceDataDetails::EPropertyOverrideState FPropertyBagInstanceDataDetails::IsPropertyOverridden(TSharedPtr ChildPropertyHandle) const { if (!ChildPropertyHandle) { return EPropertyOverrideState::Undetermined;; } int32 NumValues = 0; int32 NumOverrides = 0; const FProperty* Property = ChildPropertyHandle->GetProperty(); check(Property); EnumeratePropertyBags(BagStructProperty, [Property, &NumValues, &NumOverrides] (const FInstancedPropertyBag& DefaultPropertyBag, const FInstancedPropertyBag& PropertyBag, const IPropertyBagOverrideProvider& OverrideProvider) { NumValues++; if (const UPropertyBag* Bag = PropertyBag.GetPropertyBagStruct()) { const FPropertyBagPropertyDesc* PropertyDesc = Bag->FindPropertyDescByProperty(Property); if (PropertyDesc && OverrideProvider.IsPropertyOverridden(PropertyDesc->ID)) { NumOverrides++; } } return true; }); if (NumOverrides == 0) { return EPropertyOverrideState::No; } else if (NumOverrides == NumValues) { return EPropertyOverrideState::Yes; } return EPropertyOverrideState::Undetermined; } void FPropertyBagInstanceDataDetails::SetPropertyOverride(TSharedPtr ChildPropertyHandle, const bool bIsOverridden) { if (!ChildPropertyHandle) { return; } const FProperty* Property = ChildPropertyHandle->GetProperty(); check(Property); FScopedTransaction Transaction(FText::Format(LOCTEXT("OverrideChange", "Change Override for {0}"), FText::FromName(ChildPropertyHandle->GetProperty()->GetFName()))); PreChangeOverrides(); EnumeratePropertyBags( BagStructProperty, [Property, bIsOverridden] (const FInstancedPropertyBag& DefaultPropertyBag, const FInstancedPropertyBag& PropertyBag, const IPropertyBagOverrideProvider& OverrideProvider) { if (const UPropertyBag* Bag = PropertyBag.GetPropertyBagStruct()) { if (const FPropertyBagPropertyDesc* PropertyDesc = Bag->FindPropertyDescByProperty(Property)) { OverrideProvider.SetPropertyOverride(PropertyDesc->ID, bIsOverridden); } } return true; }); PostChangeOverrides(); } bool FPropertyBagInstanceDataDetails::IsDefaultValue(TSharedPtr ChildPropertyHandle) const { if (!ChildPropertyHandle) { return true; } int32 NumValues = 0; int32 NumOverridden = 0; int32 NumIdentical = 0; const FProperty* Property = ChildPropertyHandle->GetProperty(); check(Property); EnumeratePropertyBags( BagStructProperty, [Property, &NumValues, &NumOverridden, &NumIdentical] (const FInstancedPropertyBag& DefaultPropertyBag, const FInstancedPropertyBag& PropertyBag, const IPropertyBagOverrideProvider& OverrideProvider) { NumValues++; const UPropertyBag* DefaultBag = DefaultPropertyBag.GetPropertyBagStruct(); const UPropertyBag* Bag = PropertyBag.GetPropertyBagStruct(); if (Bag && DefaultBag) { const FPropertyBagPropertyDesc* PropertyDesc = Bag->FindPropertyDescByProperty(Property); const FPropertyBagPropertyDesc* DefaultPropertyDesc = DefaultBag->FindPropertyDescByProperty(Property); if (PropertyDesc && DefaultPropertyDesc && OverrideProvider.IsPropertyOverridden(PropertyDesc->ID)) { NumOverridden++; if (UE::StructUtils::Private::ArePropertiesIdentical(DefaultPropertyDesc, DefaultPropertyBag, PropertyDesc, PropertyBag)) { NumIdentical++; } } } return true; }); if (NumOverridden == NumIdentical) { return true; } return false; } void FPropertyBagInstanceDataDetails::ResetToDefault(TSharedPtr ChildPropertyHandle) { if (!ChildPropertyHandle) { return; } const FProperty* Property = ChildPropertyHandle->GetProperty(); check(Property); FScopedTransaction Transaction(FText::Format(LOCTEXT("ResetToDefault", "Reset {0} to default value"), FText::FromName(ChildPropertyHandle->GetProperty()->GetFName()))); ChildPropertyHandle->NotifyPreChange(); EnumeratePropertyBags( BagStructProperty, [Property] (const FInstancedPropertyBag& DefaultPropertyBag, FInstancedPropertyBag& PropertyBag, const IPropertyBagOverrideProvider& OverrideProvider) { const UPropertyBag* DefaultBag = DefaultPropertyBag.GetPropertyBagStruct(); const UPropertyBag* Bag = PropertyBag.GetPropertyBagStruct(); if (Bag && DefaultBag) { const FPropertyBagPropertyDesc* PropertyDesc = Bag->FindPropertyDescByProperty(Property); const FPropertyBagPropertyDesc* DefaultPropertyDesc = DefaultBag->FindPropertyDescByProperty(Property); if (PropertyDesc && DefaultPropertyDesc && OverrideProvider.IsPropertyOverridden(PropertyDesc->ID)) { UE::StructUtils::Private::CopyPropertyValue(DefaultPropertyDesc, DefaultPropertyBag, PropertyDesc, PropertyBag); } } return true; }); ChildPropertyHandle->NotifyPostChange(EPropertyChangeType::ValueSet); ChildPropertyHandle->NotifyFinishedChangingProperties(); } TSharedRef FPropertyBagInstanceDataDetails::OnPropertyNameContent(TSharedPtr ChildPropertyHandle, TSharedPtr InlineWidget) const { constexpr bool bInShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bInShouldCloseWindowAfterMenuSelection, nullptr); auto MoveProperty = [BagStructProperty = BagStructProperty, PropUtils = PropUtils, ChildPropertyHandle](const int32 Delta) { if (!BagStructProperty || !BagStructProperty->IsValidHandle() || !ChildPropertyHandle || !ChildPropertyHandle->IsValidHandle()) { return; } UE::StructUtils::Private::ApplyChangesToPropertyDescs( LOCTEXT("OnPropertyMoved", "Move Property"), BagStructProperty, PropUtils, [&ChildPropertyHandle, &Delta](TArray& PropertyDescs) { // Move if (PropertyDescs.Num() > 1) { const FProperty* Property = ChildPropertyHandle ? ChildPropertyHandle->GetProperty() : nullptr; const int32 PropertyIndex = PropertyDescs.IndexOfByPredicate([Property](const FPropertyBagPropertyDesc& Desc){ return Desc.CachedProperty == Property; }); if (PropertyIndex != INDEX_NONE) { const int32 NewPropertyIndex = FMath::Clamp(PropertyIndex + Delta, 0, PropertyDescs.Num() - 1); PropertyDescs.Swap(PropertyIndex, NewPropertyIndex); } } }); }; MenuBuilder.AddWidget( SNew(SBox) .HAlign(HAlign_Right) .Padding(FMargin(12, 0, 12, 0)) [ UE::StructUtils::CreateTypeSelectionWidget(ChildPropertyHandle, BagStructProperty, PropUtils, SPinTypeSelector::ESelectorType::Full, bAllowContainers) ], FText::GetEmpty()); MenuBuilder.AddSeparator(); MenuBuilder.AddMenuEntry( LOCTEXT("Rename", "Rename"), LOCTEXT("Rename_ToolTip", "Rename property"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Edit"), FUIAction(FExecuteAction::CreateLambda([InlineWidget]() { InlineWidget->EnterEditingMode(); })) ); MenuBuilder.AddMenuEntry( LOCTEXT("Remove", "Remove"), LOCTEXT("Remove_ToolTip", "Remove property"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete"), FUIAction(FExecuteAction::CreateLambda([BagStructProperty = BagStructProperty, PropUtils = PropUtils, ChildPropertyHandle]() { UE::StructUtils::Private::DeleteProperty(BagStructProperty, ChildPropertyHandle, PropUtils); })) ); MenuBuilder.AddSeparator(); MenuBuilder.AddMenuEntry( LOCTEXT("MoveUp", "Move Up"), LOCTEXT("MoveUp_ToolTip", "Move property up in the list"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.ArrowUp"), FUIAction(FExecuteAction::CreateLambda(MoveProperty, -1)) ); MenuBuilder.AddMenuEntry( LOCTEXT("MoveDown", "Move Down"), LOCTEXT("MoveDown_ToolTip", "Move property down in the list"), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.ArrowDown"), FUIAction(FExecuteAction::CreateLambda(MoveProperty, +1)) ); return MenuBuilder.MakeWidget(); } //////////////////////////////////// TSharedRef FPropertyBagDetails::MakeInstance() { return MakeShared(); } void FPropertyBagDetails::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { PropUtils = StructCustomizationUtils.GetPropertyUtilities(); StructProperty = StructPropertyHandle; check(StructProperty); if (const FProperty* MetaDataProperty = StructProperty->GetMetaDataProperty()) { PRAGMA_DISABLE_DEPRECATION_WARNINGS bFixedLayout = MetaDataProperty->HasMetaData(UE::StructUtils::Metadata::FixedLayoutName); PRAGMA_ENABLE_DEPRECATION_WARNINGS bAllowContainers = MetaDataProperty->HasMetaData(UE::StructUtils::Metadata::AllowContainersName) ? MetaDataProperty->GetBoolMetaData(UE::StructUtils::Metadata::AllowContainersName) : true; if (MetaDataProperty->HasMetaData(UE::StructUtils::Metadata::DefaultTypeName)) { if (UEnum* Enum = StaticEnum()) { int32 EnumIndex = Enum->GetIndexByNameString(MetaDataProperty->GetMetaData(UE::StructUtils::Metadata::DefaultTypeName)); if (EnumIndex != INDEX_NONE) { DefaultType = EPropertyBagPropertyType(Enum->GetValueByIndex(EnumIndex)); } } } // Load the feature set by the metadata set on the FPropertyBag. Can only accept explicit enum values currently. // TODO: Enable the option to parse a bitflag expression from string. I.E. 'Renaming | DropDownMenuButton | AllMenuOptions' if (MetaDataProperty->HasMetaData(UE::StructUtils::Metadata::ChildRowFeaturesName)) { if (const UEnum* Enum = StaticEnum()) { const int32 EnumIndex = Enum->GetIndexByNameString(MetaDataProperty->GetMetaData(UE::StructUtils::Metadata::ChildRowFeaturesName)); if (EnumIndex != INDEX_NONE) { ChildRowFeatures = static_cast(Enum->GetValueByIndex(EnumIndex)); } } } // Don't show the header if ShowOnlyInnerProperties is set if (MetaDataProperty->HasMetaData(UE::StructUtils::Metadata::ShowOnlyInnerPropertiesName)) { return; } } TSharedPtr ValueWidget = SNullWidget::NullWidget; PRAGMA_DISABLE_DEPRECATION_WARNINGS if (!bFixedLayout && ChildRowFeatures != EPropertyBagChildRowFeatures::Fixed) PRAGMA_ENABLE_DEPRECATION_WARNINGS { SAssignNew(ValueWidget, SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ MakeAddPropertyWidget(StructProperty, PropUtils, DefaultType).ToSharedRef() ]; } HeaderRow .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() .VAlign(VAlign_Center) [ ValueWidget.ToSharedRef() ] .ShouldAutoExpand(true); } void FPropertyBagDetails::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { FPropertyBagInstanceDataDetails::FConstructParams Params { .BagStructProperty = StructProperty, .PropUtils = PropUtils, .bAllowContainers = bAllowContainers, PRAGMA_DISABLE_DEPRECATION_WARNINGS .ChildRowFeatures = bFixedLayout ? EPropertyBagChildRowFeatures::Fixed : ChildRowFeatures PRAGMA_ENABLE_DEPRECATION_WARNINGS }; // Show the Value (FInstancedStruct) as child rows. const TSharedRef InstanceDetails = MakeShared(Params); StructBuilder.AddCustomBuilder(InstanceDetails); } TSharedPtr FPropertyBagDetails::MakeAddPropertyWidget(TSharedPtr InStructProperty, TSharedPtr InPropUtils, EPropertyBagPropertyType DefaultType, const FSlateColor IconColor) { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .VAlign(VAlign_Center) .HAlign(HAlign_Center) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText(LOCTEXT("AddProperty_Tooltip", "Add new property")) .OnClicked_Lambda([StructProperty = InStructProperty, PropUtils = InPropUtils, DefaultType]() { constexpr int32 MaxIterations = 100; FName NewName(TEXT("NewProperty")); int32 Number = 1; while (!UE::StructUtils::Private::IsUniqueName(NewName, FName(), StructProperty) && Number < MaxIterations) { Number++; NewName.SetNumber(Number); } if (Number == MaxIterations) { return FReply::Handled(); } UE::StructUtils::Private::ApplyChangesToPropertyDescs( LOCTEXT("OnPropertyAdded", "Add Property"), StructProperty, PropUtils, [&NewName, DefaultType](TArray& PropertyDescs) { PropertyDescs.Emplace(NewName, DefaultType); }); return FReply::Handled(); }) .Content() [ SNew(SImage) .Image(FAppStyle::GetBrush("Icons.PlusCircle")) .ColorAndOpacity(IconColor) ] ]; } //////////////////////////////////// bool UPropertyBagSchema::SupportsPinTypeContainer(TWeakPtr SchemaAction, const FEdGraphPinType& PinType, const EPinContainerType& ContainerType) const { return ContainerType == EPinContainerType::None || ContainerType == EPinContainerType::Array || ContainerType == EPinContainerType::Set; } #undef LOCTEXT_NAMESPACE