// Copyright Epic Games, Inc. All Rights Reserved. #include "DetailPropertyRow.h" #include "CategoryPropertyNode.h" #include "CustomChildBuilder.h" #include "DetailCategoryGroupNode.h" #include "DetailItemNode.h" #include "DetailWidgetRow.h" #include "ItemPropertyNode.h" #include "ObjectPropertyNode.h" #include "ObjectPropertyNode.h" #include "PropertyCustomizationHelpers.h" #include "PropertyEditorHelpers.h" #include "PropertyHandleImpl.h" #include "StructurePropertyNode.h" #include "Modules/ModuleManager.h" #include "Widgets/Layout/SSpacer.h" #include "IDetailPropertyChildrenCustomizationHandler.h" #include "UObject/PropertyOptional.h" #define LOCTEXT_NAMESPACE "DetailPropertyRow" FDetailPropertyRow::FDetailPropertyRow(TSharedPtr InPropertyNode, TSharedRef InParentCategory, TSharedPtr InExternalRootNode) : PropertyNode( InPropertyNode ) , ParentCategory( InParentCategory ) , ExternalRootNode( InExternalRootNode ) , bShowPropertyButtons( true ) , bShowCustomPropertyChildren( true ) , bForceAutoExpansion( false ) , bCachedCustomTypeInterface(false) { if (PropertyNode.IsValid()) { // Is this a set optional property? if (PropertyEditorHelpers::IsOptionalProperty(*PropertyNode) && PropertyNode->GetNumChildNodes()) { // If we are selecting multiple options with different states (ie set/unset) dont use the // Value node so optional multi-select logic is displayed (see SPropertyEditorOptional.h). uint8 MixedValues = 0; FProperty* MyProperty = PropertyNode->GetProperty(); if (FOptionalProperty* OptionalProperty = CastField(MyProperty)) { void* Optional = NULL; FReadAddressList Addresses; if (PropertyNode->GetReadAddress(Addresses)) { for (int i = 0; i < Addresses.Num(); i++) { Optional = Addresses.GetAddress(i); MixedValues |= OptionalProperty->IsSet(Optional) ? 1 : 2; if (MixedValues == 3) { break; } } } } if (MixedValues != 3) { // If we are set, only display our set value which is our child node bForceShowOnlyChildren = true; } } TSharedRef PropertyNodeRef = PropertyNode.ToSharedRef(); const TSharedRef Utilities = InParentCategory->GetParentLayoutImpl()->GetPropertyUtilities(); if (PropertyNode->AsCategoryNode() == nullptr) { MakePropertyEditor(PropertyNodeRef, Utilities, PropertyEditor); } static FName InlineCustomizationKeyMeta("AllowEditInlineCustomization"); if (PropertyNode->AsComplexNode() && ExternalRootNode.IsValid()) // AsComplexNode works both for objects and structs { // We are showing an entirely different object inline. Generate a layout for it now. if (TSharedPtr DetailsView = InParentCategory->GetDetailsViewSharedPtr()) { ExternalObjectLayout = MakeShared(); DetailsView->UpdateSinglePropertyMap(InExternalRootNode, *ExternalObjectLayout, true); } } else if ((PropertyNode->HasNodeFlags(EPropertyNodeFlags::EditInlineNew) || PropertyNode->HasNodeFlags(EPropertyNodeFlags::DynamicInstance)) && PropertyNode->GetProperty()->HasMetaData(InlineCustomizationKeyMeta)) { // Allow customization of 'edit inline new' objects if the metadata key has been specified. // The child of this node, if set, will be an object node that we will want to treat as an 'external object layout' TSharedPtr ChildNode = PropertyNode->GetNumChildNodes() > 0 ? PropertyNode->GetChildNode(0) : nullptr; TSharedPtr ComplexChildNode = StaticCastSharedPtr(ChildNode); if (ComplexChildNode.IsValid()) { // We are showing an entirely different object inline. Generate a layout for it now. if (TSharedPtr DetailsView = InParentCategory->GetDetailsViewSharedPtr()) { ExternalObjectLayout = MakeShared(); DetailsView->UpdateSinglePropertyMap(ComplexChildNode, *ExternalObjectLayout, true); } } } if (PropertyNode->GetPropertyKeyNode().IsValid()) { TSharedPtr FoundPropertyCustomisation = GetPropertyCustomization(PropertyNode->GetPropertyKeyNode().ToSharedRef(), ParentCategory.Pin().ToSharedRef()); bool bInlineRow = FoundPropertyCustomisation != nullptr ? FoundPropertyCustomisation->ShouldInlineKey() : false; static FName InlineKeyMeta("ForceInlineRow"); bInlineRow |= InPropertyNode->GetParentNode()->GetProperty()->HasMetaData(InlineKeyMeta); // Only create the property editor if it's not a struct or if it requires to be inlined (and has customization) if (!NeedsKeyNode(PropertyNodeRef, InParentCategory) || (bInlineRow && FoundPropertyCustomisation != nullptr)) { CachedKeyCustomTypeInterface = FoundPropertyCustomisation; MakePropertyEditor(PropertyNode->GetPropertyKeyNode().ToSharedRef(), Utilities, PropertyKeyEditor); } } } PropertyHandle = InParentCategory->GetParentLayoutImpl()->GetPropertyHandle(PropertyNode); } bool FDetailPropertyRow::NeedsKeyNode(TSharedRef InPropertyNode, TSharedRef InParentCategory) { FStructProperty* KeyStructProp = CastField(InPropertyNode->GetPropertyKeyNode()->GetProperty()); return KeyStructProp != nullptr; } IDetailPropertyRow& FDetailPropertyRow::DisplayName( const FText& InDisplayName ) { if (PropertyNode.IsValid()) { PropertyNode->SetDisplayNameOverride( InDisplayName ); } return *this; } IDetailPropertyRow& FDetailPropertyRow::ToolTip( const FText& InToolTip ) { if (PropertyNode.IsValid()) { PropertyNode->SetToolTipOverride( InToolTip ); } return *this; } IDetailPropertyRow& FDetailPropertyRow::ShowPropertyButtons( bool bInShowPropertyButtons ) { bShowPropertyButtons = bInShowPropertyButtons; return *this; } IDetailPropertyRow& FDetailPropertyRow::EditCondition( TAttribute EditConditionValue, FOnBooleanValueChanged OnEditConditionValueChanged, ECustomEditConditionMode EditConditionMode ) { CustomEditConditionValue = EditConditionValue; CustomEditConditionValueChanged = OnEditConditionValueChanged; CustomEditConditionMode = EditConditionMode; return *this; } IDetailPropertyRow& FDetailPropertyRow::EditConditionHides(bool bEditConditionHidesValue) { bCustomEditConditionHides = bEditConditionHidesValue; return *this; } IDetailPropertyRow& FDetailPropertyRow::IsEnabled( TAttribute InIsEnabled ) { CustomIsEnabledAttrib = InIsEnabled; return *this; } IDetailPropertyRow& FDetailPropertyRow::ShouldAutoExpand(bool bInForceExpansion) { bForceAutoExpansion = bInForceExpansion; return *this; } IDetailPropertyRow& FDetailPropertyRow::Visibility( TAttribute Visibility ) { PropertyVisibility = Visibility; return *this; } IDetailPropertyRow& FDetailPropertyRow::OverrideResetToDefault(const FResetToDefaultOverride& ResetToDefault) { CustomResetToDefault = ResetToDefault; return *this; } IDetailPropertyRow& FDetailPropertyRow::DragDropHandler(TSharedPtr InDragDropHandler) { CustomDragDropHandler = InDragDropHandler; return *this; } bool FDetailPropertyRow::IsExpanded() const { if (GetPropertyNode()) { return GetPropertyNode()->HasNodeFlags(EPropertyNodeFlags::Expanded); } return false; } void FDetailPropertyRow::GetDefaultWidgets( TSharedPtr& OutNameWidget, TSharedPtr& OutValueWidget, bool bAddWidgetDecoration ) { FDetailWidgetRow Row; GetDefaultWidgets(OutNameWidget, OutValueWidget, Row, bAddWidgetDecoration); } void FDetailPropertyRow::GetDefaultWidgets( TSharedPtr& OutNameWidget, TSharedPtr& OutValueWidget, FDetailWidgetRow& Row, bool bAddWidgetDecoration ) { TSharedPtr CustomTypeRow; TSharedPtr& CustomTypeInterface = GetTypeInterface(); if ( CustomTypeInterface.IsValid() ) { CustomTypeRow = MakeShared(); CustomTypeInterface->CustomizeHeader(PropertyHandle.ToSharedRef(), *CustomTypeRow, *this); } SetWidgetRowProperties(Row); MakeNameOrKeyWidget(Row, CustomTypeRow); MakeValueWidget(Row, CustomTypeRow, bAddWidgetDecoration); OutNameWidget = Row.NameWidget.Widget; OutValueWidget = Row.ValueWidget.Widget; } bool FDetailPropertyRow::HasColumns() const { // Regular properties always have columns return !CustomPropertyWidget.IsValid() || CustomPropertyWidget->HasColumns(); } bool FDetailPropertyRow::ShowOnlyChildren() const { return bForceShowOnlyChildren || (PropertyTypeLayoutBuilder.IsValid() && CustomPropertyWidget.IsValid() && !CustomPropertyWidget->HasAnyContent()); } bool FDetailPropertyRow::RequiresTick() const { return PropertyVisibility.IsBound() || IsOnlyVisibleWhenEditConditionMet(); } FDetailWidgetRow& FDetailPropertyRow::CustomWidget( bool bShowChildren ) { bShowCustomPropertyChildren = bShowChildren; CustomPropertyWidget = MakeShared(); return *CustomPropertyWidget; } FDetailWidgetDecl* FDetailPropertyRow::CustomNameWidget() { return CustomPropertyWidget.IsValid() ? &CustomPropertyWidget->NameContent() : nullptr; } FDetailWidgetDecl* FDetailPropertyRow::CustomValueWidget() { return CustomPropertyWidget.IsValid() ? &CustomPropertyWidget->ValueContent() : nullptr; } FDetailWidgetDecl* FDetailPropertyRow::CustomResetToDefaultWidget() { return CustomPropertyWidget.IsValid() ? &CustomPropertyWidget->ResetToDefaultContent() : nullptr; } TSharedPtr FDetailPropertyRow::GetThumbnailPool() const { TSharedPtr ParentCategoryPinned = ParentCategory.Pin(); return ParentCategoryPinned.IsValid() ? ParentCategoryPinned->GetParentLayout().GetThumbnailPool() : NULL; } TSharedPtr FDetailPropertyRow::GetPropertyUtilities() const { TSharedPtr ParentCategoryPinned = ParentCategory.Pin(); if (ParentCategoryPinned.IsValid() && ParentCategoryPinned->IsParentLayoutValid()) { return ParentCategoryPinned->GetParentLayout().GetPropertyUtilities(); } return nullptr; } FDetailWidgetRow FDetailPropertyRow::GetWidgetRow() { if( HasColumns() ) { FDetailWidgetRow Row; SetWidgetRowProperties(Row); MakeNameOrKeyWidget( Row, CustomPropertyWidget ); MakeValueWidget( Row, CustomPropertyWidget ); return Row; } else { return *CustomPropertyWidget; } } TArrayView> FDetailPropertyRow::GetPropertyHandles() const { if (CustomPropertyWidget) { return CustomPropertyWidget->PropertyHandles; } // view single item as a c-array of size one return TArrayView>(const_cast*>(&PropertyHandle), 1); } FText FDetailPropertyRow::GetFilterTextString() const { if (CustomPropertyWidget) { return CustomPropertyWidget->FilterTextString; } else { return {}; } } static bool IsHeaderRowRequired(const TSharedPtr& PropertyHandle) { TSharedPtr ParentHandle = PropertyHandle->GetParentHandle(); while (ParentHandle.IsValid()) { if (ParentHandle->AsMap().IsValid()) { return true; } ParentHandle = ParentHandle->GetParentHandle(); } return false; } static void FixEmptyHeaderRowInContainers(const TSharedPtr& PropertyHandle, const TSharedPtr& HeaderRow) { if (IsHeaderRowRequired(PropertyHandle)) { if (!HeaderRow->HasAnyContent()) { if (!HeaderRow->HasNameContent()) { HeaderRow->NameContent() [ PropertyHandle->CreatePropertyNameWidget() ]; } if (!HeaderRow->HasValueContent()) { HeaderRow->ValueContent() [ PropertyHandle->CreatePropertyValueWidget(false) ]; } } } } void FDetailPropertyRow::OnItemNodeInitialized( TSharedRef InParentCategory, const TAttribute& InIsParentEnabled, TSharedPtr InParentGroup) { IsParentEnabled = InIsParentEnabled; TSharedPtr& CustomTypeInterface = GetTypeInterface(); // Don't customize the user already customized if (!CustomPropertyWidget.IsValid() && CustomTypeInterface.IsValid()) { CustomPropertyWidget = MakeShared(); CustomTypeInterface->CustomizeHeader(PropertyHandle.ToSharedRef(), *CustomPropertyWidget, *this); FixEmptyHeaderRowInContainers(PropertyHandle, CustomPropertyWidget); // set initial value of enabled attribute to settings from struct customization if (CustomPropertyWidget->IsEnabledAttr.IsSet()) { CustomIsEnabledAttrib = CustomPropertyWidget->IsEnabledAttr; } // set initial value of auto-expand from struct customization if (CustomPropertyWidget->ForceAutoExpansion.IsSet()) { bForceAutoExpansion = CustomPropertyWidget->ForceAutoExpansion.GetValue(); } } TSharedPtr DetailsView = InParentCategory->GetDetailsViewSharedPtr(); IDetailPropertyChildrenCustomizationHandler* CustomizationHandler = DetailsView ? DetailsView->GetChildrenCustomizationHandler().Get() : nullptr; if (CustomizationHandler && CustomizationHandler->ShouldCustomizeChildren(PropertyHandle.ToSharedRef())) { PropertyTypeLayoutBuilder = MakeShared(InParentCategory, InParentGroup); CustomizationHandler->CustomizeChildren(*PropertyTypeLayoutBuilder, PropertyHandle); } else if( bShowCustomPropertyChildren && CustomTypeInterface.IsValid() ) { PropertyTypeLayoutBuilder = MakeShared(InParentCategory, InParentGroup); /** Does this row pass its custom reset behavior to its children? */ if (CustomResetToDefault.IsSet() && CustomResetToDefault->PropagatesToChildren()) { PropertyTypeLayoutBuilder->OverrideResetChildrenToDefault(CustomResetToDefault.GetValue()); } CustomTypeInterface->CustomizeChildren(PropertyHandle.ToSharedRef(), *PropertyTypeLayoutBuilder, *this); } } void FDetailPropertyRow::OnGenerateChildren( FDetailNodeList& OutChildren ) { if (PropertyNode->AsCategoryNode() && PropertyNode->GetParentNode() && !PropertyNode->GetParentNode()->AsObjectNode()) { // This is a sub-category. Populate from SubCategory builder TSharedRef ParentCategoryRef = ParentCategory.Pin().ToSharedRef(); TSharedPtr LayoutBuilder = ParentCategoryRef->GetParentLayoutImpl(); TSharedPtr MyCategory = LayoutBuilder->GetSubCategoryImpl(PropertyNode->AsCategoryNode()->GetCategoryName()); if(MyCategory.IsValid()) { MyCategory->GenerateLayout(); // Ignore the header of the category by just getting the categories children directly. We are the header in this case. // Also ignore visibility here as we dont have a filter yet and the children will be filtered later anyway const bool bIgnoreVisibility = true; const bool bIgnoreAdvancedDropdown = true; MyCategory->GetGeneratedChildren(OutChildren, bIgnoreVisibility, bIgnoreAdvancedDropdown); } else { // Fall back to the default if we can't find the category implementation GenerateChildrenForPropertyNode(PropertyNode, OutChildren); } } else if (PropertyNode->AsCategoryNode() || PropertyNode->GetProperty() || ExternalObjectLayout.IsValid()) { GenerateChildrenForPropertyNode( PropertyNode, OutChildren ); } } void FDetailPropertyRow::GenerateChildrenForPropertyNode( TSharedPtr& RootPropertyNode, FDetailNodeList& OutChildren ) { // Children should be disabled if we are disabled TAttribute ParentEnabledState = TAttribute::CreateSP(this, &FDetailPropertyRow::GetEnabledState); if( PropertyTypeLayoutBuilder.IsValid() && bShowCustomPropertyChildren ) { const TArray< FDetailLayoutCustomization >& ChildRows = PropertyTypeLayoutBuilder->GetChildCustomizations(); for( int32 ChildIndex = 0; ChildIndex < ChildRows.Num(); ++ChildIndex ) { TSharedRef ChildNodeItem = MakeShared(ChildRows[ChildIndex], ParentCategory.Pin().ToSharedRef(), ParentEnabledState); ChildNodeItem->Initialize(); OutChildren.Add( ChildNodeItem ); } } else if (ExternalObjectLayout.IsValid() && ExternalObjectLayout->DetailLayout->HasDetails()) { OutChildren.Append(ExternalObjectLayout->DetailLayout->GetAllRootTreeNodes()); } else if ((bShowCustomPropertyChildren || !CustomPropertyWidget.IsValid()) && RootPropertyNode->GetNumChildNodes() > 0) { TSharedRef ParentCategoryRef = ParentCategory.Pin().ToSharedRef(); IDetailLayoutBuilder& LayoutBuilder = ParentCategoryRef->GetParentLayout(); FProperty* ParentProperty = RootPropertyNode->GetProperty(); const bool bStructProperty = ParentProperty && ParentProperty->IsA(); const bool bMapProperty = ParentProperty && ParentProperty->IsA(); const bool bSetProperty = ParentProperty && ParentProperty->IsA(); for( int32 ChildIndex = 0; ChildIndex < RootPropertyNode->GetNumChildNodes(); ++ChildIndex ) { TSharedPtr ChildNode = RootPropertyNode->GetChildNode(ChildIndex); if (!LayoutBuilder.IsPropertyPathAllowed(ChildNode->GetPropertyPath())) { ChildNode->SetNodeFlags( EPropertyNodeFlags::RequiresValidation, false); ChildNode->SetNodeFlags( EPropertyNodeFlags::IsBeingFiltered | EPropertyNodeFlags::SkipChildValidation, true); continue; } if( ChildNode.IsValid() && ChildNode->HasNodeFlags( EPropertyNodeFlags::IsCustomized ) == 0 ) { if( ChildNode->AsObjectNode() ) { // Skip over object nodes and generate their children. Object nodes are not visible GenerateChildrenForPropertyNode( ChildNode, OutChildren ); } // Only struct children can have custom visibility that is different from their parent. else if ( !bStructProperty || LayoutBuilder.IsPropertyVisible(FPropertyAndParent(ChildNode.ToSharedRef())) ) { TArray> PropNodes; bool bHasKeyNode = false; FCategoryPropertyNode* CategoryNode = ChildNode->AsCategoryNode(); if (CategoryNode && CategoryNode->GetCategoryName() == NAME_None) { // Skip the category node and repoarent everything to the ParentCategory GenerateChildrenForPropertyNode(ChildNode, OutChildren); } else { // Create and initialize the child first FDetailLayoutCustomization Customization; Customization.PropertyRow = MakeShared(ChildNode, ParentCategoryRef); if (CustomResetToDefault.IsSet() && CustomResetToDefault->PropagatesToChildren()) { Customization.PropertyRow->OverrideResetToDefault(CustomResetToDefault.GetValue()); } TSharedRef ChildNodeItem = MakeShared(Customization, ParentCategoryRef, ParentEnabledState); ChildNodeItem->Initialize(); if ( ChildNode->GetPropertyKeyNode().IsValid() ) { // If the child has a key property, only create a second node for the key if the child did not already create a property // editor for it if ( !Customization.PropertyRow->PropertyKeyEditor.IsValid() ) { FDetailLayoutCustomization KeyCustom; KeyCustom.PropertyRow = MakeShared(ChildNode->GetPropertyKeyNode(), ParentCategoryRef); TSharedRef KeyNodeItem = MakeShared(KeyCustom, ParentCategoryRef, ParentEnabledState); KeyNodeItem->Initialize(); PropNodes.Add(KeyNodeItem); bHasKeyNode = true; } } // Add the child node PropNodes.Add(ChildNodeItem); // For set properties, set the name override to match the index if (bSetProperty) { ChildNode->SetDisplayNameOverride(FText::AsNumber(ChildIndex)); } if (bMapProperty && bHasKeyNode) { // Group the key/value nodes for map properties static FText KeyValueGroupNameFormat = LOCTEXT("KeyValueGroupName", "Element {0}"); FText KeyValueGroupName = FText::Format(KeyValueGroupNameFormat, ChildIndex); TSharedRef KeyValueGroupNode = MakeShared(FName(*KeyValueGroupName.ToString()), ParentCategoryRef); KeyValueGroupNode->SetChildren(PropNodes); KeyValueGroupNode->SetShowBorder(false); KeyValueGroupNode->SetHasSplitter(true); OutChildren.Add(KeyValueGroupNode); } else { OutChildren.Append(PropNodes); } } } } } } } FName FDetailPropertyRow::GetRowName() const { if (HasExternalProperty()) { if (GetCustomExpansionId() != NAME_None) { return GetCustomExpansionId(); } else if (FProperty* ExternalRootProperty = ExternalRootNode->GetProperty()) { return ExternalRootProperty->GetFName(); } } if (GetPropertyNode()) { if (FProperty* Property = GetPropertyNode()->GetProperty()) { return Property->GetFName(); } } return NAME_None; } TSharedRef FDetailPropertyRow::MakePropertyEditor(const TSharedRef& InPropertyNode, const TSharedRef& PropertyUtilities, TSharedPtr& InEditor ) { if( !InEditor.IsValid() ) { InEditor = FPropertyEditor::Create( InPropertyNode, PropertyUtilities ); } return InEditor.ToSharedRef(); } TSharedPtr FDetailPropertyRow::GetPropertyCustomization(const TSharedRef& InPropertyNode, const TSharedRef& InParentCategory) { TSharedPtr CustomInterface; if (!PropertyEditorHelpers::IsStaticArray(*InPropertyNode)) { FProperty* Property = InPropertyNode->GetProperty(); TSharedPtr PropHandle = InParentCategory->GetParentLayoutImpl()->GetPropertyHandle(InPropertyNode); static FName NAME_PropertyEditor("PropertyEditor"); FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked(NAME_PropertyEditor); FPropertyTypeLayoutCallback LayoutCallback; if (Property != nullptr) { LayoutCallback = PropertyEditorModule.GetPropertyTypeCustomization(Property, *PropHandle, InParentCategory->GetCustomPropertyTypeLayoutMap()); } else { // This add support to objects and structs added to the category with AddExternalObjectProperty / AddExternalStructureProperty if (FComplexPropertyNode* ComplexNode = InPropertyNode->AsComplexNode()) { if (FObjectPropertyNode* ObjectNode = ComplexNode->AsObjectNode()) { UClass* PropertyClass = ObjectNode->GetObjectBaseClass(); while (PropertyClass) { LayoutCallback = PropertyEditorModule.FindPropertyTypeLayoutCallback(PropertyClass->GetFName(), *PropHandle, InParentCategory->GetCustomPropertyTypeLayoutMap()); if (LayoutCallback.IsValid()) { break; } PropertyClass = PropertyClass->GetSuperClass(); } } else if (FStructurePropertyNode* StructureNode = ComplexNode->AsStructureNode()) { const FName PropertyTypeName = StructureNode->GetBaseStructure()->GetFName(); LayoutCallback = PropertyEditorModule.FindPropertyTypeLayoutCallback(PropertyTypeName, *PropHandle, InParentCategory->GetCustomPropertyTypeLayoutMap()); } } } if (LayoutCallback.IsValid()) { if (PropHandle->IsValidHandle()) { CustomInterface = LayoutCallback.GetCustomizationInstance(); } } } return CustomInterface; } template void MakeExternalStructPropertyRowCustomization(const T& Struct, const UStruct* StructClass, FName PropertyName, TSharedRef ParentCategory, FDetailLayoutCustomization& OutCustomization, const FAddPropertyParams& Parameters, bool bAllowChildren) { TSharedRef RootPropertyNode = MakeShared(); //SET RootPropertyNode->SetStructure(Struct); FPropertyNodeInitParams InitParams; InitParams.ParentNode = nullptr; InitParams.Property = nullptr; InitParams.ArrayOffset = 0; InitParams.ArrayIndex = INDEX_NONE; InitParams.bForceHiddenPropertyVisibility = Parameters.ShouldForcePropertyVisible() || FPropertySettings::Get().ShowHiddenProperties(); InitParams.bCreateCategoryNodes = PropertyName == NAME_None; InitParams.bAllowChildren = bAllowChildren; Parameters.OverrideAllowChildren(InitParams.bAllowChildren); Parameters.OverrideCreateCategoryNodes(InitParams.bCreateCategoryNodes); RootPropertyNode->InitNode(InitParams); ParentCategory->GetParentLayoutImpl()->AddExternalRootPropertyNode(RootPropertyNode); if (PropertyName != NAME_None) { TSharedPtr PropertyNode = RootPropertyNode->GenerateSingleChild(PropertyName); if (PropertyNode.IsValid()) { PropertyNode->RebuildChildren(); OutCustomization.PropertyRow = MakeShared(PropertyNode, ParentCategory, RootPropertyNode); OutCustomization.PropertyRow->SetCustomExpansionId(Parameters.GetUniqueId()); } } else { if (bAllowChildren && InitParams.bCreateCategoryNodes) { // Using the RootPropertyNode as the property node enables sub categories OutCustomization.PropertyRow = MakeShared(RootPropertyNode, ParentCategory, RootPropertyNode); OutCustomization.PropertyRow->SetCustomExpansionId(Parameters.GetUniqueId()); } else { static const FName PropertyEditorModuleName("PropertyEditor"); FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked(PropertyEditorModuleName); // Make a "fake" struct property to represent the entire struct FStructProperty* StructProperty = PropertyEditorModule.RegisterStructProperty(StructClass); // Generate a node for the struct TSharedPtr ItemNode = MakeShared(); FPropertyNodeInitParams ItemNodeInitParams; ItemNodeInitParams.ParentNode = RootPropertyNode; ItemNodeInitParams.Property = StructProperty; ItemNodeInitParams.ArrayOffset = 0; ItemNodeInitParams.ArrayIndex = INDEX_NONE; ItemNodeInitParams.bAllowChildren = true; ItemNodeInitParams.bForceHiddenPropertyVisibility = Parameters.ShouldForcePropertyVisible() || FPropertySettings::Get().ShowHiddenProperties(); ItemNodeInitParams.bCreateCategoryNodes = false; ItemNode->InitNode(ItemNodeInitParams); RootPropertyNode->AddChildNode(ItemNode); OutCustomization.PropertyRow = MakeShared(ItemNode, ParentCategory, RootPropertyNode); OutCustomization.PropertyRow->SetCustomExpansionId(Parameters.GetUniqueId()); } } } void FDetailPropertyRow::MakeExternalPropertyRowCustomization(TSharedPtr StructData, FName PropertyName, TSharedRef ParentCategory, FDetailLayoutCustomization& OutCustomization, const FAddPropertyParams& Parameters) { MakeExternalStructPropertyRowCustomization<>( StructData, StructData->GetStruct(), PropertyName, ParentCategory, OutCustomization, Parameters, /* bAllowChildren= */ false ); } void FDetailPropertyRow::MakeExternalPropertyRowCustomization(TSharedPtr StructDataProvider, FName PropertyName, TSharedRef ParentCategory, struct FDetailLayoutCustomization& OutCustomization, const FAddPropertyParams& Parameters) { MakeExternalStructPropertyRowCustomization<>( StructDataProvider, StructDataProvider->GetBaseStructure(), PropertyName, ParentCategory, OutCustomization, Parameters, /* bAllowChildren= */ true ); } void FDetailPropertyRow::MakeExternalPropertyRowCustomization(const TArray& InObjects, FName PropertyName, TSharedRef ParentCategory, struct FDetailLayoutCustomization& OutCustomization, const FAddPropertyParams& Parameters) { TSharedRef RootPropertyNode = MakeShared(); for (UObject* Object : InObjects) { RootPropertyNode->AddObject(Object); } FPropertyNodeInitParams InitParams; InitParams.ParentNode = nullptr; InitParams.Property = nullptr; InitParams.ArrayOffset = 0; InitParams.ArrayIndex = INDEX_NONE; InitParams.bAllowChildren = false; InitParams.bForceHiddenPropertyVisibility = Parameters.ShouldForcePropertyVisible() || FPropertySettings::Get().ShowHiddenProperties(); InitParams.bCreateCategoryNodes = PropertyName == NAME_None; Parameters.OverrideAllowChildren(InitParams.bAllowChildren); Parameters.OverrideCreateCategoryNodes(InitParams.bCreateCategoryNodes); RootPropertyNode->InitNode(InitParams); if (PropertyName != NAME_None) { TSharedPtr PropertyNode = RootPropertyNode->GenerateSingleChild(PropertyName); if(PropertyNode.IsValid()) { // This is useless as PropertyNode should already be in the child nodes RootPropertyNode->AddChildNode(PropertyNode); if (InitParams.bCreateCategoryNodes) { PropertyNode->SetNodeFlags(EPropertyNodeFlags::ShowCategories, true); } else { PropertyNode->SetNodeFlags(EPropertyNodeFlags::ShowCategories, false); } PropertyNode->RebuildChildren(); OutCustomization.PropertyRow = MakeShared(PropertyNode, ParentCategory, RootPropertyNode); OutCustomization.PropertyRow->SetCustomExpansionId(Parameters.GetUniqueId()); } } else { OutCustomization.PropertyRow = MakeShared(RootPropertyNode, ParentCategory, RootPropertyNode); OutCustomization.PropertyRow->SetCustomExpansionId(Parameters.GetUniqueId()); } ParentCategory->GetParentLayoutImpl()->AddExternalRootPropertyNode(RootPropertyNode); } void FDetailPropertyRow::MakeChildPropertyRowCustomization(TSharedRef PropertyHandle, TSharedPtr StructDataProvider, FName PropertyName, TSharedRef ParentCategory, FDetailLayoutCustomization& OutCustomization, const FAddPropertyParams& Parameters, const FText& DisplayNameOverride) { TSharedRef PropertyHandleImpl = StaticCastSharedRef(PropertyHandle); TSharedPtr RootPropertyNode = StaticCastSharedPtr(PropertyHandleImpl->GetPropertyNode()); if (PropertyName != NAME_None) { TSharedPtr PropertyNode = RootPropertyNode->GenerateSingleChild(PropertyName); if (PropertyNode.IsValid()) { PropertyNode->RebuildChildren(); OutCustomization.PropertyRow = MakeShared(PropertyNode, ParentCategory); OutCustomization.PropertyRow->SetCustomExpansionId(Parameters.GetUniqueId()); } } else { // Generate a node for the struct TSharedRef StructPropertyNode = MakeShared(); StructPropertyNode->SetStructure(StructDataProvider); StructPropertyNode->SetDisplayNameOverride(DisplayNameOverride); // Make a "fake" struct property to represent the entire struct static const FName PropertyEditorModuleName("PropertyEditor"); FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked(PropertyEditorModuleName); FStructProperty* StructProperty = PropertyEditorModule.RegisterStructProperty(StructDataProvider->GetBaseStructure()); FPropertyNodeInitParams ItemNodeInitParams; ItemNodeInitParams.ParentNode = RootPropertyNode; ItemNodeInitParams.Property = StructProperty; ItemNodeInitParams.ArrayOffset = 0; ItemNodeInitParams.ArrayIndex = INDEX_NONE; ItemNodeInitParams.bAllowChildren = true; ItemNodeInitParams.bForceHiddenPropertyVisibility = Parameters.ShouldForcePropertyVisible() || FPropertySettings::Get().ShowHiddenProperties(); ItemNodeInitParams.bCreateCategoryNodes = false; StructPropertyNode->InitNode(ItemNodeInitParams); RootPropertyNode->AddChildNode(StructPropertyNode); OutCustomization.PropertyRow = MakeShared(StructPropertyNode, ParentCategory); OutCustomization.PropertyRow->SetCustomExpansionId(Parameters.GetUniqueId()); } } EVisibility FDetailPropertyRow::GetPropertyVisibility() const { if (IsOnlyVisibleWhenEditConditionMet() && !IsEditConditionMet()) { return EVisibility::Collapsed; } else if (CustomPropertyWidget.IsValid() && CustomPropertyWidget->VisibilityAttr.IsBound()) { return CustomPropertyWidget->VisibilityAttr.Get(); } return PropertyVisibility.Get(); } bool FDetailPropertyRow::HasEditCondition() const { return (PropertyEditor.IsValid() && PropertyEditor->HasEditCondition()) || CustomEditConditionValue.IsSet(); } bool FDetailPropertyRow::GetEnabledState() const { bool Result = IsParentEnabled.Get(true); Result = Result && CustomIsEnabledAttrib.Get(true); Result = Result && IsEditConditionMet(); return Result; } bool FDetailPropertyRow::IsEditConditionMet() const { bool bIsEditConditionMet = true; if (HasEditCondition()) { if (CustomEditConditionValue.IsSet()) { bIsEditConditionMet = bIsEditConditionMet && CustomEditConditionValue.Get(); // In override mode, we ship checking the native property edit condition if (CustomEditConditionMode == ECustomEditConditionMode::Override) { return bIsEditConditionMet; } } if (PropertyEditor.IsValid()) { bIsEditConditionMet = bIsEditConditionMet && PropertyEditor->IsEditConditionMet(); } } return bIsEditConditionMet; } bool FDetailPropertyRow::IsOnlyVisibleWhenEditConditionMet() const { return (PropertyEditor.IsValid() && PropertyEditor->IsOnlyVisibleWhenEditConditionMet()) || (bCustomEditConditionHides && HasEditCondition()); } TSharedPtr& FDetailPropertyRow::GetTypeInterface() { if (!bCachedCustomTypeInterface) { if (PropertyNode.IsValid() && ParentCategory.IsValid()) { CachedCustomTypeInterface = GetPropertyCustomization(PropertyNode.ToSharedRef(), ParentCategory.Pin().ToSharedRef()); } bCachedCustomTypeInterface = true; } return CachedCustomTypeInterface; } bool FDetailPropertyRow::GetForceAutoExpansion() const { return bForceAutoExpansion; } static void TogglePropertyEditorEditCondition(bool bValue, TWeakPtr PropertyEditorWeak) { TSharedPtr PropertyEditorPtr = PropertyEditorWeak.Pin(); if (PropertyEditorPtr.IsValid() && PropertyEditorPtr->IsEditConditionMet() != bValue) { PropertyEditorPtr->ToggleEditConditionState(); } } static void ExecuteCustomEditConditionToggle(bool bValue, FOnBooleanValueChanged CustomEditConditionToggle, TWeakPtr PropertyEditorWeak) { CustomEditConditionToggle.ExecuteIfBound(bValue); TSharedPtr PropertyEditorPtr = PropertyEditorWeak.Pin(); if (PropertyEditorPtr.IsValid()) { PropertyEditorPtr->GetPropertyNode()->InvalidateCachedState(); } } void FDetailPropertyRow::SetWidgetRowProperties(FDetailWidgetRow& Row) const { // set edit condition handlers - use customized if provided TAttribute EditConditionValue; if (HasEditCondition()) { EditConditionValue = TAttribute::CreateSP(this, &FDetailPropertyRow::IsEditConditionMet); } FOnBooleanValueChanged OnEditConditionValueChanged; if (CustomEditConditionValueChanged.IsBound()) { TWeakPtr PropertyEditorWeak = PropertyEditor; OnEditConditionValueChanged = FOnBooleanValueChanged::CreateStatic(&ExecuteCustomEditConditionToggle, CustomEditConditionValueChanged, PropertyEditorWeak); } else if (PropertyEditor.IsValid() && PropertyEditor->SupportsEditConditionToggle()) { TWeakPtr PropertyEditorWeak = PropertyEditor; OnEditConditionValueChanged = FOnBooleanValueChanged::CreateStatic(&TogglePropertyEditorEditCondition, PropertyEditorWeak); } Row.EditCondition(EditConditionValue, OnEditConditionValueChanged); Row.IsEnabled(CustomIsEnabledAttrib); Row.CustomResetToDefault = CustomResetToDefault; Row.CustomDragDropHandler = CustomDragDropHandler; Row.PropertyHandles.Add(GetPropertyHandle()); // set custom actions and reset to default if (CustomPropertyWidget.IsValid()) { Row.CopyMenuAction = CustomPropertyWidget->CopyMenuAction; Row.PasteMenuAction = CustomPropertyWidget->PasteMenuAction; Row.CustomMenuItems = CustomPropertyWidget->CustomMenuItems; Row.OnPasteFromTextDelegate = CustomPropertyWidget->OnPasteFromTextDelegate; Row.FilterTextString = CustomPropertyWidget->FilterTextString; if (CustomPropertyWidget->CustomResetToDefault.IsSet()) { ensureMsgf(!CustomResetToDefault.IsSet(), TEXT("Duplicate reset to default handlers set on both FDetailPropertyRow and CustomWidget()!")); Row.CustomResetToDefault = CustomPropertyWidget->CustomResetToDefault; } if(CustomPropertyWidget->HasResetToDefaultContent()) { Row.ResetToDefaultContent() [ CustomPropertyWidget->ResetToDefaultWidget.Widget ]; } } } void FDetailPropertyRow::MakeNameOrKeyWidget( FDetailWidgetRow& Row, const TSharedPtr InCustomRow ) const { EVerticalAlignment VerticalAlignment = VAlign_Center; EHorizontalAlignment HorizontalAlignment = HAlign_Fill; // We will only use key widgets for non-struct keys const bool bHasKeyNode = PropertyKeyEditor.IsValid(); if (!bHasKeyNode && InCustomRow.IsValid()) { VerticalAlignment = InCustomRow->NameWidget.VerticalAlignment; HorizontalAlignment = InCustomRow->NameWidget.HorizontalAlignment; } TAttribute IsEnabledAttrib = TAttribute::CreateSP( this, &FDetailPropertyRow::GetEnabledState ); TSharedRef NameHorizontalBox = SNew(SHorizontalBox) .Clipping(EWidgetClipping::OnDemand); TSharedPtr NameWidget = SNullWidget::NullWidget; // Key nodes take precedence over custom rows if (bHasKeyNode) { if (PropertyHandle->HasMetaData(TEXT("ReadOnlyKeys"))) { PropertyKeyEditor->GetPropertyNode()->SetNodeFlags(EPropertyNodeFlags::IsReadOnly, true); } // Does this key have a custom type, use it if (CachedKeyCustomTypeInterface) { // Create a widget that will properly represent the key const TSharedPtr CustomTypeWidget = MakeShared(); CachedKeyCustomTypeInterface->CustomizeHeader(PropertyKeyEditor->GetPropertyHandle(), *CustomTypeWidget, const_cast(*this)); NameWidget = CustomTypeWidget->ValueWidget.Widget; } else { NameWidget = SNew(SPropertyValueWidget, PropertyKeyEditor, ParentCategory.Pin()->GetParentLayoutImpl()->GetPropertyUtilities()) .IsEnabled(IsEnabledAttrib) .ShowPropertyButtons(false); } } else if (PropertyEditorHelpers::IsChildOfOption(*PropertyNode)) { TSharedRef ParentEditor = FPropertyEditor::Create( PropertyNode->GetParentNode()->AsShared(), ParentCategory.Pin()->GetParentLayoutImpl()->GetPropertyUtilities() ); NameWidget = SNew( SPropertyNameWidget, ParentEditor ) .IsEnabled( IsEnabledAttrib ); } else if (InCustomRow.IsValid()) { NameWidget = SNew( SBox ) .IsEnabled( IsEnabledAttrib ) [ InCustomRow->NameWidget.Widget ]; } else if (PropertyEditor.IsValid()) { NameWidget = SNew( SPropertyNameWidget, PropertyEditor ) .IsEnabled( IsEnabledAttrib ); } SHorizontalBox::FSlot* SlotPointer = nullptr; NameHorizontalBox->AddSlot() .Expose(SlotPointer) [ NameWidget.ToSharedRef() ]; if (bHasKeyNode) { SlotPointer->SetPadding(FMargin(0.0f, 0.0f, 2.0f, 0.0f)); } else if (InCustomRow.IsValid()) { // Allow custom name slots to fill all of the area. Eg., the user adds a SHorizontalBox with left and right align slots. SlotPointer->SetFillWidth(1.0f); } else { SlotPointer->SetAutoWidth(); } Row.NameContent() .HAlign( HorizontalAlignment ) .VAlign( VerticalAlignment ) [ NameHorizontalBox ]; } void FDetailPropertyRow::MakeValueWidget( FDetailWidgetRow& Row, const TSharedPtr InCustomRow, bool bAddWidgetDecoration ) const { EVerticalAlignment VerticalAlignment = VAlign_Center; EHorizontalAlignment HorizontalAlignment = HAlign_Left; TOptional MinWidth; TOptional MaxWidth; TAttribute IsEnabledAttrib = TAttribute::CreateSP( this, &FDetailPropertyRow::GetEnabledState ); TSharedRef ValueWidget = SNew( SHorizontalBox ) .IsEnabled( IsEnabledAttrib ); if (InCustomRow.IsValid()) { VerticalAlignment = InCustomRow->ValueWidget.VerticalAlignment; HorizontalAlignment = InCustomRow->ValueWidget.HorizontalAlignment; MinWidth = InCustomRow->ValueWidget.MinWidth; MaxWidth = InCustomRow->ValueWidget.MaxWidth; ValueWidget->AddSlot() .VAlign(VerticalAlignment) [ InCustomRow->ValueWidget.Widget ]; Row .ExtensionContent() [ InCustomRow->ExtensionWidget.Widget ]; } else if (PropertyEditor.IsValid()) { TSharedPtr PropertyValue; ValueWidget->AddSlot() [ SAssignNew( PropertyValue, SPropertyValueWidget, PropertyEditor, GetPropertyUtilities() ) .ShowPropertyButtons( false ) // We handle this ourselves .InWidgetRow(&Row) ]; MinWidth = PropertyValue->GetMinDesiredWidth(); MaxWidth = PropertyValue->GetMaxDesiredWidth(); } if (bAddWidgetDecoration && PropertyEditor.IsValid()) { if (bShowPropertyButtons) { TArray< TSharedRef > RequiredButtons; PropertyEditorHelpers::MakeRequiredPropertyButtons( PropertyEditor.ToSharedRef(), /*OUT*/RequiredButtons ); for( int32 ButtonIndex = 0; ButtonIndex < RequiredButtons.Num(); ++ButtonIndex ) { ValueWidget->AddSlot() .AutoWidth() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(4.0f, 1.0f, 0.0f, 1.0f) [ RequiredButtons[ButtonIndex] ]; } } // Don't add config hierarchy to container children, can't edit child properties at the hiearchy's per file level TSharedPtr ParentHandle = PropertyHandle->GetParentHandle(); bool bIsChildProperty = ParentHandle && (ParentHandle->AsArray() || ParentHandle->AsMap() || ParentHandle->AsSet()); if (!bIsChildProperty && PropertyHandle->HasMetaData(TEXT("ConfigHierarchyEditable"))) { ValueWidget->AddSlot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Left) .Padding(4.0f, 0.0f, 4.0f, 0.0f) [ PropertyCustomizationHelpers::MakeEditConfigHierarchyButton(FSimpleDelegate::CreateSP(PropertyEditor.ToSharedRef(), &FPropertyEditor::EditConfigHierarchy)) ]; } } Row.ValueContent() .HAlign( HorizontalAlignment ) .VAlign( VerticalAlignment ) .MinDesiredWidth( MinWidth ) .MaxDesiredWidth( MaxWidth ) [ ValueWidget ]; } #undef LOCTEXT_NAMESPACE