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

446 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "InstancedStructDetails.h"
#include "DetailWidgetRow.h"
#include "DetailLayoutBuilder.h"
#include "Editor.h"
#include "Editor/EditorEngine.h"
#include "IDetailChildrenBuilder.h"
#include "IDetailGroup.h"
#include "IPropertyUtilities.h"
#include "UObject/Package.h"
#include "Widgets/Images/SImage.h"
#include "Modules/ModuleManager.h"
#include "StructUtils/UserDefinedStruct.h"
#include "StructUtils/InstancedStruct.h"
#include "IStructureDataProvider.h"
#include "Misc/ConfigCacheIni.h"
#include "StructUtilsDelegates.h"
#include "SInstancedStructPicker.h"
#define LOCTEXT_NAMESPACE "StructUtilsEditor"
////////////////////////////////////
class FInstancedStructProvider : public IStructureDataProvider
{
public:
FInstancedStructProvider() = default;
explicit FInstancedStructProvider(const TSharedPtr<IPropertyHandle>& InStructProperty)
: StructProperty(InStructProperty)
{
}
virtual ~FInstancedStructProvider() override {}
void Reset()
{
StructProperty = nullptr;
}
virtual bool IsValid() const override
{
bool bHasValidData = false;
EnumerateInstances([&bHasValidData](const UScriptStruct* ScriptStruct, uint8* Memory, UPackage* Package)
{
if (ScriptStruct && Memory)
{
bHasValidData = true;
return false; // Stop
}
return true; // Continue
});
return bHasValidData;
}
virtual const UStruct* GetBaseStructure() const override
{
// Taken from UClass::FindCommonBase
auto FindCommonBaseStruct = [](const UScriptStruct* StructA, const UScriptStruct* StructB)
{
const UScriptStruct* CommonBaseStruct = StructA;
while (CommonBaseStruct && StructB && !StructB->IsChildOf(CommonBaseStruct))
{
CommonBaseStruct = Cast<UScriptStruct>(CommonBaseStruct->GetSuperStruct());
}
return CommonBaseStruct;
};
const UScriptStruct* CommonStruct = nullptr;
EnumerateInstances([&CommonStruct, &FindCommonBaseStruct](const UScriptStruct* ScriptStruct, uint8* Memory, UPackage* Package)
{
if (ScriptStruct)
{
CommonStruct = FindCommonBaseStruct(ScriptStruct, CommonStruct);
}
return true; // Continue
});
return CommonStruct;
}
virtual void GetInstances(TArray<TSharedPtr<FStructOnScope>>& OutInstances, const UStruct* ExpectedBaseStructure) const override
{
// The returned instances need to be compatible with base structure.
// This function returns empty instances in case they are not compatible, with the idea that we have as many instances as we have outer objects.
EnumerateInstances([&OutInstances, ExpectedBaseStructure](const UScriptStruct* ScriptStruct, uint8* Memory, UPackage* Package)
{
TSharedPtr<FStructOnScope> Result;
if (ExpectedBaseStructure && ScriptStruct && ScriptStruct->IsChildOf(ExpectedBaseStructure))
{
Result = MakeShared<FStructOnScope>(ScriptStruct, Memory);
Result->SetPackage(Package);
}
OutInstances.Add(Result);
return true; // Continue
});
}
virtual bool IsPropertyIndirection() const override
{
return true;
}
virtual uint8* GetValueBaseAddress(uint8* ParentValueAddress, const UStruct* ExpectedBaseStructure) const override
{
if (!ParentValueAddress)
{
return nullptr;
}
FInstancedStruct& InstancedStruct = *reinterpret_cast<FInstancedStruct*>(ParentValueAddress);
if (ExpectedBaseStructure && InstancedStruct.GetScriptStruct() && InstancedStruct.GetScriptStruct()->IsChildOf(ExpectedBaseStructure))
{
return InstancedStruct.GetMutableMemory();
}
return nullptr;
}
protected:
void EnumerateInstances(TFunctionRef<bool(const UScriptStruct* ScriptStruct, uint8* Memory, UPackage* Package)> InFunc) const
{
if (!StructProperty.IsValid())
{
return;
}
TArray<UPackage*> Packages;
StructProperty->GetOuterPackages(Packages);
StructProperty->EnumerateRawData([&InFunc, &Packages](void* RawData, const int32 DataIndex, const int32 /*NumDatas*/)
{
const UScriptStruct* ScriptStruct = nullptr;
uint8* Memory = nullptr;
UPackage* Package = nullptr;
if (FInstancedStruct* InstancedStruct = static_cast<FInstancedStruct*>(RawData))
{
ScriptStruct = InstancedStruct->GetScriptStruct();
Memory = InstancedStruct->GetMutableMemory();
if (ensureMsgf(Packages.IsValidIndex(DataIndex), TEXT("Expecting packges and raw data to match.")))
{
Package = Packages[DataIndex];
}
}
return InFunc(ScriptStruct, Memory, Package);
});
}
TSharedPtr<IPropertyHandle> StructProperty;
};
////////////////////////////////////
FInstancedStructDataDetails::FInstancedStructDataDetails(TSharedPtr<IPropertyHandle> InStructProperty)
{
#if DO_CHECK
FStructProperty* StructProp = CastFieldChecked<FStructProperty>(InStructProperty->GetProperty());
check(StructProp);
check(StructProp->Struct == FInstancedStruct::StaticStruct());
#endif
StructProperty = InStructProperty;
}
FInstancedStructDataDetails::~FInstancedStructDataDetails()
{
UE::StructUtils::Delegates::OnUserDefinedStructReinstanced.Remove(UserDefinedStructReinstancedHandle);
}
void FInstancedStructDataDetails::OnUserDefinedStructReinstancedHandle(const UUserDefinedStruct& Struct)
{
OnStructLayoutChanges();
}
void FInstancedStructDataDetails::SetOnRebuildChildren(FSimpleDelegate InOnRegenerateChildren)
{
OnRegenerateChildren = InOnRegenerateChildren;
}
TArray<TWeakObjectPtr<const UStruct>> FInstancedStructDataDetails::GetInstanceTypes() const
{
TArray<TWeakObjectPtr<const UStruct>> Result;
StructProperty->EnumerateConstRawData([&Result](const void* RawData, const int32 /*DataIndex*/, const int32 /*NumDatas*/)
{
TWeakObjectPtr<const UStruct>& Type = Result.AddDefaulted_GetRef();
if (const FInstancedStruct* InstancedStruct = static_cast<const FInstancedStruct*>(RawData))
{
Result.Add(InstancedStruct->GetScriptStruct());
}
else
{
Result.Add(nullptr);
}
return true;
});
return Result;
}
void FInstancedStructDataDetails::GetPropertyGroups(
const TArray<TSharedPtr<IPropertyHandle>>& InProperties,
IDetailChildrenBuilder& InChildBuilder,
TMap<TSharedPtr<IPropertyHandle>, IDetailGroup*>& OutPropertyToGroup) const
{
static const FName CategoryName = FName(TEXT("Category"));
static const FName EnableCategoriesName = FName(TEXT("EnableCategories"));
// Temporarily store a mapping of category -> group while groups are being built
TMap<FString, IDetailGroup*> CategoryToGroup;
for (const TSharedPtr<IPropertyHandle>& PropertyHandle : InProperties)
{
// The property needs the "EnableCategories" metadata in order to be added under a group. Grouping is opt-in.
if (!PropertyHandle->HasMetaData(EnableCategoriesName))
{
continue;
}
const FString& PropertyCategory = PropertyHandle->GetMetaData(CategoryName).TrimStartAndEnd();
if (PropertyCategory.IsEmpty())
{
continue;
}
constexpr bool bCullEmpty = true;
TArray<FString> CategoriesToAdd;
PropertyCategory.ParseIntoArray(CategoriesToAdd, TEXT("|"), bCullEmpty);
if (CategoriesToAdd.IsEmpty())
{
continue;
}
// Tracks the category name as it is being built up (eg, Foo -> Foo|Bar -> Foo|Bar|Baz)
FString CompleteCategory;
// For this property, add all of the groups needed for its category (eg, Foo, Foo|Bar, and Foo|Bar|Baz)
IDetailGroup* CurrentGroup = nullptr;
for (int32 Index = 0; Index < CategoriesToAdd.Num(); ++Index)
{
FString& CategoryToAdd = CategoriesToAdd[Index];
CategoryToAdd.TrimStartAndEndInline();
// Cover the edge case where there's a category like "Foo|", which is invalid
CategoryToAdd.TrimCharInline(TEXT('|'), nullptr);
CompleteCategory = CompleteCategory.IsEmpty()
? CategoryToAdd
: FString::Join(TArray({CompleteCategory, CategoryToAdd}), TEXT("|"));
// Create the category's group if it has not yet been created
if (!CategoryToGroup.Contains(CompleteCategory))
{
if (CurrentGroup)
{
// Add the group to the previous group if this is a nested category (eg, if this is the Foo|Bar group, add to the Foo group)
CurrentGroup = &CurrentGroup->AddGroup(FName(CompleteCategory), FText::FromString(CategoryToAdd));
}
else
{
// Otherwise, add the group as a normal group via the builder
CurrentGroup = &InChildBuilder.AddGroup(FName(CompleteCategory), FText::FromString(CategoryToAdd));
}
OnGroupRowAdded(*CurrentGroup, Index, CategoryToAdd);
CategoryToGroup.Add(CompleteCategory, CurrentGroup);
}
else
{
CurrentGroup = CategoryToGroup[CompleteCategory];
}
}
check(CurrentGroup);
OutPropertyToGroup.Add(PropertyHandle, CurrentGroup);
}
}
void FInstancedStructDataDetails::OnStructLayoutChanges()
{
bCanHandleStructValuePostChange = false;
OnRegenerateChildren.ExecuteIfBound();
}
void FInstancedStructDataDetails::OnStructHandlePostChange()
{
if (bCanHandleStructValuePostChange)
{
TArray<TWeakObjectPtr<const UStruct>> InstanceTypes = GetInstanceTypes();
if (InstanceTypes != CachedInstanceTypes)
{
OnRegenerateChildren.ExecuteIfBound();
}
}
}
void FInstancedStructDataDetails::GenerateHeaderRowContent(FDetailWidgetRow& NodeRow)
{
StructProperty->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FInstancedStructDataDetails::OnStructHandlePostChange));
if (!UserDefinedStructReinstancedHandle.IsValid())
{
UserDefinedStructReinstancedHandle = UE::StructUtils::Delegates::OnUserDefinedStructReinstanced.AddSP(this, &FInstancedStructDataDetails::OnUserDefinedStructReinstancedHandle);
}
}
void FInstancedStructDataDetails::GenerateChildContent(IDetailChildrenBuilder& ChildBuilder)
{
// Add the rows for the struct
TSharedRef<FInstancedStructProvider> NewStructProvider = MakeShared<FInstancedStructProvider>(StructProperty);
bool bCustomizedProperty = false;
const UStruct* BaseStruct = NewStructProvider->GetBaseStructure();
if (BaseStruct)
{
static const FName EditModuleName("PropertyEditor");
if (const FPropertyEditorModule* EditModule = FModuleManager::GetModulePtr<FPropertyEditorModule>(EditModuleName))
{
if (EditModule->IsCustomizedStruct(BaseStruct, FCustomPropertyTypeLayoutMap()))
{
bCustomizedProperty = true;
}
}
}
if (bCustomizedProperty)
{
// Use the struct name instead of the fully-qualified property name
const FText Label = BaseStruct->GetDisplayNameText();
const FName PropertyName = StructProperty->GetProperty()->GetFName();
// If the struct has a property customization, then we'll route through AddChildStructure, as it supports
// IPropertyTypeCustomization. The other branch is mostly kept as-is for legacy support purposes.
ChildBuilder.AddChildStructure(
StructProperty.ToSharedRef(), NewStructProvider, PropertyName, Label
);
}
else
{
StructProperty->RemoveChildren();
TArray<TSharedPtr<IPropertyHandle>> ChildProperties = StructProperty->AddChildStructure(NewStructProvider);
// Properties may have Category metadata. If that's the case, they should be added under groups.
TMap<TSharedPtr<IPropertyHandle>, IDetailGroup*> PropertyToGroup;
GetPropertyGroups(ChildProperties, ChildBuilder, PropertyToGroup);
for (TSharedPtr<IPropertyHandle> ChildHandle : ChildProperties)
{
// If the property has a group, add it under the group. Otherwise, just add it normally via the builder.
IDetailGroup** PropertyGroup = PropertyToGroup.Find(ChildHandle);
if (PropertyGroup && *PropertyGroup)
{
IDetailPropertyRow& Row = (*PropertyGroup)->AddPropertyRow(ChildHandle.ToSharedRef());
OnChildRowAdded(Row);
}
else
{
IDetailPropertyRow& Row = ChildBuilder.AddProperty(ChildHandle.ToSharedRef());
OnChildRowAdded(Row);
}
}
}
bCanHandleStructValuePostChange = true;
CachedInstanceTypes = GetInstanceTypes();
}
void FInstancedStructDataDetails::Tick(float DeltaTime)
{
// If the instance types change (e.g. due to selecting new struct type), we'll need to update the layout.
TArray<TWeakObjectPtr<const UStruct>> InstanceTypes = GetInstanceTypes();
if (InstanceTypes != CachedInstanceTypes)
{
OnRegenerateChildren.ExecuteIfBound();
}
}
FName FInstancedStructDataDetails::GetName() const
{
static const FName Name("InstancedStructDataDetails");
return Name;
}
////////////////////////////////////
TSharedRef<IPropertyTypeCustomization> FInstancedStructDetails::MakeInstance()
{
return MakeShared<FInstancedStructDetails>();
}
FInstancedStructDetails::~FInstancedStructDetails()
{
if (OnObjectsReinstancedHandle.IsValid())
{
FCoreUObjectDelegates::OnObjectsReinstanced.Remove(OnObjectsReinstancedHandle);
}
}
void FInstancedStructDetails::CustomizeHeader(TSharedRef<class IPropertyHandle> StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
StructProperty = StructPropertyHandle;
PropUtils = StructCustomizationUtils.GetPropertyUtilities();
OnObjectsReinstancedHandle = FCoreUObjectDelegates::OnObjectsReinstanced.AddSP(this, &FInstancedStructDetails::OnObjectsReinstanced);
HeaderRow
.NameContent()
[
StructPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
.MinDesiredWidth(250.f)
.VAlign(VAlign_Center)
[
SAssignNew(StructPicker, SInstancedStructPicker, StructProperty, PropUtils)
]
.IsEnabled(StructProperty->IsEditable());
}
void FInstancedStructDetails::OnObjectsReinstanced(const FReplacementObjectMap& ObjectMap)
{
// Force update the details when BP is compiled, since we may cached hold references to the old object or class.
if (!ObjectMap.IsEmpty() && PropUtils.IsValid())
{
PropUtils->RequestRefresh();
}
}
void FInstancedStructDetails::CustomizeChildren(TSharedRef<class IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
TSharedRef<FInstancedStructDataDetails> DataDetails = MakeShared<FInstancedStructDataDetails>(StructPropertyHandle);
StructBuilder.AddCustomBuilder(DataDetails);
}
#undef LOCTEXT_NAMESPACE