// 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& 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(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>& 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 Result; if (ExpectedBaseStructure && ScriptStruct && ScriptStruct->IsChildOf(ExpectedBaseStructure)) { Result = MakeShared(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(ParentValueAddress); if (ExpectedBaseStructure && InstancedStruct.GetScriptStruct() && InstancedStruct.GetScriptStruct()->IsChildOf(ExpectedBaseStructure)) { return InstancedStruct.GetMutableMemory(); } return nullptr; } protected: void EnumerateInstances(TFunctionRef InFunc) const { if (!StructProperty.IsValid()) { return; } TArray 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(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 StructProperty; }; //////////////////////////////////// FInstancedStructDataDetails::FInstancedStructDataDetails(TSharedPtr InStructProperty) { #if DO_CHECK FStructProperty* StructProp = CastFieldChecked(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> FInstancedStructDataDetails::GetInstanceTypes() const { TArray> Result; StructProperty->EnumerateConstRawData([&Result](const void* RawData, const int32 /*DataIndex*/, const int32 /*NumDatas*/) { TWeakObjectPtr& Type = Result.AddDefaulted_GetRef(); if (const FInstancedStruct* InstancedStruct = static_cast(RawData)) { Result.Add(InstancedStruct->GetScriptStruct()); } else { Result.Add(nullptr); } return true; }); return Result; } void FInstancedStructDataDetails::GetPropertyGroups( const TArray>& InProperties, IDetailChildrenBuilder& InChildBuilder, TMap, 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 CategoryToGroup; for (const TSharedPtr& 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 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> 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 NewStructProvider = MakeShared(StructProperty); bool bCustomizedProperty = false; const UStruct* BaseStruct = NewStructProvider->GetBaseStructure(); if (BaseStruct) { static const FName EditModuleName("PropertyEditor"); if (const FPropertyEditorModule* EditModule = FModuleManager::GetModulePtr(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> ChildProperties = StructProperty->AddChildStructure(NewStructProvider); // Properties may have Category metadata. If that's the case, they should be added under groups. TMap, IDetailGroup*> PropertyToGroup; GetPropertyGroups(ChildProperties, ChildBuilder, PropertyToGroup); for (TSharedPtr 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> InstanceTypes = GetInstanceTypes(); if (InstanceTypes != CachedInstanceTypes) { OnRegenerateChildren.ExecuteIfBound(); } } FName FInstancedStructDataDetails::GetName() const { static const FName Name("InstancedStructDataDetails"); return Name; } //////////////////////////////////// TSharedRef FInstancedStructDetails::MakeInstance() { return MakeShared(); } FInstancedStructDetails::~FInstancedStructDetails() { if (OnObjectsReinstancedHandle.IsValid()) { FCoreUObjectDelegates::OnObjectsReinstanced.Remove(OnObjectsReinstancedHandle); } } void FInstancedStructDetails::CustomizeHeader(TSharedRef 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 StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { TSharedRef DataDetails = MakeShared(StructPropertyHandle); StructBuilder.AddCustomBuilder(DataDetails); } #undef LOCTEXT_NAMESPACE