// Copyright Epic Games, Inc. All Rights Reserved. #include "GroomComponentDetailsCustomization.h" #include "Layout/Margin.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SBoxPanel.h" #include "Framework/Commands/UICommandList.h" #include "Widgets/Layout/SWrapBox.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Layout/SUniformGridPanel.h" #include "Styling/AppStyle.h" #include "Widgets/Images/SImage.h" #include "Widgets/SOverlay.h" #include "EditorModeManager.h" #include "PropertyCustomizationHelpers.h" #include "IDetailChildrenBuilder.h" #include "PropertyHandle.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "IDetailPropertyRow.h" #include "DetailCategoryBuilder.h" #include "IDetailsView.h" #include "IAssetTools.h" #include "AssetToolsModule.h" #include "ScopedTransaction.h" #include "IPropertyUtilities.h" #include "Subsystems/AssetEditorSubsystem.h" #define LOCTEXT_NAMESPACE "GroomComponent" ////////////////////////////////////////////////////////////////////////// // FGroomComponentDetailsCustomization TSharedRef FGroomComponentDetailsCustomization::MakeInstance() { return MakeShareable(new FGroomComponentDetailsCustomization); } void FGroomComponentDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { const TArray< TWeakObjectPtr >& SelectedObjects = DetailLayout.GetSelectedObjects(); MyDetailLayout = nullptr; FNotifyHook* NotifyHook = DetailLayout.GetPropertyUtilities()->GetNotifyHook(); bool bEditingActor = false; UGroomComponent* GroomComponent = nullptr; for (int32 ObjectIndex = 0; ObjectIndex < SelectedObjects.Num(); ++ObjectIndex) { UObject* TestObject = SelectedObjects[ObjectIndex].Get(); if (AActor* CurrentActor = Cast(TestObject)) { if (UGroomComponent* CurrentComponent = CurrentActor->FindComponentByClass()) { bEditingActor = true; GroomComponent = CurrentComponent; break; } } else if (UGroomComponent* TestComponent = Cast(TestObject)) { GroomComponent = TestComponent; break; } } GroomComponentPtr = GroomComponent; if (GroomComponentPtr.IsValid()) { // Ensure the group desc is update to date GroomComponentPtr->UpdateHairGroupsDesc(); // Add a delegate to refresh details panel when the groom asset changed TSharedRef GroomAssetHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UGroomComponent, GroomAsset)); FSimpleDelegate OnGroomAssetChangedDelegate = FSimpleDelegate::CreateSP(this, &FGroomComponentDetailsCustomization::OnGroomAssetChanged, &DetailLayout); GroomAssetHandle->SetOnPropertyValueChanged(OnGroomAssetChangedDelegate); } IDetailCategoryBuilder& HairGroupCategory = DetailLayout.EditCategory("GroomGroupsDesc", FText::GetEmpty(), ECategoryPriority::TypeSpecific); CustomizeDescGroupProperties(DetailLayout, HairGroupCategory); } void FGroomComponentDetailsCustomization::CustomizeDescGroupProperties(IDetailLayoutBuilder& DetailLayout, IDetailCategoryBuilder& StrandsGroupFilesCategory) { TSharedRef GroupDescAssetsProperty = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UGroomComponent, GroomGroupsDesc), UGroomComponent::StaticClass()); if (GroupDescAssetsProperty->IsValidHandle()) { TSharedRef GroupDescPropertyBuilder = MakeShareable(new FDetailArrayBuilder(GroupDescAssetsProperty, false, false, false)); GroupDescPropertyBuilder->OnGenerateArrayElementWidget(FOnGenerateArrayElementWidget::CreateSP(this, &FGroomComponentDetailsCustomization::OnGenerateElementForHairGroup, &DetailLayout)); GroupDescPropertyBuilder->SetDisplayName(FText::FromString(TEXT("Hair Groups"))); StrandsGroupFilesCategory.AddCustomBuilder(GroupDescPropertyBuilder, false); } } template bool AssignIfDifferentHairComponent(T& Dest, const T& Src, bool bSetValue) { const bool bHasChanged = Dest != Src; if (bHasChanged && bSetValue) { Dest = Src; } return bHasChanged; } #define HAIR_RESET_COMPONENT(MemberName) { if (PropertyName == GET_MEMBER_NAME_CHECKED(FHairGroupDesc, MemberName)) { bHasChanged = AssignIfDifferentHairComponent(GroomComponentPtr->GroomGroupsDesc[GroupIndex].MemberName, Default.MemberName, bSetValue); } } bool FGroomComponentDetailsCustomization::CommonResetToDefault(TSharedPtr ChildHandle, int32 GroupIndex, bool bSetValue) { bool bHasChanged = false; if (ChildHandle == nullptr || GroomComponentPtr == nullptr || GroupIndex < 0 || GroupIndex >= GroomComponentPtr->GroomGroupsDesc.Num()) { return bHasChanged; } FName PropertyName = ChildHandle->GetProperty()->GetFName(); FHairGroupDesc Default; HAIR_RESET_COMPONENT(HairWidth); HAIR_RESET_COMPONENT(HairRootScale); HAIR_RESET_COMPONENT(HairTipScale); HAIR_RESET_COMPONENT(HairLengthScale); HAIR_RESET_COMPONENT(HairShadowDensity); HAIR_RESET_COMPONENT(HairRaytracingRadiusScale); HAIR_RESET_COMPONENT(bUseHairRaytracingGeometry); HAIR_RESET_COMPONENT(bUseStableRasterization); HAIR_RESET_COMPONENT(bScatterSceneLighting); HAIR_RESET_COMPONENT(LODBias); if (bSetValue && bHasChanged) { FScopedTransaction ScopedTransaction(NSLOCTEXT("UnrealEd", "PropertyWindowResetToDefault", "Reset to Default")); GroomComponentPtr->UpdateHairGroupsDescAndInvalidateRenderState(); } return bHasChanged; } bool FGroomComponentDetailsCustomization::ShouldResetToDefault(TSharedPtr ChildHandle, int32 GroupIndex) { return CommonResetToDefault(ChildHandle, GroupIndex, false); } void FGroomComponentDetailsCustomization::ResetToDefault(TSharedPtr ChildHandle, int32 GroupIndex) { CommonResetToDefault(ChildHandle, GroupIndex, true); } void FGroomComponentDetailsCustomization::AddPropertyWithCustomReset(TSharedPtr& PropertyHandle, IDetailChildrenBuilder& Builder, int32 GroupIndex) { FIsResetToDefaultVisible IsResetVisible = FIsResetToDefaultVisible::CreateSP(this, &FGroomComponentDetailsCustomization::ShouldResetToDefault, GroupIndex); FResetToDefaultHandler ResetHandler = FResetToDefaultHandler::CreateSP(this, &FGroomComponentDetailsCustomization::ResetToDefault, GroupIndex); FResetToDefaultOverride ResetOverride = FResetToDefaultOverride::Create(IsResetVisible, ResetHandler); Builder.AddProperty(PropertyHandle.ToSharedRef()).OverrideResetToDefault(ResetOverride); } static TSharedRef MakeHairInfoGrid(const FSlateFontInfo& DetailFontInfo, const FHairGroupInfo& Infos) { TSharedRef Grid = SNew(SUniformGridPanel).SlotPadding(2.0f); // Header Grid->AddSlot(0, 0) // x, y .HAlign(HAlign_Right) [ SNew(STextBlock) .Font(DetailFontInfo) .Text(LOCTEXT("HairInfo_Curves", "Curves")) ]; Grid->AddSlot(1, 0) // x, y .HAlign(HAlign_Right) [ SNew(STextBlock) .Font(DetailFontInfo) .Text(LOCTEXT("HairInfo_Guides", "Guides")) ]; Grid->AddSlot(2, 0) // x, y .HAlign(HAlign_Right) [ SNew(STextBlock) .Font(DetailFontInfo) .Text(LOCTEXT("HairInfo_Length", "Max. Length")) ]; // Value Grid->AddSlot(0, 1) // x, y .HAlign(HAlign_Right) [ SNew(STextBlock) .Font(DetailFontInfo) .Text(FText::AsNumber(Infos.NumCurves)) ]; Grid->AddSlot(1, 1) // x, y .HAlign(HAlign_Right) [ SNew(STextBlock) .Font(DetailFontInfo) .Text(FText::AsNumber(Infos.NumGuides)) ]; Grid->AddSlot(2, 1) // x, y .HAlign(HAlign_Right) [ SNew(STextBlock) .Font(DetailFontInfo) .Text(FText::AsNumber(Infos.MaxCurveLength)) ]; return Grid; } // Hair group custom display TSharedRef GetGroupNameWidget(const UGroomAsset* GroomAsset, int32 GroupIndex, const FLinearColor& GroupColor); void FGroomComponentDetailsCustomization::OnGenerateElementForHairGroup(TSharedRef StructProperty, int32 GroupIndex, IDetailChildrenBuilder& ChildrenBuilder, IDetailLayoutBuilder* DetailLayout) { const FSlateFontInfo DetailFontInfo = IDetailLayoutBuilder::GetDetailFont(); static const FSlateBrush* GenericBrush = FCoreStyle::Get().GetBrush("GenericWhiteBox"); float OtherMargin = 2.0f; float RightMargin = 10.0f; const FLinearColor GroupColorBlock = GetHairGroupDebugColor(GroupIndex) * 0.75f; ChildrenBuilder.AddCustomRow(LOCTEXT("HairInfo_Separator", "Separator")) .WholeRowContent() .VAlign(VAlign_Fill) .HAlign(HAlign_Fill) [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SImage) .Image(GenericBrush) .ColorAndOpacity(GroupColorBlock) ] + SOverlay::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Center) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(OtherMargin, OtherMargin, RightMargin, OtherMargin) [ GetGroupNameWidget(GroomComponentPtr.IsValid() ? GroomComponentPtr->GroomAsset : nullptr, GroupIndex, FLinearColor::White) ] ] ]; if (GroomComponentPtr != nullptr && GroupIndex>=0 && GroupIndex < GroomComponentPtr->GroomGroupsDesc.Num()) { FHairGroupInfo Infos; if (GroomComponentPtr->GroomAsset && GroupIndex < GroomComponentPtr->GroomAsset->GetHairGroupsInfo().Num()) { Infos = GroomComponentPtr->GroomAsset->GetHairGroupsInfo()[GroupIndex]; } ChildrenBuilder.AddCustomRow(LOCTEXT("HairInfo_Separator", "Separator")) .ValueContent() .HAlign(HAlign_Fill) [ MakeHairInfoGrid(DetailFontInfo, Infos) ]; } uint32 ChildrenCount = 0; StructProperty->GetNumChildren(ChildrenCount); for (uint32 ChildIt = 0; ChildIt < ChildrenCount; ++ChildIt) { TSharedPtr ChildHandle = StructProperty->GetChildHandle(ChildIt); AddPropertyWithCustomReset(ChildHandle, ChildrenBuilder, GroupIndex); } } void FGroomComponentDetailsCustomization::OnGroomAssetChanged(IDetailLayoutBuilder* LayoutBuilder) { LayoutBuilder->ForceRefreshDetails(); } ////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE