// Copyright Epic Games, Inc. All Rights Reserved. #include "MaterialParameterCollectionDetails.h" #include "DetailLayoutBuilder.h" #include "Editor.h" #include "EditorSupportDelegates.h" #include "IDetailGroup.h" #include "IPropertyUtilities.h" #include "Materials/MaterialParameterCollection.h" #include "ScopedTransaction.h" #define LOCTEXT_NAMESPACE "MaterialParameterCollectionDetails" TSharedRef FMaterialParameterCollectionDetails::MakeInstance() { return MakeShareable(new FMaterialParameterCollectionDetails); } FMaterialParameterCollectionDetails::~FMaterialParameterCollectionDetails() { UnbindDelegates(); } class FMaterialParameterCollectionDetails::FBaseOverridesSelectorConst { public: FBaseOverridesSelectorConst(const UMaterialParameterCollection* OverrideCollection) : Collection(const_cast(OverrideCollection)) { } template decltype(auto) Contains(const FCollectionParameterType& CollectionParameter, FArgs&&... Args) const { return SelectBaseOverridesMap(CollectionParameter).Contains(CollectionParameter.Id, Forward(Args)...); } template decltype(auto) Find(const FCollectionParameterType& CollectionParameter, FArgs&&... Args) const { return SelectBaseOverridesMap(CollectionParameter).Find(CollectionParameter.Id, Forward(Args)...); } protected: TMap& SelectBaseOverridesMap(const FCollectionScalarParameter&) const { return Collection->ScalarParameterBaseOverrides; } TMap& SelectBaseOverridesMap(const FCollectionVectorParameter&) const { return Collection->VectorParameterBaseOverrides; } private: UMaterialParameterCollection* Collection; }; class FMaterialParameterCollectionDetails::FBaseOverridesSelector : public FBaseOverridesSelectorConst { public: FBaseOverridesSelector(UMaterialParameterCollection* OverrideCollection) : FBaseOverridesSelectorConst(OverrideCollection) { } template decltype(auto) Add(const FCollectionParameterType& CollectionParameter, FArgs&&... Args) const { return SelectBaseOverridesMap(CollectionParameter).Add(CollectionParameter.Id, Forward(Args)...); } template decltype(auto) FindOrAdd(const FCollectionParameterType& CollectionParameter, FArgs&&... Args) const { return SelectBaseOverridesMap(CollectionParameter).FindOrAdd(CollectionParameter.Id, Forward(Args)...); } template decltype(auto) Remove(const FCollectionParameterType& CollectionParameter, FArgs&&... Args) const { return SelectBaseOverridesMap(CollectionParameter).Remove(CollectionParameter.Id, Forward(Args)...); } }; FMaterialParameterCollectionDetails::FBaseOverridesSelector FMaterialParameterCollectionDetails::GetBaseOverridesMap(UMaterialParameterCollection* OverrideCollection) { return FBaseOverridesSelector(OverrideCollection); } FMaterialParameterCollectionDetails::FBaseOverridesSelectorConst FMaterialParameterCollectionDetails::GetBaseOverridesMap(const UMaterialParameterCollection* OverrideCollection) { return FBaseOverridesSelectorConst(OverrideCollection); } template auto FMaterialParameterCollectionDetails::GetParameterValue(const UMaterialParameterCollection* OverrideCollection, const FCollectionParameterType& CollectionParameter, const UMaterialParameterCollection* BaseCollection) { for (; OverrideCollection != BaseCollection; OverrideCollection = OverrideCollection->Base) { if (auto OverrideValue = GetBaseOverridesMap(OverrideCollection).Find(CollectionParameter)) { return *OverrideValue; } } return CollectionParameter.DefaultValue; } FProperty* FMaterialParameterCollectionDetails::GetBaseOverridesMapProperty(const FCollectionScalarParameter&) { return FindFProperty(UMaterialParameterCollection::StaticClass(), GET_MEMBER_NAME_CHECKED(UMaterialParameterCollection, ScalarParameterBaseOverrides)); } FProperty* FMaterialParameterCollectionDetails::GetBaseOverridesMapProperty(const FCollectionVectorParameter&) { return FindFProperty(UMaterialParameterCollection::StaticClass(), GET_MEMBER_NAME_CHECKED(UMaterialParameterCollection, VectorParameterBaseOverrides)); } template void FMaterialParameterCollectionDetails::AddParameter(IDetailGroup& DetailGroup, TSharedPtr PropertyHandle, UMaterialParameterCollection* Collection, const FCollectionParameterType& CollectionParameter, const UMaterialParameterCollection* BaseCollection) { using FValueType = decltype(CollectionParameter.DefaultValue); // Set the property display name to the parameter's name. PropertyHandle->SetPropertyDisplayName(FText::FromName(CollectionParameter.ParameterName)); // Initialize the property value with the current parameter value from the collection instance. void* ValueData = nullptr; if (PropertyHandle->GetValueData(ValueData) == FPropertyAccess::Success && ValueData) { FValueType& Value = *static_cast(ValueData); Value = GetParameterValue(Collection, CollectionParameter, BaseCollection); } // Create a delegate to invoke when the property value changes. TDelegate OnPropertyValueChangedDelegate = TDelegate::CreateWeakLambda(Collection, [Collection = Collection, CollectionParameter, PropertyHandle](const FPropertyChangedEvent& PropertyChangedEvent) { // Determine if this is the final setting of the property value. if (PropertyChangedEvent.ChangeType != EPropertyChangeType::ValueSet) { return; } // Update the collection's parameter value with the new property value. void* ValueData = nullptr; if (PropertyHandle->GetValueData(ValueData) == FPropertyAccess::Success && ValueData) { FProperty* CollectionPropertyToChange = GetBaseOverridesMapProperty(CollectionParameter); Collection->PreEditChange(CollectionPropertyToChange); FValueType& Value = *static_cast(ValueData); FValueType& ParameterValue = GetBaseOverridesMap(Collection).FindOrAdd(CollectionParameter); ParameterValue = Value; FPropertyChangedEvent CollectionChangedEvent(CollectionPropertyToChange, EPropertyChangeType::ValueSet); Collection->PostEditChangeProperty(CollectionChangedEvent); } }); // Invoke the delegate when the property value or one of its child property values changes. PropertyHandle->SetOnPropertyValueChangedWithData(OnPropertyValueChangedDelegate); PropertyHandle->SetOnChildPropertyValueChangedWithData(OnPropertyValueChangedDelegate); // If the collection instance includes the parameter in its parameter map, then the parameter is overridden. auto IsOverridden = [Collection = Collection, CollectionParameter]() { return GetBaseOverridesMap(Collection).Contains(CollectionParameter); }; auto OnOverride = [Collection = Collection, BaseCollection, CollectionParameter, PropertyHandle](bool bOverride) { void* ValueData = nullptr; if (PropertyHandle->GetValueData(ValueData) == FPropertyAccess::Success && ValueData) { // Create a new transaction for undo/redo support - normally handled by IPropertyHandle FScopedTransaction EditConditionChangedTransaction(NSLOCTEXT("PropertyEditor", "UpdatedEditConditionFmt", "Edit Condition Changed")); FProperty* CollectionPropertyToChange = GetBaseOverridesMapProperty(CollectionParameter); Collection->PreEditChange(CollectionPropertyToChange); FValueType& Value = *static_cast(ValueData); // Remove the parameter from the collection instance. auto&& BaseOverridesMap = GetBaseOverridesMap(Collection); BaseOverridesMap.Remove(CollectionParameter); // Update the property value with the parameter value from the collection instance's parent. Value = GetParameterValue(Collection, CollectionParameter, BaseCollection); if (bOverride) { // Add the parameter to the collection instance, with the value specified by its parent. BaseOverridesMap.Add(CollectionParameter, Value); } FPropertyChangedEvent CollectionChangedEvent(CollectionPropertyToChange, (bOverride ? EPropertyChangeType::ArrayAdd : EPropertyChangeType::ArrayRemove)); Collection->PostEditChangeProperty(CollectionChangedEvent); // Update all viewports after we modify the collection - normally handled by IPropertyHandle FEditorSupportDelegates::RedrawAllViewports.Broadcast(); } }; // Set the edit condition to use the override lambdas. DetailGroup.AddPropertyRow(PropertyHandle.ToSharedRef()). EditCondition( TAttribute::Create(TAttribute::FGetter::CreateWeakLambda(Collection, IsOverridden)), FOnBooleanValueChanged::CreateWeakLambda(Collection, OnOverride)); } template void FMaterialParameterCollectionDetails::AddParameters(IDetailCategoryBuilder& DetailCategory, FName PropertyName, UMaterialParameterCollection* Collection, const TArray& CollectionParameters, UMaterialParameterCollection* BaseCollection) { IDetailGroup& DetailGroup = DetailCategory.AddGroup(PropertyName, FText::FromName(PropertyName), false, false); if (CollectionParameters.IsEmpty()) { return; } if (UScriptStruct* CollectionParameterStruct = FCollectionParameterType::StaticStruct()) { for (auto& CollectionParameter : CollectionParameters) { TSharedPtr StructOnScope = MakeShared(CollectionParameterStruct); if (IDetailPropertyRow* DetailPropertyRow = DetailCategory.AddExternalStructure(StructOnScope)) { DetailPropertyRow->Visibility(EVisibility::Collapsed); if (TSharedPtr ParentHandle = DetailPropertyRow->GetPropertyHandle()) { if (TSharedPtr PropertyHandle = ParentHandle->GetChildHandle("DefaultValue")) { AddParameter(DetailGroup, PropertyHandle, Collection, CollectionParameter, BaseCollection); } } } } } } void FMaterialParameterCollectionDetails::CustomizeCollectionDetails(UMaterialParameterCollection* Collection) { FName ScalarParametersName = "ScalarParameters"; FName VectorParametersName = "VectorParameters"; TSharedRef ScalarParametersHandle = DetailLayout->GetProperty(ScalarParametersName, UMaterialParameterCollection::StaticClass()); TSharedRef VectorParametersHandle = DetailLayout->GetProperty(VectorParametersName, UMaterialParameterCollection::StaticClass()); TSharedRef BaseHandle = DetailLayout->GetProperty("Base", UMaterialParameterCollection::StaticClass()); DetailLayout->HideProperty(ScalarParametersHandle); DetailLayout->HideProperty(VectorParametersHandle); DetailLayout->HideProperty(BaseHandle); IDetailCategoryBuilder& MaterialCategory = DetailLayout->EditCategory("Material"); MaterialCategory.AddProperty(ScalarParametersHandle); MaterialCategory.AddProperty(VectorParametersHandle); MaterialCategory.AddProperty(BaseHandle); // Create a delegate to invoke when the Base changes. TDelegate OnPropertyValueChangedDelegate = TDelegate::CreateWeakLambda(Collection, [Collection, DetailLayout = DetailLayout, BaseHandle, BaseCollectionWeak = TWeakObjectPtr(Collection->Base)](const FPropertyChangedEvent& PropertyChangedEvent) { // Determine if this is the final setting of the property value. if (PropertyChangedEvent.ChangeType != EPropertyChangeType::ValueSet) { return; } // Update all properties to account for the new Base. DetailLayout->ForceRefreshDetails(); }); BaseHandle->SetOnPropertyValueChangedWithData(OnPropertyValueChangedDelegate); BaseHandle->SetOnPropertyResetToDefault(FSimpleDelegate::CreateLambda([DetailLayout = DetailLayout]() { DetailLayout->ForceRefreshDetails(); })); for (UMaterialParameterCollection* BaseCollection = Collection->Base; BaseCollection; BaseCollection = BaseCollection->Base) { IDetailCategoryBuilder& BaseCategory = DetailLayout->EditCategory(BaseCollection->GetFName()); AddParameters(BaseCategory, ScalarParametersName, Collection, BaseCollection->ScalarParameters, BaseCollection); AddParameters(BaseCategory, VectorParametersName, Collection, BaseCollection->VectorParameters, BaseCollection); } } void FMaterialParameterCollectionDetails::CustomizeDetails(IDetailLayoutBuilder& InDetailLayout) { UnbindDelegates(); DetailLayout = &InDetailLayout; // Bind delegates for detecting undo/redo and property changes in other objects. FEditorDelegates::PostUndoRedo.AddRaw(DetailLayout, &IDetailLayoutBuilder::ForceRefreshDetails); FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FMaterialParameterCollectionDetails::OnObjectPropertyChanged); // Customize the details of each collection being customized. TArray> Collections = DetailLayout->GetObjectsOfTypeBeingCustomized(); for (TWeakObjectPtr& CollectionWeak : Collections) { if (UMaterialParameterCollection* Collection = CollectionWeak.Get()) { CustomizeCollectionDetails(Collection); } } } void FMaterialParameterCollectionDetails::OnObjectPropertyChanged(UObject* Object, FPropertyChangedEvent& PropertyChangedEvent) { if (!DetailLayout || !Object->IsA()) { return; } // Refresh the layout details if the modified object is the base of any of the collections being customized. TArray> Collections = DetailLayout->GetObjectsOfTypeBeingCustomized(); for (TWeakObjectPtr& CollectionWeak : Collections) { if (UMaterialParameterCollection* Collection = CollectionWeak.Get()) { for (UMaterialParameterCollection* BaseCollection = Collection->Base; BaseCollection; BaseCollection = BaseCollection->Base) { if (BaseCollection == Object) { DetailLayout->ForceRefreshDetails(); return; } } } } } void FMaterialParameterCollectionDetails::UnbindDelegates() { FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this); if (DetailLayout) { FEditorDelegates::PostUndoRedo.RemoveAll(DetailLayout); } } #undef LOCTEXT_NAMESPACE