// Copyright Epic Games, Inc. All Rights Reserved. #include "PostProcessSettingsCustomization.h" #include "Containers/Array.h" #include "Containers/Map.h" #include "Containers/UnrealString.h" #include "Delegates/Delegate.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "LevelEditor.h" #include "SLevelViewport.h" #include "LevelEditorViewport.h" #include "Engine/BlendableInterface.h" #include "Factories/Factory.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformCrt.h" #include "HAL/PlatformMath.h" #include "IDetailChildrenBuilder.h" #include "IDetailGroup.h" #include "IPropertyUtilities.h" #include "ISettingsEditorModule.h" #include "Internationalization/Internationalization.h" #include "Layout/Margin.h" #include "Materials/Material.h" #include "Materials/MaterialInstanceConstant.h" #include "Misc/ConfigCacheIni.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Modules/ModuleManager.h" #include "ObjectEditorUtils.h" #include "PropertyCustomizationHelpers.h" #include "PropertyEditorModule.h" #include "PropertyHandle.h" #include "SlotBase.h" #include "Engine/RendererSettings.h" #include "Framework/Notifications/NotificationManager.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Templates/Tuple.h" #include "Textures/SlateIcon.h" #include "Types/SlateStructs.h" #include "UObject/Class.h" #include "UObject/Field.h" #include "UObject/NameTypes.h" #include "UObject/Object.h" #include "UObject/ObjectMacros.h" #include "UObject/UObjectGlobals.h" #include "UObject/UObjectIterator.h" #include "UObject/UnrealType.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SWidgetSwitcher.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/SToolTip.h" #include "Widgets/Images/SImage.h" #include "SWarningOrErrorBox.h" #include "RenderViewportFeedback.h" class IDetailPropertyRow; class SWidget; class UPackage; #define LOCTEXT_NAMESPACE "PostProcessSettingsCustomization" const FName ShowPostProcessCategoriesName("ShowPostProcessCategories"); const FName ShowOnlyInnerPropertiesName("ShowOnlyInnerProperties"); struct FCategoryOrGroup { IDetailCategoryBuilder* Category; IDetailGroup* Group; FCategoryOrGroup(IDetailCategoryBuilder& NewCategory) : Category(&NewCategory) , Group(nullptr) {} FCategoryOrGroup(IDetailGroup& NewGroup) : Category(nullptr) , Group(&NewGroup) {} FCategoryOrGroup() : Category(nullptr) , Group(nullptr) {} IDetailPropertyRow& AddProperty(TSharedRef PropertyHandle) { if (Category) { return Category->AddProperty(PropertyHandle); } else { return Group->AddPropertyRow(PropertyHandle); } } IDetailGroup& AddGroup(FName GroupName, const FText& DisplayName) { if (Category) { return Category->AddGroup(GroupName, DisplayName); } else { return Group->AddGroup(GroupName, DisplayName); } } bool IsValid() const { return Group || Category; } }; struct FAlert { enum class EType { Warning, Error }; EType Type; FText Text; TAttribute Visibility; }; struct FPostProcessGroup { FString RawGroupName; FString DisplayName; FString ParentPath; FCategoryOrGroup RootCategory; FCategoryOrGroup Detail; TOptional Alert; TArray> SimplePropertyHandles; TArray> AdvancedPropertyHandles; bool IsValid() const { return !RawGroupName.IsEmpty() && !DisplayName.IsEmpty() && RootCategory.IsValid(); } FPostProcessGroup() : RootCategory() , Detail() {} }; class FPostProcessRayTracingDisabledWarning { static inline TWeakPtr NotificationPtr = nullptr; public: static void DisplayRayTracingDisabledWarning() { bool bSuppressNotification = false; GConfig->GetBool(TEXT("PostProcess"), TEXT("SuppressEnableRayTracingNotification"), bSuppressNotification, GEditorPerProjectIni); if (bSuppressNotification) { return; } static IConsoleVariable* RayTracingCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.RayTracing")); static const auto LumenUseHardwareRayTracingCVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.Lumen.HardwareRayTracing")); const bool bRayTracingEnabled = RayTracingCVar->GetInt() != 0; const bool bLumenRayTracingEnabled = LumenUseHardwareRayTracingCVar->GetValueOnAnyThread() != 0; if (bRayTracingEnabled && bLumenRayTracingEnabled) { return; } if (NotificationPtr.IsValid()) { return; } const FText WarningText =LOCTEXT("RayTracedTranslucencyWarning", "The following Project Settings must be enabled for Ray Traced Translucency:\n" "- Hardware Ray Tracing: Support HWRT\n" "- Lumen: Use HWRT When Available"); FNotificationInfo Warning(WarningText); Warning.bFireAndForget = false; Warning.bUseLargeFont = false; Warning.bUseThrobber = false; Warning.bUseSuccessFailIcons = false; Warning.WidthOverride = 400.0f; Warning.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("RawTracedTranslucencyWarning_Enable", "Enable"), LOCTEXT("RawTracedTranslucencyWarning_EnableToolTip", "Enable Support Hardware Ray Tracing and Use Hardware Ray Tracing When Available in your project settings"), FSimpleDelegate::CreateStatic(&EnableRayTracingSettings))); Warning.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("RawTracedTranslucencyWarning_NotNow", "Not Now"), LOCTEXT("RawTracedTranslucencyWarning_NotNowToolTip", "Don't enable Support Hardware Ray Tracing and Use Hardware Ray Tracing When Available"), FSimpleDelegate::CreateStatic(&CloseNotification))); Warning.CheckBoxState = TAttribute::Create(&GetSuppressNotificationCheckboxState); Warning.CheckBoxStateChanged = FOnCheckStateChanged::CreateStatic(&SetSuppressNotification); Warning.CheckBoxText = LOCTEXT("RayTracedTranslucencyWarning_SuppressCheckBox", "Don't show this again"); NotificationPtr = FSlateNotificationManager::Get().AddNotification(Warning); NotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } private: static ECheckBoxState GetSuppressNotificationCheckboxState() { bool bSuppressNotification = false; GConfig->GetBool(TEXT("PostProcess"), TEXT("SuppressEnableRayTracingNotification"), bSuppressNotification, GEditorPerProjectIni); return bSuppressNotification ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } static void SetSuppressNotification(ECheckBoxState NewState) { // If the user selects to not show this again, set that in the config so we know about it in between sessions const bool bSuppressNotification = (NewState == ECheckBoxState::Checked); GConfig->SetBool(TEXT("PostProcess"), TEXT("SuppressEnableRayTracingNotification"), bSuppressNotification, GEditorPerProjectIni); } static void EnableRayTracingSettings() { static IConsoleVariable* RayTracingCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.RayTracing")); RayTracingCVar->Set(true); static IConsoleVariable* LumenUseHardwareRayTracingCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.Lumen.HardwareRayTracing")); LumenUseHardwareRayTracingCVar->Set(true); URendererSettings* const RendererSettings = GetMutableDefault(); RendererSettings->bEnableRayTracing = true; RendererSettings->bUseHardwareRayTracingForLumen = true; auto UpdatePropertyValue = [RendererSettings](const FName& PropertyName) { FProperty* Property = RendererSettings->GetClass()->FindPropertyByName(PropertyName); FPropertyChangedEvent PropertyChangedEvent(Property, EPropertyChangeType::ValueSet, {RendererSettings}); RendererSettings->PostEditChangeProperty(PropertyChangedEvent); RendererSettings->UpdateSinglePropertyInConfigFile(Property, RendererSettings->GetDefaultConfigFilename()); }; UpdatePropertyValue(GET_MEMBER_NAME_CHECKED(URendererSettings, bEnableRayTracing)); UpdatePropertyValue(GET_MEMBER_NAME_CHECKED(URendererSettings, bUseHardwareRayTracingForLumen)); FModuleManager::GetModuleChecked("SettingsEditor").OnApplicationRestartRequired(); CloseNotification(); } static void CloseNotification() { if (NotificationPtr.IsValid()) { NotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Success); NotificationPtr.Pin()->ExpireAndFadeout(); } NotificationPtr.Reset(); } }; // See FDetailGroup::MakeNameWidget() static TSharedRef MakeNameWidget(FText LocalizedDisplayName, TOptional LocalizedToolTip) { TSharedRef TextBlock = SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LocalizedDisplayName); if (LocalizedToolTip.IsSet() && !LocalizedToolTip->IsEmpty()) { TextBlock->SetToolTipText(LocalizedToolTip.GetValue()); } return SNew(SButton) .ButtonStyle(FAppStyle::Get(), "NoBorder") .ContentPadding(FMargin(0, 2, 0, 2)) //.OnClicked(this, &FDetailGroup::OnNameClicked) .ForegroundColor(FSlateColor::UseForeground()) .Content() [ TextBlock ]; }; static bool GetRenderFeedbackForActiveViewport(FRenderViewportFeedback& OutValue) { static FLevelEditorModule* LevelEditorModule = nullptr; if (!LevelEditorModule && FModuleManager::Get().IsModuleLoaded("LevelEditor")) { LevelEditorModule = &FModuleManager::LoadModuleChecked("LevelEditor"); } if (!LevelEditorModule) { return false; } const TSharedPtr ActiveLevelViewport = LevelEditorModule->GetFirstActiveLevelViewport(); if (!ActiveLevelViewport) { return false; } FLevelEditorViewportClient& ViewportClient = ActiveLevelViewport->GetLevelViewportClient(); if (ViewportClient.RenderViewportFeedback) { OutValue = ViewportClient.RenderViewportFeedback->Data(); return true; } else { // Request data collection for next frame ViewportClient.RenderViewportFeedback = MakeShared(); return false; } } static void CustomizeGroup(FPostProcessGroup& Owner, TSharedRef& StructPropertyHandle); static void MakeDetail(FPostProcessGroup& Elem, TMap& NameToGroupMap, TSharedRef& StructPropertyHandle) { if (Elem.Detail.IsValid()) { return; } FCategoryOrGroup* ParentDetail; int _; bool bIsRoot = !Elem.ParentPath.FindChar('|', _); if (bIsRoot) { ParentDetail = &Elem.RootCategory; } else { ParentDetail = &NameToGroupMap.Find(Elem.ParentPath)->Detail; } Elem.Detail = ParentDetail->AddGroup(*Elem.RawGroupName, FText::FromString(Elem.DisplayName)); CustomizeGroup(Elem, StructPropertyHandle); } static FPostProcessGroup* FindOrCreateGroup(TMap& NameToGroupMap, FCategoryOrGroup* Root, FStringView GroupPath, TSharedRef& StructPropertyHandle) { FPostProcessGroup* PPGroup = NameToGroupMap.Find(FString(GroupPath)); if (!PPGroup) { int32 LastSeperator; bool bIsRoot = !GroupPath.FindLastChar('|', LastSeperator); if (bIsRoot) { return nullptr; } else { PPGroup = &NameToGroupMap.Add(FString(GroupPath)); } } // Is this a new group? if (!PPGroup->IsValid()) { PPGroup->RootCategory = *Root; PPGroup->RawGroupName = GroupPath; int32 LastSeparatorIndex; if (GroupPath.FindLastChar('|', LastSeparatorIndex)) { PPGroup->ParentPath = GroupPath.Left(LastSeparatorIndex); FPostProcessGroup* Parent = FindOrCreateGroup(NameToGroupMap, Root, PPGroup->ParentPath, StructPropertyHandle); // Parent is now certainly non-empty, so build its IDetailGroup that the child will go into if (Parent) { MakeDetail(*Parent, NameToGroupMap, StructPropertyHandle); } PPGroup->DisplayName = GroupPath.RightChop(LastSeparatorIndex+1); } else { // Path is root checkNoEntry(); } } return PPGroup; } static void CustomizeGroup(FPostProcessGroup& Group, TSharedRef& StructPropertyHandle) { if (Group.RawGroupName == TEXT("Lens|Bloom|Gaussian")) { Group.Alert = FAlert { .Type = FAlert::EType::Warning, .Text = LOCTEXT("PostProcessBloomGaussianWarning", "Primary viewport currently shows convolution bloom"), .Visibility = MakeAttributeLambda([=, CurVisibility = EVisibility::Collapsed]() mutable { // Show if Gaussian bloom is selected, but not currently used in the viewport. // Visibility is updated only if we can access all the required data. FRenderViewportFeedback RenderViewportFeedback; if (GetRenderFeedbackForActiveViewport(RenderViewportFeedback)) { FPostProcessSettings* PostProcessSettings; if (StructPropertyHandle->GetValueData((void*&)PostProcessSettings) == FPropertyAccess::Success) { bool bBloomFFTSelected = PostProcessSettings->bOverride_BloomMethod && PostProcessSettings->BloomMethod == EBloomMethod::BM_FFT; CurVisibility = bBloomFFTSelected && RenderViewportFeedback.BloomMethod == EBloomMethod::BM_SOG ? EVisibility::Visible : EVisibility::Collapsed; } } return CurVisibility; }) }; } else if (Group.RawGroupName == TEXT("Lens|Bloom|Convolution")) { Group.Alert = FAlert { .Type = FAlert::EType::Warning, .Text = LOCTEXT("PostProcessBloomConvolutionWarning", "Primary viewport currently shows Gaussian bloom"), .Visibility = MakeAttributeLambda([=, CurVisibility = EVisibility::Collapsed]() mutable { // Show if convolution bloom is selected, but not currently used in the viewport. // Visibility is updated only if we can access all the required data. FRenderViewportFeedback RenderViewportFeedback; if (GetRenderFeedbackForActiveViewport(RenderViewportFeedback)) { FPostProcessSettings* PostProcessSettings; if (StructPropertyHandle->GetValueData((void*&)PostProcessSettings) == FPropertyAccess::Success) { bool bBloomSOGSelected = PostProcessSettings->bOverride_BloomMethod && PostProcessSettings->BloomMethod == EBloomMethod::BM_SOG; CurVisibility = bBloomSOGSelected && RenderViewportFeedback.BloomMethod == EBloomMethod::BM_FFT ? EVisibility::Visible : EVisibility::Collapsed; } } return CurVisibility; }) }; } } // Return false to hide the property static bool CustomizeProperty(TSharedPtr& PropertyHandle, FProperty* Property, const FName& CategoryFName, TSharedRef& StructHandle, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { static const FName ExposureCategory("Lens|Exposure"); static const FName TranslucencyCategory("Rendering Features|Translucency"); if (CategoryFName == ExposureCategory) { static const auto VarDefaultAutoExposureExtendDefaultLuminanceRange = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange")); const bool bExtendedLuminanceRange = VarDefaultAutoExposureExtendDefaultLuminanceRange->GetValueOnGameThread() == 1; if (bExtendedLuminanceRange) { if (Property->GetName() == GET_MEMBER_NAME_CHECKED(FPostProcessSettings, AutoExposureMinBrightness)) { Property->SetMetaData(TEXT("DisplayName"), TEXT("Min EV100")); } else if (Property->GetName() == GET_MEMBER_NAME_CHECKED(FPostProcessSettings, AutoExposureMaxBrightness)) { Property->SetMetaData(TEXT("DisplayName"), TEXT("Max EV100")); } else if (Property->GetName() == GET_MEMBER_NAME_CHECKED(FPostProcessSettings, HistogramLogMin)) { Property->SetMetaData(TEXT("DisplayName"), TEXT("Histogram Min EV100")); } else if (Property->GetName() == GET_MEMBER_NAME_CHECKED(FPostProcessSettings, HistogramLogMax)) { Property->SetMetaData(TEXT("DisplayName"), TEXT("Histogram Max EV100")); } } } // Special handling for the ray tracing translucency section, which needs to show or hide specific properties based on the translucency type else if (CategoryFName == TranslucencyCategory) { TSharedPtr TranslucencyTypeHandle = StructHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FPostProcessSettings, TranslucencyType)); if (PropertyHandle->GetProperty()->GetFName() != TranslucencyTypeHandle->GetProperty()->GetName()) { // Based on the translucency type property value, hide any properties that don't match the current translucency type uint8 TranslucencyType = 0; TranslucencyTypeHandle->GetValue(TranslucencyType); const FString& TranslucencyTypeMetaData = PropertyHandle->GetMetaData(TEXT("TranslucencyType")); const FString& TranslucencyTypeName = StaticEnum()->GetNameStringByValue(TranslucencyType); if (!TranslucencyTypeMetaData.Equals(TranslucencyTypeName, ESearchCase::IgnoreCase)) { PropertyHandle->MarkHiddenByCustomization(); return false; } } else { // Add a property changed event on the translucency type property to refresh the details panel so that the necessary properties // are made visible or hidden, as well as notifying the user if the project settings don't have hardware ray tracing enabled, // which is required for ray traced translucency PropertyHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateLambda([StructHandle, &StructCustomizationUtils]() { TSharedPtr TranslucencyTypeHandle = StructHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FPostProcessSettings, TranslucencyType)); uint8 TranslucencyType = 0; TranslucencyTypeHandle->GetValue(TranslucencyType); if (static_cast(TranslucencyType) == ETranslucencyType::RayTraced) { FPostProcessRayTracingDisabledWarning::DisplayRayTracingDisabledWarning(); } StructCustomizationUtils.GetPropertyUtilities()->ForceRefresh(); })); } } return true; } void FPostProcessSettingsCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { uint32 NumChildren = 0; FPropertyAccess::Result Result = StructPropertyHandle->GetNumChildren(NumChildren); // Create new categories in the parent layout rather than adding all post process settings to one category IDetailLayoutBuilder& LayoutBuilder = StructBuilder.GetParentCategory().GetParentLayout(); TMap NameToCategoryBuilderMap; TMap NameToGroupMap; TArray CategoryAndGroups; bool bShowPostProcessCategories = StructPropertyHandle->HasMetaData(ShowPostProcessCategoriesName); if (Result == FPropertyAccess::Success && NumChildren > 0) { for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) { TSharedPtr ChildHandle = StructPropertyHandle->GetChildHandle(ChildIndex); if (ChildHandle.IsValid() && ChildHandle->GetProperty()) { FProperty* Property = ChildHandle->GetProperty(); FName CategoryFName = FObjectEditorUtils::GetCategoryFName(Property); FString RawCategoryName = CategoryFName.ToString(); CategoryAndGroups.Reset(); RawCategoryName.ParseIntoArray(CategoryAndGroups, TEXT("|"), 1); FString RootCategoryName = CategoryAndGroups.Num() > 0 ? CategoryAndGroups[0] : RawCategoryName; FCategoryOrGroup* Category = NameToCategoryBuilderMap.Find(RootCategoryName); if (!Category) { if (bShowPostProcessCategories) { IDetailCategoryBuilder& NewCategory = LayoutBuilder.EditCategory(*RootCategoryName, FText::GetEmpty(), ECategoryPriority::TypeSpecific); Category = &NameToCategoryBuilderMap.Emplace(RootCategoryName, NewCategory); } else { IDetailGroup& NewGroup = StructBuilder.AddGroup(*RootCategoryName, FText::FromString(RootCategoryName)); Category = &NameToCategoryBuilderMap.Emplace(RootCategoryName, NewGroup); } } bool bVisible = CustomizeProperty(ChildHandle, Property, CategoryFName, StructPropertyHandle, StructCustomizationUtils); if (!bVisible) { continue; } if (CategoryAndGroups.Num() > 1) { FPostProcessGroup* PPGroup = FindOrCreateGroup(NameToGroupMap, Category, RawCategoryName, StructPropertyHandle); check(PPGroup); bool bIsSimple = !ChildHandle->GetProperty()->HasAnyPropertyFlags(CPF_AdvancedDisplay); if (bIsSimple) { PPGroup->SimplePropertyHandles.Add(ChildHandle); } else { PPGroup->AdvancedPropertyHandles.Add(ChildHandle); } } else { Category->AddProperty(ChildHandle.ToSharedRef()); } } } for (auto& NameAndGroup : NameToGroupMap) { FPostProcessGroup& PPGroup = NameAndGroup.Value; if (PPGroup.SimplePropertyHandles.Num() > 0 || PPGroup.AdvancedPropertyHandles.Num() > 0) { MakeDetail(PPGroup, NameToGroupMap, StructPropertyHandle); IDetailGroup& SimpleGroup = *PPGroup.Detail.Group; if (PPGroup.Alert.IsSet()) { FDetailWidgetRow Header = SimpleGroup.HeaderRow() .NameContent() [ MakeNameWidget(FText::FromString(PPGroup.DisplayName), {}) ] .ExtensionContent() [ SNew(SBorder) .Visibility(PPGroup.Alert->Visibility) .ToolTip( SNew(SToolTip) .Text(PPGroup.Alert->Text) ) .Padding(3, 3, 3, 3) .BorderBackgroundColor(FColor::Transparent) [ SNew(SImage) .DesiredSizeOverride(FVector2D(16,16)) .Image_Lambda([Type=PPGroup.Alert->Type]() { return (Type == FAlert::EType::Warning) ? FAppStyle::Get().GetBrush("Icons.WarningWithColor") : FAppStyle::Get().GetBrush("Icons.ErrorWithColor"); }) .ColorAndOpacity(FSlateColor::UseForeground()) ] ]; } static const FString ColorGradingName = TEXT("Color Grading"); // Only enable group reset on color grading category groups if (PPGroup.RawGroupName.Contains(ColorGradingName)) { SimpleGroup.EnableReset(true); } for (auto& SimpleProperty : PPGroup.SimplePropertyHandles) { SimpleGroup.AddPropertyRow(SimpleProperty.ToSharedRef()); } if (PPGroup.AdvancedPropertyHandles.Num() > 0) { IDetailGroup& AdvancedGroup = SimpleGroup.AddGroup(*(PPGroup.RawGroupName+TEXT("Advanced")), LOCTEXT("PostProcessAdvancedGroup", "Advanced")); for (auto& AdvancedProperty : PPGroup.AdvancedPropertyHandles) { AdvancedGroup.AddPropertyRow(AdvancedProperty.ToSharedRef()); } } } } } } void FPostProcessSettingsCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { bool bShowHeader = !StructPropertyHandle->HasMetaData(ShowPostProcessCategoriesName) && !StructPropertyHandle->HasMetaData(ShowOnlyInnerPropertiesName); if (bShowHeader) { HeaderRow.NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ]; HeaderRow.ValueContent() [ StructPropertyHandle->CreatePropertyValueWidget() ]; } } void FWeightedBlendableCustomization::AddDirectAsset(TSharedRef StructPropertyHandle, TSharedPtr Weight, TSharedPtr Value, UClass* Class) { Weight->SetValue(1.0f); { TArray Objects; StructPropertyHandle->GetOuterObjects(Objects); TArray Values; for (TArray::TConstIterator It = Objects.CreateConstIterator(); It; It++) { UObject* Obj = *It; const UObject* NewObj = NewObject(Obj, Class); FString Str = NewObj->GetPathName(); Values.Add(Str); } Value->SetPerObjectValues(Values); } } void FWeightedBlendableCustomization::AddIndirectAsset(TSharedPtr Weight) { Weight->SetValue(1.0f); } EVisibility FWeightedBlendableCustomization::IsWeightVisible(TSharedPtr Weight) const { float WeightValue = 1.0f; Weight->GetValue(WeightValue); return (WeightValue >= 0) ? EVisibility::Visible : EVisibility::Hidden; } FText FWeightedBlendableCustomization::GetDirectAssetName(TSharedPtr Value) const { UObject* RefObject = 0; Value->GetValue(RefObject); check(RefObject); return FText::FromString(RefObject->GetFullName()); } FReply FWeightedBlendableCustomization::JumpToDirectAsset(TSharedPtr Value) { UObject* RefObject = 0; Value->GetValue(RefObject); GEditor->GetEditorSubsystem()->OpenEditorForAsset(RefObject); return FReply::Handled(); } TSharedRef FWeightedBlendableCustomization::GenerateContentWidget(TSharedRef StructPropertyHandle, UPackage* Package, TSharedPtr Weight, TSharedPtr Value) { bool bSeparatorIsNeeded = false; FMenuBuilder MenuBuilder(true, NULL); { for (TObjectIterator It; It; ++It) { if (It->IsChildOf(UFactory::StaticClass())) { UFactory* Factory = It->GetDefaultObject(); check(Factory); UClass* SupportedClass = Factory->GetSupportedClass(); if (SupportedClass) { if (SupportedClass->ImplementsInterface(UBlendableInterface::StaticClass())) { // At the moment we know about 3 Blendables: Material, UMaterialInstanceConstant, LightPropagationVolumeBlendable // The materials are not that useful to have here (hard to reference) so we suppress them here if (!( SupportedClass == UMaterial::StaticClass() || SupportedClass == UMaterialInstanceConstant::StaticClass() )) { FUIAction Direct2(FExecuteAction::CreateSP(this, &FWeightedBlendableCustomization::AddDirectAsset, StructPropertyHandle, Weight, Value, SupportedClass)); FName ClassName = SupportedClass->GetFName(); MenuBuilder.AddMenuEntry(FText::FromString(ClassName.GetPlainNameString()), LOCTEXT("Blendable_DirectAsset2h", "Creates an asset that is owned by the containing object"), FSlateIcon(), Direct2); bSeparatorIsNeeded = true; } } } } } if (bSeparatorIsNeeded) { MenuBuilder.AddMenuSeparator(); } FUIAction Indirect(FExecuteAction::CreateSP(this, &FWeightedBlendableCustomization::AddIndirectAsset, Weight)); MenuBuilder.AddMenuEntry(LOCTEXT("Blendable_IndirectAsset", "Asset reference"), LOCTEXT("Blendable_IndirectAsseth", "reference a Blendable asset (owned by a content package), e.g. material with Post Process domain"), FSlateIcon(), Indirect); } TSharedRef Switcher = SNew(SWidgetSwitcher) .WidgetIndex(this, &FWeightedBlendableCustomization::ComputeSwitcherIndex, StructPropertyHandle, Package, Weight, Value); Switcher->AddSlot() [ SNew(SComboButton) .ButtonContent() [ SNew(STextBlock) .Text(LOCTEXT("Blendable_ChooseElement", "Choose")) ] .ContentPadding(FMargin(6.0, 2.0)) .MenuContent() [ MenuBuilder.MakeWidget() ] ]; Switcher->AddSlot() [ SNew(SButton) .ContentPadding(FMargin(0,0)) .Text(this, &FWeightedBlendableCustomization::GetDirectAssetName, Value) .OnClicked(this, &FWeightedBlendableCustomization::JumpToDirectAsset, Value) ]; Switcher->AddSlot() [ SNew(SObjectPropertyEntryBox) .PropertyHandle(Value) ]; return Switcher; } int32 FWeightedBlendableCustomization::ComputeSwitcherIndex(TSharedRef StructPropertyHandle, UPackage* Package, TSharedPtr Weight, TSharedPtr Value) const { float WeightValue = 1.0f; UObject* RefObject = 0; Weight->GetValue(WeightValue); Value->GetValue(RefObject); if (RefObject) { UPackage* PropPackage = RefObject->GetOutermost(); return (PropPackage == Package) ? 1 : 2; } else { return (WeightValue < 0.0f) ? 0 : 2; } } void FWeightedBlendableCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { // we don't have children but this is a pure virtual so we need to override } void FWeightedBlendableCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { TSharedPtr SharedWeightProp; { TSharedPtr ChildHandle = StructPropertyHandle->GetChildHandle(FName(TEXT("Weight"))); if (ChildHandle.IsValid() && ChildHandle->GetProperty()) { SharedWeightProp = ChildHandle; } } TSharedPtr SharedValueProp; { TSharedPtr ChildHandle = StructPropertyHandle->GetChildHandle(FName(TEXT("Object"))); if (ChildHandle.IsValid() && ChildHandle->GetProperty()) { SharedValueProp = ChildHandle; } } float WeightValue = 1.0f; UObject* RefObject = 0; SharedWeightProp->GetValue(WeightValue); SharedValueProp->GetValue(RefObject); UPackage* StructPackage = 0; { const TSharedPtr ParentHandle = StructPropertyHandle->GetParentHandle(); TArray Objects; StructPropertyHandle->GetOuterObjects(Objects); for (TArray::TConstIterator It = Objects.CreateConstIterator(); It; It++) { UObject* ref = *It; if (StructPackage) { // Differing outermost package values indicate that the current RefObject refers to post-process // volumes selected within different levels, e.g. persistent and a sub-level. // In this case, do not store a package name. It is only used by ComputeSwitcherIndex() to determine direct // vs. indirect assets in the post process materials/blendables array. When more than one volume is selected, the direct // asset entries will simple read 'Multiple values' since each belongs to separate post-process volumes. if (StructPackage != ref->GetOutermost()) { StructPackage = NULL; break; } } else { StructPackage = ref->GetOutermost(); } } } HeaderRow.NameContent() [ SNew(SHorizontalBox) .Visibility(this, &FWeightedBlendableCustomization::IsWeightVisible, SharedWeightProp) +SHorizontalBox::Slot() [ SNew(SBox) .MinDesiredWidth(60.0f) .MaxDesiredWidth(60.0f) [ SharedWeightProp->CreatePropertyValueWidget() ] ] ]; HeaderRow.ValueContent() .MaxDesiredWidth(0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() [ GenerateContentWidget(StructPropertyHandle, StructPackage, SharedWeightProp, SharedValueProp) ] ]; } #undef LOCTEXT_NAMESPACE