2296 lines
79 KiB
C++
2296 lines
79 KiB
C++
// 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<UPropertyBagSchema>();
|
|
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<UEnum>(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<EPropertyBagPropertyType>();
|
|
check(PropertyTypeEnum);
|
|
const UPropertyBagSchema* Schema = GetDefault<UPropertyBagSchema>();
|
|
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<UObject*>(Desc.ValueTypeObject.Get());
|
|
break;
|
|
case EPropertyBagPropertyType::Struct:
|
|
PinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
|
|
PinType.PinSubCategoryObject = const_cast<UObject*>(Desc.ValueTypeObject.Get());
|
|
break;
|
|
case EPropertyBagPropertyType::Object:
|
|
PinType.PinCategory = UEdGraphSchema_K2::PC_Object;
|
|
PinType.PinSubCategoryObject = const_cast<UObject*>(Desc.ValueTypeObject.Get());
|
|
break;
|
|
case EPropertyBagPropertyType::SoftObject:
|
|
PinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject;
|
|
PinType.PinSubCategoryObject = const_cast<UObject*>(Desc.ValueTypeObject.Get());
|
|
break;
|
|
case EPropertyBagPropertyType::Class:
|
|
PinType.PinCategory = UEdGraphSchema_K2::PC_Class;
|
|
PinType.PinSubCategoryObject = const_cast<UObject*>(Desc.ValueTypeObject.Get());
|
|
break;
|
|
case EPropertyBagPropertyType::SoftClass:
|
|
PinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass;
|
|
PinType.PinSubCategoryObject = const_cast<UObject*>(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<typename T>
|
|
bool IsScriptStruct(const TSharedPtr<IPropertyHandle>& PropertyHandle)
|
|
{
|
|
if (!PropertyHandle)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FStructProperty* StructProperty = CastField<FStructProperty>(PropertyHandle->GetProperty());
|
|
return StructProperty && StructProperty->Struct->IsA(TBaseStructure<T>::Get()->GetClass());
|
|
}
|
|
|
|
/** @return true if the property is one of the known missing types. */
|
|
bool HasMissingType(const TSharedPtr<IPropertyHandle>& PropertyHandle)
|
|
{
|
|
if (!PropertyHandle)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Handles Struct
|
|
if (FStructProperty* StructProperty = CastField<FStructProperty>(PropertyHandle->GetProperty()))
|
|
{
|
|
return StructProperty->Struct == FPropertyBagMissingStruct::StaticStruct();
|
|
}
|
|
// Handles Object, SoftObject, Class, SoftClass.
|
|
if (FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(PropertyHandle->GetProperty()))
|
|
{
|
|
return ObjectProperty->PropertyClass == UPropertyBagMissingObject::StaticClass();
|
|
}
|
|
// Handles Enum
|
|
if (FEnumProperty* EnumProperty = CastField<FEnumProperty>(PropertyHandle->GetProperty()))
|
|
{
|
|
return EnumProperty->GetEnum() == StaticEnum<EPropertyBagMissingEnum>();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** @return property bag struct common to all edited properties. */
|
|
const UPropertyBag* GetCommonBagStruct(TSharedPtr<IPropertyHandle> StructProperty)
|
|
{
|
|
const UPropertyBag* CommonBagStruct = nullptr;
|
|
|
|
if (ensure(IsScriptStruct<FInstancedPropertyBag>(StructProperty)))
|
|
{
|
|
StructProperty->EnumerateConstRawData([&CommonBagStruct](const void* RawData, const int32 /*DataIndex*/, const int32 /*NumDatas*/)
|
|
{
|
|
if (RawData)
|
|
{
|
|
const FInstancedPropertyBag* Bag = static_cast<const FInstancedPropertyBag*>(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<FPropertyBagPropertyDesc> GetCommonPropertyDescs(const TSharedPtr<IPropertyHandle>& StructProperty)
|
|
{
|
|
TArray<FPropertyBagPropertyDesc> 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<IPropertyHandle>& StructProperty, const TConstArrayView<FPropertyBagPropertyDesc> PropertyDescs)
|
|
{
|
|
if (ensure(IsScriptStruct<FInstancedPropertyBag>(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<FInstancedPropertyBag*>(RawData))
|
|
{
|
|
Bag->MigrateToNewBagStruct(NewBagStruct);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
|
|
FName GetPropertyNameSafe(const TSharedPtr<IPropertyHandle>& 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<IPropertyHandle>& StructProperty)
|
|
{
|
|
if (NewName == OldName)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!StructProperty || !StructProperty->IsValidHandle())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bFound = false;
|
|
|
|
if (ensure(IsScriptStruct<FInstancedPropertyBag>(StructProperty)))
|
|
{
|
|
StructProperty->EnumerateConstRawData([&bFound, NewName](const void* RawData, const int32 /*DataIndex*/, const int32 /*NumDatas*/)
|
|
{
|
|
if (const FInstancedPropertyBag* Bag = static_cast<const FInstancedPropertyBag*>(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<typename TFunc>
|
|
void ApplyChangesToPropertyDescs(const FText& SessionName, const TSharedPtr<IPropertyHandle>& StructProperty, const TSharedPtr<IPropertyUtilities>& PropUtils, TFunc&& Function)
|
|
{
|
|
if (!StructProperty || !PropUtils)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FScopedTransaction Transaction(SessionName);
|
|
TArray<FPropertyBagPropertyDesc> PropertyDescs = GetCommonPropertyDescs(StructProperty);
|
|
StructProperty->NotifyPreChange();
|
|
|
|
Function(PropertyDescs);
|
|
|
|
SetPropertyDescs(StructProperty, PropertyDescs);
|
|
|
|
StructProperty->NotifyPostChange(EPropertyChangeType::ValueSet);
|
|
StructProperty->NotifyFinishedChangingProperties();
|
|
if (PropUtils)
|
|
{
|
|
PropUtils->ForceRefresh();
|
|
}
|
|
}
|
|
|
|
template <typename TFunc>
|
|
void ApplyChangesToSinglePropertyDesc(const FText& SessionName, const TSharedPtr<IPropertyHandle> PropertyHandle, const TSharedPtr<IPropertyHandle>& StructProperty, const TSharedPtr<IPropertyUtilities>& PropUtils, TFunc Function)
|
|
{
|
|
ApplyChangesToPropertyDescs(SessionName, StructProperty, PropUtils, [Function = std::move(Function), Property = PropertyHandle->GetProperty()](TArray<FPropertyBagPropertyDesc>& PropertyDescs)
|
|
{
|
|
if (FPropertyBagPropertyDesc* Desc = PropertyDescs.FindByPredicate([Property](const FPropertyBagPropertyDesc& OutDesc) { return OutDesc.CachedProperty == Property; }))
|
|
{
|
|
Function(*Desc);
|
|
}
|
|
});
|
|
}
|
|
|
|
template <typename TFunc>
|
|
void ApplyChangesToSinglePropertyDesc(const FText& SessionName, const FPropertyBagPropertyDesc& PropertyDesc, const TSharedPtr<IPropertyHandle>& StructProperty, const TSharedPtr<IPropertyUtilities>& PropUtils, TFunc Function)
|
|
{
|
|
ApplyChangesToPropertyDescs(SessionName, StructProperty, PropUtils, [Function = std::move(Function), &PropertyDesc](TArray<FPropertyBagPropertyDesc>& 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<IPropertyHandle> 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<UObject*> OutObjects;
|
|
InStructProperty->GetOuterObjects(OutObjects);
|
|
|
|
// Check for external function references, taken from GetOptions
|
|
if (FunctionName.Contains(TEXT(".")))
|
|
{
|
|
OutFunc = FindObject<UFunction>(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<typename T> struct TypeName { static const TCHAR* Get() = delete; };
|
|
|
|
#define DEFINE_TYPENAME(InType) template<> struct TypeName<InType> { 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<typename T>
|
|
static FFuncParam Make(T& Value)
|
|
{
|
|
FFuncParam Result;
|
|
Result.Value = &Value;
|
|
Result.CppType = TypeName<T>::Get();
|
|
return Result;
|
|
}
|
|
|
|
template<typename T>
|
|
static FFuncParam Make(const T& Value)
|
|
{
|
|
FFuncParam Result;
|
|
Result.Value = &const_cast<T&>(Value);
|
|
Result.CppType = TypeName<T>::Get();
|
|
Result.bIsConst = true;
|
|
return Result;
|
|
}
|
|
};
|
|
|
|
// Validate that the function pass as parameter has signature ReturnType(ArgsTypes...)
|
|
template <typename ReturnType, typename... ArgsTypes>
|
|
bool ValidateFunctionSignature(UFunction* InFunc)
|
|
{
|
|
if (!InFunc)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
constexpr int32 NumParms = std::is_same_v <ReturnType, void> ? sizeof...(ArgsTypes) : (sizeof...(ArgsTypes) + 1);
|
|
|
|
if (NumParms != InFunc->NumParms)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TCHAR* ArgsCppTypes[NumParms] = { TypeName<ArgsTypes>::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<ReturnType, void>)
|
|
{
|
|
ArgsCppTypes[NumParms - 1] = TypeName<ReturnType>::Get();
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, check that the function doesn't have a return param
|
|
if (InFunc->GetReturnProperty() != nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int32 ArgCppTypesIndex = 0;
|
|
for (TFieldIterator<FProperty> 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<ReturnType, void>)
|
|
{
|
|
if (ArgCppTypesIndex == NumParms - 1 && !It->HasAnyPropertyFlags(EPropertyFlags::CPF_ReturnParm))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ArgCppTypesIndex++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename ReturnType, typename... ArgsTypes>
|
|
TValueOrError<ReturnType, void> CallFunc(UObject* InTargetObject, UFunction* InFunc, ArgsTypes&& ...InArgs)
|
|
{
|
|
if (!InTargetObject || !InFunc)
|
|
{
|
|
return MakeError();
|
|
}
|
|
|
|
constexpr int32 NumParms = std::is_same_v <ReturnType, void> ? sizeof...(ArgsTypes) : (sizeof...(ArgsTypes) + 1);
|
|
|
|
if (NumParms != InFunc->NumParms)
|
|
{
|
|
return MakeError();
|
|
}
|
|
|
|
FFuncParam InParams[NumParms] = { FFuncParam::Make(std::forward<ArgsTypes>(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<ReturnType, void> != (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<ReturnType, void>)
|
|
{
|
|
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<FProperty> 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<FProperty> 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<ReturnType, void>)
|
|
{
|
|
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<IPropertyHandle>& BagStructProperty, TArray<TSharedPtr<UEdGraphSchema_K2::FPinTypeTreeInfo>>& 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<bool, FEdGraphPinType, bool>(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<bool, void> bIsValid = CallFunc<bool>(IsPinTypeAcceptedTarget, IsPinTypeAcceptedFunc, InPinType, bInIsChild);
|
|
return bIsValid.HasValue() && bIsValid.GetValue();
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
};
|
|
|
|
check(GetDefault<UEdGraphSchema_K2>());
|
|
TArray<TSharedPtr<UEdGraphSchema_K2::FPinTypeTreeInfo>> TempTypeTree;
|
|
GetDefault<UPropertyBagSchema>()->GetVariableTypeTree(TempTypeTree, TypeTreeFilter);
|
|
|
|
// Filter
|
|
for (TSharedPtr<UEdGraphSchema_K2::FPinTypeTreeInfo>& PinType : TempTypeTree)
|
|
{
|
|
if (!PinType.IsValid() || !IsPinTypeAccepted(PinType->GetPinType(/*bForceLoadSubCategoryObject*/false), /*bInIsChild=*/ false))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (int32 ChildIndex = 0; ChildIndex < PinType->Children.Num();)
|
|
{
|
|
TSharedPtr<UEdGraphSchema_K2::FPinTypeTreeInfo> 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<IPropertyHandle>& InStructProperty, const TSharedPtr<IPropertyHandle>& 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<bool, FGuid, FName>(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<bool, void> bCanRemove = UE::StructUtils::Private::CallFunc<bool>(CanRemovePropertyTarget, CanRemovePropertyFunc, PropertyDesc->ID, PropertyDesc->Name);
|
|
|
|
if (bCanRemove.HasError() || !bCanRemove.GetValue())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void DeleteProperty(TSharedPtr<IPropertyHandle> InStructProperty, TSharedPtr<IPropertyHandle> ChildPropertyHandle, const TSharedPtr<IPropertyUtilities>& 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<FPropertyBagPropertyDesc>& PropertyDescs)
|
|
{
|
|
const FProperty* Property = ChildPropertyHandle ? ChildPropertyHandle->GetProperty() : nullptr;
|
|
PropertyDescs.RemoveAll([Property](const FPropertyBagPropertyDesc& Desc) { return Desc.CachedProperty == Property; });
|
|
});
|
|
}
|
|
|
|
FEdGraphPinType GetPinInfo(const TSharedPtr<IPropertyHandle>& ChildPropertyHandle, const TSharedPtr<IPropertyHandle>& 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<FPropertyBagPropertyDesc> 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<IPropertyHandle> ChildPropertyHandle, const TSharedPtr<IPropertyHandle>& InBagStructProperty, const TSharedPtr<IPropertyUtilities>& 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<FPropertyBagPropertyDesc>& 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<SWidget> CreateTypeSelectionWidget(TSharedPtr<IPropertyHandle> ChildPropertyHandle, const TSharedPtr<IPropertyHandle>& InBagStructProperty, const TSharedPtr<IPropertyUtilities>& InPropUtils, SPinTypeSelector::ESelectorType SelectorType, const bool bAllowContainers)
|
|
{
|
|
return SNew(SBox)
|
|
.HAlign(HAlign_Right)
|
|
.Padding(FMargin(4, 0))
|
|
[
|
|
SNew(STypeSelector, FGetPinTypeTree::CreateLambda(
|
|
[BagStructProperty = InBagStructProperty](TArray<TSharedPtr<UEdGraphSchema_K2::FPinTypeTreeInfo>>& 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<UPropertyBagSchema>())
|
|
.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<FInstancedPropertyBag>(BagStructProperty));
|
|
ensure(PropUtils != nullptr);
|
|
}
|
|
|
|
/** For backwards compatibility. */
|
|
FPropertyBagInstanceDataDetails::FPropertyBagInstanceDataDetails(TSharedPtr<IPropertyHandle> InStructProperty, const TSharedPtr<IPropertyUtilities>& 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<FInstancedPropertyBag>(BagStructProperty));
|
|
ensure(PropUtils != nullptr);
|
|
}
|
|
|
|
void FPropertyBagInstanceDataDetails::OnGroupRowAdded(IDetailGroup& GroupRow, int32 Level, const FString& Category) const
|
|
{
|
|
using namespace UE::StructUtils;
|
|
|
|
FDetailWidgetRow& FolderRow = GroupRow.HeaderRow();
|
|
TWeakPtr<const FPropertyBagInstanceDataDetails> WeakSelf = SharedThis<const FPropertyBagInstanceDataDetails>(this);
|
|
FString FullCategoryName = GroupRow.GetGroupName().ToString();
|
|
|
|
/*** DRAG AND DROP HANDLER ***/
|
|
if (EnumHasAllFlags(ChildRowFeatures, EPropertyBagChildRowFeatures::DragAndDrop | EPropertyBagChildRowFeatures::Menu_Categories))
|
|
{
|
|
TSharedPtr<FPropertyBagDetailsDragDropHandlerTarget> DragDropHandler = MakeShared<FPropertyBagDetailsDragDropHandlerTarget>();
|
|
|
|
DragDropHandler->BindCanAcceptDragDrop(
|
|
FCanAcceptPropertyBagDetailsRowDropOp::CreateLambda(
|
|
[FullCategoryName](const TSharedPtr<FPropertyBagDetailsDragDropOp>& DropOp, EItemDropZone DropZone) -> TOptional<EItemDropZone>
|
|
{
|
|
if (!DropOp.IsValid() || DropZone != EItemDropZone::OntoItem)
|
|
{
|
|
DropOp->SetDecoration(EPropertyBagDropState::Invalid);
|
|
return TOptional<EItemDropZone>();
|
|
}
|
|
|
|
TOptional<FPropertyBagDetailsDragDropOp::FDecoration> 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<EItemDropZone>();
|
|
}
|
|
|
|
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<const FPropertyBagInstanceDataDetails> 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<SInlineEditableTextBlock> 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<FPropertyBagPropertyDesc>& 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<SBorder> 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<FPropertyBagPropertyDesc>& 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<FSlateColor>::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<SWidget> NameWidget;
|
|
TSharedPtr<SWidget> PropertyValueWidget;
|
|
FDetailWidgetRow DetailWidgetRow;
|
|
ChildRow.GetDefaultWidgets(NameWidget, PropertyValueWidget, DetailWidgetRow);
|
|
|
|
TSharedPtr<IPropertyHandle> ChildPropertyHandle = ChildRow.GetPropertyHandle();
|
|
check(ChildPropertyHandle);
|
|
|
|
TWeakPtr<FPropertyBagInstanceDataDetails> WeakSelf = SharedThis<FPropertyBagInstanceDataDetails>(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<bool> EditConditionValue = TAttribute<bool>::CreateLambda(
|
|
[WeakSelf, ChildPropertyHandle]() -> bool
|
|
{
|
|
if (const TSharedPtr<FPropertyBagInstanceDataDetails> Self = WeakSelf.Pin())
|
|
{
|
|
return Self->IsPropertyOverridden(ChildPropertyHandle) == EPropertyOverrideState::Yes;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
FOnBooleanValueChanged OnEditConditionChanged = FOnBooleanValueChanged::CreateLambda([WeakSelf, ChildPropertyHandle](bool bNewValue)
|
|
{
|
|
if (const TSharedPtr<FPropertyBagInstanceDataDetails> Self = WeakSelf.Pin())
|
|
{
|
|
Self->SetPropertyOverride(ChildPropertyHandle, bNewValue);
|
|
}
|
|
});
|
|
|
|
ChildRow.EditCondition(std::move(EditConditionValue), std::move(OnEditConditionChanged));
|
|
|
|
FIsResetToDefaultVisible IsResetVisible = FIsResetToDefaultVisible::CreateLambda([WeakSelf](TSharedPtr<IPropertyHandle> PropertyHandle)
|
|
{
|
|
if (const TSharedPtr<FPropertyBagInstanceDataDetails> Self = WeakSelf.Pin())
|
|
{
|
|
return !Self->IsDefaultValue(PropertyHandle);
|
|
}
|
|
return false;
|
|
});
|
|
FResetToDefaultHandler ResetHandler = FResetToDefaultHandler::CreateLambda([WeakSelf](TSharedPtr<IPropertyHandle> PropertyHandle)
|
|
{
|
|
if (const TSharedPtr<FPropertyBagInstanceDataDetails> Self = WeakSelf.Pin())
|
|
{
|
|
Self->ResetToDefault(PropertyHandle);
|
|
}
|
|
});
|
|
FResetToDefaultOverride ResetOverride = FResetToDefaultOverride::Create(IsResetVisible, ResetHandler);
|
|
|
|
ChildRow.OverrideResetToDefault(ResetOverride);
|
|
}
|
|
|
|
if (!bIsFixed)
|
|
{
|
|
/*** BUILD PROPERTY NAME WIDGET ***/
|
|
TSharedRef<SHorizontalBox> 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<SInlineEditableTextBlock> 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<FPropertyBagPropertyDesc>& 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<FPropertyBagPropertyDesc>& 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<FText>::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<FPropertyBagDetailsDragDropHandler> DragDropHandler = MakeShared<FPropertyBagDetailsDragDropHandler>(*PropertyDesc);
|
|
|
|
// Can accept drag and drop check for if this is a valid drop
|
|
DragDropHandler->BindCanAcceptDragDrop(
|
|
FCanAcceptPropertyBagDetailsRowDropOp::CreateLambda(
|
|
[PropertyDesc](const TSharedPtr<FPropertyBagDetailsDragDropOp>& DropOp, EItemDropZone DropZone) -> TOptional<EItemDropZone>
|
|
{
|
|
if (!PropertyDesc || !DropOp.IsValid())
|
|
{
|
|
DropOp->SetDecoration(EPropertyBagDropState::Invalid);
|
|
return TOptional<EItemDropZone>();
|
|
}
|
|
|
|
if (DropZone == EItemDropZone::OntoItem && PropertyDesc->ID != DropOp->PropertyDesc.ID)
|
|
{
|
|
DropOp->SetDecoration(EPropertyBagDropState::Invalid);
|
|
return TOptional<EItemDropZone>();
|
|
}
|
|
|
|
// 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<EItemDropZone>();
|
|
}
|
|
|
|
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<FPropertyBagInstanceDataDetails> 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<FInstancedPropertyBag*>(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<IPropertyHandle> 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<IPropertyHandle> 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<IPropertyHandle> 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<IPropertyHandle> 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<SWidget> FPropertyBagInstanceDataDetails::OnPropertyNameContent(TSharedPtr<IPropertyHandle> ChildPropertyHandle, TSharedPtr<SInlineEditableTextBlock> 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<FPropertyBagPropertyDesc>& 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<IPropertyTypeCustomization> FPropertyBagDetails::MakeInstance()
|
|
{
|
|
return MakeShared<FPropertyBagDetails>();
|
|
}
|
|
|
|
void FPropertyBagDetails::CustomizeHeader(TSharedRef<IPropertyHandle> 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<EPropertyBagPropertyType>())
|
|
{
|
|
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<EPropertyBagChildRowFeatures>())
|
|
{
|
|
const int32 EnumIndex = Enum->GetIndexByNameString(MetaDataProperty->GetMetaData(UE::StructUtils::Metadata::ChildRowFeaturesName));
|
|
if (EnumIndex != INDEX_NONE)
|
|
{
|
|
ChildRowFeatures = static_cast<EPropertyBagChildRowFeatures>(Enum->GetValueByIndex(EnumIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't show the header if ShowOnlyInnerProperties is set
|
|
if (MetaDataProperty->HasMetaData(UE::StructUtils::Metadata::ShowOnlyInnerPropertiesName))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
TSharedPtr<SWidget> 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<IPropertyHandle> 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<FPropertyBagInstanceDataDetails> InstanceDetails = MakeShared<FPropertyBagInstanceDataDetails>(Params);
|
|
StructBuilder.AddCustomBuilder(InstanceDetails);
|
|
}
|
|
|
|
TSharedPtr<SWidget> FPropertyBagDetails::MakeAddPropertyWidget(TSharedPtr<IPropertyHandle> InStructProperty, TSharedPtr<IPropertyUtilities> 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<FPropertyBagPropertyDesc>& PropertyDescs)
|
|
{
|
|
PropertyDescs.Emplace(NewName, DefaultType);
|
|
});
|
|
|
|
return FReply::Handled();
|
|
|
|
})
|
|
.Content()
|
|
[
|
|
SNew(SImage)
|
|
.Image(FAppStyle::GetBrush("Icons.PlusCircle"))
|
|
.ColorAndOpacity(IconColor)
|
|
]
|
|
];
|
|
|
|
}
|
|
|
|
////////////////////////////////////
|
|
|
|
bool UPropertyBagSchema::SupportsPinTypeContainer(TWeakPtr<const FEdGraphSchemaAction> SchemaAction,
|
|
const FEdGraphPinType& PinType, const EPinContainerType& ContainerType) const
|
|
{
|
|
return ContainerType == EPinContainerType::None || ContainerType == EPinContainerType::Array || ContainerType == EPinContainerType::Set;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|