// Copyright Epic Games, Inc. All Rights Reserved. #include "GroomMaterialDetails.h" #include "Widgets/Input/SCheckBox.h" #include "Misc/MessageDialog.h" #include "Modules/ModuleManager.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "Framework/Commands/UIAction.h" #include "Widgets/Text/STextBlock.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "IDetailChildrenBuilder.h" #include "IDetailPropertyRow.h" #include "IDetailGroup.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "Widgets/SToolTip.h" #include "IDocumentation.h" #include "GroomComponent.h" #include "SlateOptMacros.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SUniformGridPanel.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SSpinBox.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SVectorInputBox.h" #include "Editor/UnrealEdEngine.h" #include "EditorDirectories.h" #include "UnrealEdGlobals.h" #include "IDetailsView.h" #include "MaterialList.h" #include "PropertyCustomizationHelpers.h" #include "Interfaces/IMainFrameModule.h" #include "ScopedTransaction.h" #include "Editor.h" #include "HAL/PlatformApplicationMisc.h" #include "Rendering/SkeletalMeshModel.h" #include "IContentBrowserSingleton.h" #include "ContentBrowserModule.h" #include "EditorFramework/AssetImportData.h" #include "Logging/LogMacros.h" #include "MeshDescription.h" #include "MeshAttributes.h" #include "MeshAttributeArray.h" #include "Widgets/Input/STextComboBox.h" #include "Widgets/Input/SNumericEntryBox.h" #include "IDocumentation.h" #include "Widgets/Layout/SWrapBox.h" #include "Widgets/Input/SNumericDropDown.h" #include "ComponentReregisterContext.h" #include "Widgets/Layout/SExpandableArea.h" #include "SKismetInspector.h" #include "PropertyEditorDelegates.h" #include "PropertyCustomizationHelpers.h" #include "GroomCustomAssetEditorToolkit.h" #include "JsonObjectConverter.h" #define LOCTEXT_NAMESPACE "GroomMaterialDetails" ///////////////////////////////////////////////////////////////////////////////////////////////////////// // FGroomMaterialDetails FGroomMaterialDetails::FGroomMaterialDetails(IGroomCustomAssetEditorToolkit* InToolkit) : GroomDetailLayout(nullptr) { if (InToolkit) { GroomAsset = InToolkit->GetCustomAsset();// InGroomAsset; } bDeleteWarningConsumed = false; } FGroomMaterialDetails::~FGroomMaterialDetails() { } TSharedRef FGroomMaterialDetails::MakeInstance(IGroomCustomAssetEditorToolkit* InToolkit) { return MakeShareable(new FGroomMaterialDetails(InToolkit)); } void FGroomMaterialDetails::OnCopyMaterialList() { } void FGroomMaterialDetails::OnPasteMaterialList() { } bool FGroomMaterialDetails::OnCanCopyMaterialList() const { return false; } void FGroomMaterialDetails::AddMaterials(IDetailLayoutBuilder& DetailLayout) { if (!GroomAsset) { return; } // Create material list panel to let users control the materials array { FString MaterialCategoryName = FString(TEXT("Material Slots")); IDetailCategoryBuilder& MaterialCategory = DetailLayout.EditCategory(*MaterialCategoryName, FText::GetEmpty(), ECategoryPriority::Important); MaterialCategory.AddCustomRow(LOCTEXT("AddLODLevelCategories_MaterialArrayOperationAdd", "Materials Operation Add Material Slot")) .CopyAction(FUIAction(FExecuteAction::CreateSP(this, &FGroomMaterialDetails::OnCopyMaterialList), FCanExecuteAction::CreateSP(this, &FGroomMaterialDetails::OnCanCopyMaterialList))) .PasteAction(FUIAction(FExecuteAction::CreateSP(this, &FGroomMaterialDetails::OnPasteMaterialList))) .NameContent() .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("AddLODLevelCategories_MaterialArrayOperations", "Material Slots")) ] .ValueContent() .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(this, &FGroomMaterialDetails::GetMaterialArrayText) ] + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Padding(2.0f, 1.0f) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") .Text(LOCTEXT("AddLODLevelCategories_MaterialArrayOpAdd", "Add Material Slot")) .ToolTipText(LOCTEXT("AddLODLevelCategories_MaterialArrayOpAdd_Tooltip", "Add Material Slot at the end of the Material slot array. Those Material slots can be used to override a LODs section, (not the base LOD)")) .ContentPadding(4.0f) .ForegroundColor(FSlateColor::UseForeground()) .OnClicked(this, &FGroomMaterialDetails::AddMaterialSlot) .IsEnabled(true) .IsFocusable(false) [ SNew(SImage) .Image(FAppStyle::GetBrush("Icons.PlusCircle")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] ] ]; { FMaterialListDelegates MaterialListDelegates; MaterialListDelegates.OnGetMaterials.BindSP(this, &FGroomMaterialDetails::OnGetMaterialsForArray, 0); MaterialListDelegates.OnMaterialChanged.BindSP(this, &FGroomMaterialDetails::OnMaterialArrayChanged, 0); MaterialListDelegates.OnGenerateCustomNameWidgets.BindSP(this, &FGroomMaterialDetails::OnGenerateCustomNameWidgetsForMaterialArray); MaterialListDelegates.OnGenerateCustomMaterialWidgets.BindSP(this, &FGroomMaterialDetails::OnGenerateCustomMaterialWidgetsForMaterialArray, 0); MaterialListDelegates.OnMaterialListDirty.BindSP(this, &FGroomMaterialDetails::OnMaterialListDirty); MaterialListDelegates.OnCopyMaterialItem.BindSP(this, &FGroomMaterialDetails::OnCopyMaterialItem); MaterialListDelegates.OnCanCopyMaterialItem.BindSP(this, &FGroomMaterialDetails::OnCanCopyMaterialItem); MaterialListDelegates.OnPasteMaterialItem.BindSP(this, &FGroomMaterialDetails::OnPasteMaterialItem); //Pass an empty material list owner (owner can be use by the asset picker filter. In this case we do not need it) TArray MaterialListOwner; MaterialListOwner.Add(GroomAsset); MaterialCategory.AddCustomBuilder(MakeShareable(new FMaterialList(MaterialCategory.GetParentLayout(), MaterialListDelegates, MaterialListOwner, false, true))); } } } void FGroomMaterialDetails::OnCopyMaterialItem(int32 CurrentSlot) { TSharedRef RootJsonObject = MakeShareable(new FJsonObject()); if (GroomAsset->GetHairGroupsMaterials().IsValidIndex(CurrentSlot)) { const FHairGroupsMaterial& Material = GroomAsset->GetHairGroupsMaterials()[CurrentSlot]; FStaticMaterial TmpMaterial; TmpMaterial.MaterialInterface = Material.Material; TmpMaterial.MaterialSlotName = Material.SlotName; FJsonObjectConverter::UStructToJsonObject(FStaticMaterial::StaticStruct(), &TmpMaterial, RootJsonObject, 0, 0); } typedef TJsonWriter> FStringWriter; typedef TJsonWriterFactory> FStringWriterFactory; FString CopyStr; TSharedRef Writer = FStringWriterFactory::Create(&CopyStr); FJsonSerializer::Serialize(RootJsonObject, Writer); if (!CopyStr.IsEmpty()) { FPlatformApplicationMisc::ClipboardCopy(*CopyStr); } } bool FGroomMaterialDetails::OnCanCopyMaterialItem(int32 CurrentSlot) const { return GroomAsset->GetHairGroupsMaterials().IsValidIndex(CurrentSlot); } void FGroomMaterialDetails::OnPasteMaterialItem(int32 CurrentSlot) { FString PastedText; FPlatformApplicationMisc::ClipboardPaste(PastedText); TSharedPtr RootJsonObject; TSharedRef> Reader = TJsonReaderFactory<>::Create(PastedText); FJsonSerializer::Deserialize(Reader, RootJsonObject); if (RootJsonObject.IsValid()) { FProperty* Property = UGroomAsset::StaticClass()->FindPropertyByName(UGroomAsset::GetHairGroupsMaterialsMemberName()); check(Property != nullptr); GroomAsset->PreEditChange(Property); FScopedTransaction Transaction(LOCTEXT("GroomAssetChangedPasteMaterialItem", "GroomAsset editor: Pasted material item")); GroomAsset->Modify(); if (GroomAsset->GetHairGroupsMaterials().IsValidIndex(CurrentSlot)) { FStaticMaterial TmpMaterial; FJsonObjectConverter::JsonObjectToUStruct(RootJsonObject.ToSharedRef(), FStaticMaterial::StaticStruct(), &TmpMaterial, 0, 0); GroomAsset->GetHairGroupsMaterials()[CurrentSlot].Material = TmpMaterial.MaterialInterface; GroomAsset->GetHairGroupsMaterials()[CurrentSlot].SlotName= TmpMaterial.MaterialSlotName; } CallPostEditChange(Property); } } void FGroomMaterialDetails::CallPostEditChange(FProperty* PropertyChanged/*=nullptr*/) { if (PropertyChanged) { FPropertyChangedEvent PropertyUpdateStruct(PropertyChanged); GroomAsset->PostEditChangeProperty(PropertyUpdateStruct); } else { GroomAsset->Modify(); GroomAsset->PostEditChange(); } GroomDetailLayout->ForceRefreshDetails(); } void FGroomMaterialDetails::ApplyChanges() { GroomDetailLayout->ForceRefreshDetails(); } FText FGroomMaterialDetails::GetMaterialSlotNameText(int32 MaterialIndex) const { if (IsMaterialValid(MaterialIndex)) { return FText::FromName(GroomAsset->GetHairGroupsMaterials()[MaterialIndex].SlotName); } return LOCTEXT("HairMaterial_InvalidIndex", "Invalid Material Index"); } void FGroomMaterialDetails::CustomizeDetails( IDetailLayoutBuilder& DetailLayout ) { const TArray>& SelectedObjects = DetailLayout.GetSelectedObjects(); check(SelectedObjects.Num()<=1); // The OnGenerateCustomWidgets delegate will not be useful if we try to process more than one object. GroomAsset = SelectedObjects.Num() > 0 ? Cast(SelectedObjects[0].Get()) : nullptr; // Hide all properties { DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsInterpolationMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsRenderingMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsPhysicsMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsCardsMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsMeshesMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsMaterialsMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsLODMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairGroupsInfoMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetEnableGlobalInterpolationMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetHairInterpolationTypeMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetAutoLODBiasMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetDisableBelowMinLodStrippingMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetMinLODMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetLODModeMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetEnableSimulationCacheMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetRiggedSkeletalMeshMemberName(), UGroomAsset::StaticClass())); DetailLayout.HideProperty(DetailLayout.GetProperty(UGroomAsset::GetDataflowSettingsMemberName(), UGroomAsset::StaticClass())); } GroomDetailLayout = &DetailLayout; AddMaterials(DetailLayout); } void FGroomMaterialDetails::OnGetMaterialsForArray(class IMaterialListBuilder& OutMaterials, int32 LODIndex) { if (!GroomAsset) return; for (int32 MaterialIndex = 0; MaterialIndex < GroomAsset->GetHairGroupsMaterials().Num(); ++MaterialIndex) { OutMaterials.AddMaterial(MaterialIndex, GroomAsset->GetHairGroupsMaterials()[MaterialIndex].Material, true); } } void FGroomMaterialDetails::OnMaterialArrayChanged(UMaterialInterface* NewMaterial, UMaterialInterface* PrevMaterial, int32 SlotIndex, bool bReplaceAll, int32 LODIndex) { if (!GroomAsset) return; // Whether or not we made a transaction and need to end it bool bMadeTransaction = false; FProperty* MaterialProperty = FindFProperty(UGroomAsset::StaticClass(), "HairGroupsMaterials"); check(MaterialProperty); GroomAsset->PreEditChange(MaterialProperty); check(GroomAsset->GetHairGroupsMaterials().Num() > SlotIndex) if (NewMaterial != PrevMaterial) { GEditor->BeginTransaction(LOCTEXT("GroomEditorMaterialChanged", "Groom editor: material changed")); bMadeTransaction = true; GroomAsset->Modify(); GroomAsset->GetHairGroupsMaterials()[SlotIndex].Material = NewMaterial; // Add a default name to the material slot if this slot was manually add and there is no name yet if (NewMaterial != nullptr && GroomAsset->GetHairGroupsMaterials()[SlotIndex].SlotName == NAME_None) { if (GroomAsset->GetHairGroupsMaterials()[SlotIndex].SlotName == NAME_None) { GroomAsset->GetHairGroupsMaterials()[SlotIndex].SlotName = NewMaterial->GetFName(); } //Ensure the imported material slot name is unique if (GroomAsset->GetHairGroupsMaterials()[SlotIndex].SlotName == NAME_None) { UGroomAsset* LocalGroomAsset = GroomAsset; auto IsMaterialNameUnique = [&LocalGroomAsset, SlotIndex](const FName TestName) { for (int32 MaterialIndex = 0; MaterialIndex < LocalGroomAsset->GetHairGroupsMaterials().Num(); ++MaterialIndex) { if (MaterialIndex == SlotIndex) { continue; } if (LocalGroomAsset->GetHairGroupsMaterials()[MaterialIndex].SlotName == TestName) { return false; } } return true; }; int32 MatchNameCounter = 0; //Make sure the name is unique for imported material slot name bool bUniqueName = false; FString MaterialSlotName = NewMaterial->GetName(); while (!bUniqueName) { bUniqueName = true; if (!IsMaterialNameUnique(FName(*MaterialSlotName))) { bUniqueName = false; MatchNameCounter++; MaterialSlotName = NewMaterial->GetName() + TEXT("_") + FString::FromInt(MatchNameCounter); } } GroomAsset->GetHairGroupsMaterials()[SlotIndex].SlotName = FName(*MaterialSlotName); } } } FPropertyChangedEvent PropertyChangedEvent(MaterialProperty); GroomAsset->PostEditChangeProperty(PropertyChangedEvent); if (bMadeTransaction) { // End the transation if we created one GEditor->EndTransaction(); // Redraw viewports to reflect the material changes GUnrealEd->RedrawLevelEditingViewports(); } } FReply FGroomMaterialDetails::AddMaterialSlot() { if (!GroomAsset) { return FReply::Handled(); } FScopedTransaction Transaction(LOCTEXT("PersonaAddMaterialSlotTransaction", "Persona editor: Add material slot")); GroomAsset->Modify(); FHairGroupsMaterial NewMaterial; NewMaterial.SlotName = FName(TEXT("Material")); // Build a unique name FName SlotName = NewMaterial.SlotName; uint32 UniqueId = 0; bool bHasUniqueName = true; do { bHasUniqueName = true; for (const FHairGroupsMaterial& Group : GroomAsset->GetHairGroupsMaterials()) { if (Group.SlotName == SlotName) { bHasUniqueName = false; FString NewSlotName = NewMaterial.SlotName.ToString() + FString::FromInt(++UniqueId); SlotName = FName(*NewSlotName); break; } } } while (!bHasUniqueName); // Add new material NewMaterial.SlotName = SlotName; GroomAsset->GetHairGroupsMaterials().Add(NewMaterial); GroomAsset->PostEditChange(); return FReply::Handled(); } FText FGroomMaterialDetails::GetMaterialArrayText() const { FString MaterialArrayText = TEXT(" Material Slots"); int32 SlotNumber = 0; if (GroomAsset) { SlotNumber = GroomAsset->GetHairGroupsMaterials().Num(); } MaterialArrayText = FString::FromInt(SlotNumber) + MaterialArrayText; return FText::FromString(MaterialArrayText); } FText FGroomMaterialDetails::GetMaterialNameText(int32 MaterialIndex) const { if (IsMaterialValid(MaterialIndex)) { return FText::FromName(GroomAsset->GetHairGroupsMaterials()[MaterialIndex].SlotName); } return FText::FromName(NAME_None); } void FGroomMaterialDetails::OnMaterialNameCommitted(const FText& InValue, ETextCommit::Type CommitType, int32 MaterialIndex) { FName NewSlotName = FName(*(InValue.ToString())); if (IsMaterialValid(MaterialIndex) && NewSlotName != GroomAsset->GetHairGroupsMaterials()[MaterialIndex].SlotName) { FScopedTransaction ScopeTransaction(LOCTEXT("PersonaMaterialSlotNameChanged", "Persona editor: Material slot name change")); FProperty* ChangedProperty = FindFProperty(UGroomAsset::StaticClass(), "HairGroupsMaterials"); check(ChangedProperty); GroomAsset->PreEditChange(ChangedProperty); // Rename group which were using the old slot name FName PreviousSlotName = GroomAsset->GetHairGroupsMaterials()[MaterialIndex].SlotName; for (FHairGroupsRendering& Group : GroomAsset->GetHairGroupsRendering()) { if (PreviousSlotName == Group.MaterialSlotName) { Group.MaterialSlotName = NewSlotName; } } for (FHairGroupsCardsSourceDescription& Group : GroomAsset->GetHairGroupsCards()) { if (PreviousSlotName == Group.MaterialSlotName) { Group.MaterialSlotName = NewSlotName; } } for (FHairGroupsMeshesSourceDescription& Group : GroomAsset->GetHairGroupsMeshes()) { if (PreviousSlotName == Group.MaterialSlotName) { Group.MaterialSlotName = NewSlotName; } } GroomAsset->GetHairGroupsMaterials()[MaterialIndex].SlotName = NewSlotName; FPropertyChangedEvent PropertyUpdateStruct(ChangedProperty); GroomAsset->PostEditChangeProperty(PropertyUpdateStruct); } } TSharedRef FGroomMaterialDetails::OnGenerateCustomNameWidgetsForMaterialArray(UMaterialInterface* Material, int32 MaterialIndex) { return SNew(SVerticalBox); } TSharedRef FGroomMaterialDetails::OnGenerateCustomMaterialWidgetsForMaterialArray(UMaterialInterface* Material, int32 MaterialIndex, int32 LODIndex) { bool bMaterialIsUsed = GroomAsset && GroomAsset->IsMaterialUsed(MaterialIndex); return SNew(SMaterialSlotWidget, MaterialIndex, bMaterialIsUsed) .MaterialName(this, &FGroomMaterialDetails::GetMaterialNameText, MaterialIndex) .OnMaterialNameCommitted(this, &FGroomMaterialDetails::OnMaterialNameCommitted, MaterialIndex) .CanDeleteMaterialSlot(this, &FGroomMaterialDetails::CanDeleteMaterialSlot, MaterialIndex) .OnDeleteMaterialSlot(this, &FGroomMaterialDetails::OnDeleteMaterialSlot, MaterialIndex); } bool FGroomMaterialDetails::IsMaterialValid(int32 MaterialIndex) const { return GroomAsset && MaterialIndex >= 0 && MaterialIndex < GroomAsset->GetHairGroupsMaterials().Num(); } bool FGroomMaterialDetails::CanDeleteMaterialSlot(int32 MaterialIndex) const { if (!GroomAsset) { return false; } return !GroomAsset->IsMaterialUsed(MaterialIndex); } void FGroomMaterialDetails::OnDeleteMaterialSlot(int32 MaterialIndex) { if (!GroomAsset || !CanDeleteMaterialSlot(MaterialIndex)) { return; } if (!bDeleteWarningConsumed) { EAppReturnType::Type Answer = FMessageDialog::Open(EAppMsgType::OkCancel, LOCTEXT("FPersonaMeshDetails_DeleteMaterialSlot", "WARNING - Deleting a material slot can break the game play blueprint or the game play code. All indexes after the delete slot will change")); if (Answer == EAppReturnType::Cancel) { return; } bDeleteWarningConsumed = true; } GroomAsset->GetHairGroupsMaterials().RemoveAt(MaterialIndex); FScopedTransaction Transaction(LOCTEXT("PersonaOnDeleteMaterialSlotTransaction", "Persona editor: Delete material slot")); GroomAsset->Modify(); } bool FGroomMaterialDetails::OnMaterialListDirty() { bool ForceMaterialListRefresh = false; return ForceMaterialListRefresh; } TSharedRef FGroomMaterialDetails::OnGenerateCustomNameWidgetsForSection(int32 LodIndex, int32 SectionIndex) { bool IsSectionChunked = false; TSharedRef SectionWidget = SNew(SVerticalBox); SectionWidget->AddSlot() .AutoHeight() .Padding(0, 2, 0, 0); return SectionWidget; } TSharedRef FGroomMaterialDetails::OnGenerateCustomSectionWidgetsForSection(int32 LODIndex, int32 SectionIndex) { TSharedRef SectionWidget = SNew(SVerticalBox); SectionWidget->AddSlot() .AutoHeight() .Padding(0, 2, 0, 0); return SectionWidget; } EVisibility FGroomMaterialDetails::ShowEnabledSectionDetail(int32 LodIndex, int32 SectionIndex) const { return EVisibility::All; } EVisibility FGroomMaterialDetails::ShowDisabledSectionDetail(int32 LodIndex, int32 SectionIndex) const { return EVisibility::All; } void FGroomMaterialDetails::OnMaterialSelectedChanged(ECheckBoxState NewState, int32 MaterialIndex) { } ECheckBoxState FGroomMaterialDetails::IsIsolateMaterialEnabled(int32 MaterialIndex) const { ECheckBoxState State = ECheckBoxState::Unchecked; return State; } void FGroomMaterialDetails::OnMaterialIsolatedChanged(ECheckBoxState NewState, int32 MaterialIndex) { } ECheckBoxState FGroomMaterialDetails::IsSectionSelected(int32 SectionIndex) const { ECheckBoxState State = ECheckBoxState::Unchecked; return State; } void FGroomMaterialDetails::OnSectionSelectedChanged(ECheckBoxState NewState, int32 SectionIndex) { } ECheckBoxState FGroomMaterialDetails::IsIsolateSectionEnabled(int32 SectionIndex) const { ECheckBoxState State = ECheckBoxState::Unchecked; return State; } void FGroomMaterialDetails::OnSectionIsolatedChanged(ECheckBoxState NewState, int32 SectionIndex) { } #undef LOCTEXT_NAMESPACE