// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "PropertyEditorPrivatePCH.h" #include "PropertyNode.h" #include "PropertyHandleImpl.h" #include "PropertyEditorHelpers.h" #include "IPropertyUtilities.h" FDetailLayoutBuilderImpl::FDetailLayoutBuilderImpl(FClassToPropertyMap& InPropertyMap, const TSharedRef< class IPropertyUtilities >& InPropertyUtilities, const TSharedRef< IDetailsViewPrivate >& InDetailsView) : PropertyMap( InPropertyMap ) , PropertyDetailsUtilities( InPropertyUtilities ) , DetailsView( *InDetailsView ) , CurrentCustomizationClass( NULL ) { } IDetailCategoryBuilder& FDetailLayoutBuilderImpl::EditCategory( FName CategoryName, const FText& NewLocalizedDisplayName, ECategoryPriority::Type CategoryType ) { FText LocalizedDisplayName = NewLocalizedDisplayName; // Use a generic name if one was not specified if( CategoryName == NAME_None ) { static const FText GeneralString = NSLOCTEXT("DetailLayoutBuilderImpl", "General", "General"); static const FName GeneralName = *GeneralString.ToString(); CategoryName = GeneralName; LocalizedDisplayName = GeneralString; } TSharedPtr CategoryImpl; // If the default category map had a category by the provided name, remove it from the map as it is now customized if( !DefaultCategoryMap.RemoveAndCopyValue( CategoryName, CategoryImpl ) ) { // Default category map did not have a category by the requested name. Find or add it to the custom map TSharedPtr& NewCategoryImpl = CustomCategoryMap.FindOrAdd( CategoryName ); if( !NewCategoryImpl.IsValid() ) { NewCategoryImpl = MakeShareable( new FDetailCategoryImpl( CategoryName, SharedThis(this) ) ); // We want categories within a type to display in the order they were added but sorting is unstable so we make unique numbers uint32 SortOrder = (uint32)CategoryType * 1000 + (CustomCategoryMap.Num() - 1); NewCategoryImpl->SetSortOrder( SortOrder ); } CategoryImpl = NewCategoryImpl; } else { // Custom category should not exist yet as it was in the default category map checkSlow( !CustomCategoryMap.Contains( CategoryName ) && CategoryImpl.IsValid() ); CustomCategoryMap.Add( CategoryName, CategoryImpl ); // We want categories within a type to display in the order they were added but sorting is unstable so we make unique numbers uint32 SortOrder = (uint32)CategoryType * 1000 + (CustomCategoryMap.Num() - 1); CategoryImpl->SetSortOrder( SortOrder ); } CategoryImpl->SetDisplayName( CategoryName, LocalizedDisplayName ); return *CategoryImpl; } TSharedRef FDetailLayoutBuilderImpl::GetProperty( const FName PropertyPath, const UClass* ClassOutermost, FName InInstanceName ) { TSharedPtr PropertyHandle; TSharedPtr PropertyNodePtr = GetPropertyNode( PropertyPath, ClassOutermost, InInstanceName ); return GetPropertyHandle( PropertyNodePtr ); } void FDetailLayoutBuilderImpl::HideProperty( const TSharedPtr PropertyHandle ) { if( PropertyHandle.IsValid() && PropertyHandle->IsValidHandle() ) { // Mark the property as customized so it wont show up in the default location TSharedPtr PropertyNode = GetPropertyNode( PropertyHandle ); if( PropertyNode.IsValid() ) { SetCustomProperty( PropertyNode ); } } } void FDetailLayoutBuilderImpl::HideProperty( FName PropertyPath, const UClass* ClassOutermost, FName InstanceName ) { TSharedPtr PropertyNode = GetPropertyNode( PropertyPath, ClassOutermost, InstanceName ); if( PropertyNode.IsValid() ) { SetCustomProperty( PropertyNode ); } } void FDetailLayoutBuilderImpl::ForceRefreshDetails() { DetailsView.ForceRefresh(); } FDetailCategoryImpl& FDetailLayoutBuilderImpl::DefaultCategory( FName CategoryName ) { TSharedPtr& CategoryImpl = DefaultCategoryMap.FindOrAdd( CategoryName ); if( !CategoryImpl.IsValid() ) { CategoryImpl = MakeShareable( new FDetailCategoryImpl( CategoryName, SharedThis(this) ) ); // We want categories within a type to display in the order they were added but sorting is unstable so we make unique numbers uint32 SortOrder = (uint32)ECategoryPriority::Default * 1000 + (DefaultCategoryMap.Num() - 1); CategoryImpl->SetSortOrder( SortOrder ); } CategoryImpl->SetDisplayName( CategoryName, FText::GetEmpty() ); return *CategoryImpl; } void FDetailLayoutBuilderImpl::BuildCategories( const FCategoryMap& CategoryMap, TArray< TSharedRef >& OutSimpleCategories, TArray< TSharedRef >& OutAdvancedCategories ) { for( FCategoryMap::TConstIterator It(CategoryMap); It; ++It ) { TSharedRef DetailCategory = It.Value().ToSharedRef(); const bool bCategoryHiddenByClass = GetDetailsView().IsCategoryHiddenByClass( DetailCategory->GetCategoryName() ); if( !bCategoryHiddenByClass ) { DetailCategory->GenerateLayout(); if( DetailCategory->ContainsOnlyAdvanced() ) { OutAdvancedCategories.Add( DetailCategory ); } else { OutSimpleCategories.Add( DetailCategory ); } } } } void FDetailLayoutBuilderImpl::GenerateDetailLayout() { AllRootTreeNodes.Empty(); // Sort by the order in which categories were edited struct FCompareFDetailCategoryImpl { FORCEINLINE bool operator()( TSharedPtr A, TSharedPtr B ) const { return A->GetSortOrder() < B->GetSortOrder(); } }; // Merge the two category lists and sort them based on priority FCategoryMap AllCategories = CustomCategoryMap; AllCategories.Append( DefaultCategoryMap ); TArray< TSharedRef > SimpleCategories; TArray< TSharedRef > AdvancedOnlyCategories; BuildCategories( CustomCategoryMap, SimpleCategories, AdvancedOnlyCategories ); BuildCategories( DefaultCategoryMap, SimpleCategories, AdvancedOnlyCategories ); SimpleCategories.Sort( FCompareFDetailCategoryImpl() ); AdvancedOnlyCategories.Sort( FCompareFDetailCategoryImpl() ); /** Merge the two category lists in sorted order */ for( int32 CategoryIndex = 0; CategoryIndex < SimpleCategories.Num(); ++CategoryIndex ) { AllRootTreeNodes.Add( SimpleCategories[CategoryIndex] ); } for( int32 CategoryIndex = 0; CategoryIndex < AdvancedOnlyCategories.Num(); ++CategoryIndex ) { AllRootTreeNodes.Add( AdvancedOnlyCategories[CategoryIndex] ); } } void FDetailLayoutBuilderImpl::FilterDetailLayout( const FDetailFilter& InFilter ) { CurrentFilter = InFilter; FilteredRootTreeNodes.Empty(); for( int32 RootNodeIndex = 0; RootNodeIndex < AllRootTreeNodes.Num(); ++RootNodeIndex ) { TSharedRef& RootNode = AllRootTreeNodes[RootNodeIndex]; // No parent const bool bParentVisibleDueToFiltering = false; RootNode->FilterNode( InFilter ); if( RootNode->GetVisibility() == ENodeVisibility::Visible ) { FilteredRootTreeNodes.Add( RootNode ); DetailsView.RequestItemExpanded( RootNode, RootNode->ShouldBeExpanded() ); } } } void FDetailLayoutBuilderImpl::SetCurrentCustomizationClass( UClass* CurrentClass, FName VariableName ) { CurrentCustomizationClass = CurrentClass; CurrentCustomizationVariableName = VariableName; } TSharedPtr FDetailLayoutBuilderImpl::GetPropertyNode( const FName PropertyName, const UClass* ClassOutermost, FName InstanceName ) const { TSharedPtr PropertyNode = GetPropertyNodeInternal( PropertyName, ClassOutermost, InstanceName ); return PropertyNode; } /** * Parses a path node string into a property and index The string should be in the format "Property[Index]" for arrays or "Property" for non arrays * * @param OutProperty The property name parsed from the string * @param OutIndex The index of the property in an array (INDEX_NONE if not found) */ static void GetPropertyAndIndex( const FString& PathNode, FString& OutProperty, int32& OutIndex ) { OutIndex = INDEX_NONE; FString FoundIndexStr; // Split the text into the property (left of the brackets) and index Right of the open bracket if( PathNode.Split( TEXT("["), &OutProperty, &FoundIndexStr, ESearchCase::IgnoreCase, ESearchDir::FromEnd ) ) { // Convert the index string into a number OutIndex = FCString::Atoi( *FoundIndexStr ); } else { // No index was found, the path node is just the property OutProperty = PathNode; } } /** * Finds a child property node from the provided parent node (does not recurse into grandchildren) * * @param InParentNode The parent node to locate the child from * @param PropertyName The property name to find * @param Index The index of the property if its in an array */ static TSharedPtr FindChildPropertyNode( FPropertyNode& InParentNode, const FString& PropertyName, int32 Index ) { TSharedPtr FoundNode(NULL); // search each child for a property with the provided name for( int32 ChildIndex = 0; ChildIndex < InParentNode.GetNumChildNodes(); ++ChildIndex ) { TSharedPtr& ChildNode = InParentNode.GetChildNode(ChildIndex); UProperty* Property = ChildNode->GetProperty(); if( Property && Property->GetFName() == *PropertyName ) { FoundNode = ChildNode; break; } } // Find the array element. if( FoundNode.IsValid() && Index != INDEX_NONE ) { // The found node is the top array so get its child which is the actual node FoundNode = FoundNode->GetChildNode( Index ); } return FoundNode; } TSharedPtr FDetailLayoutBuilderImpl::GetPropertyNode( TSharedPtr PropertyHandle ) const { TSharedPtr PropertyNode = NULL; if( PropertyHandle->IsValidHandle() ) { PropertyNode = StaticCastSharedPtr(PropertyHandle)->GetPropertyNode(); } return PropertyNode; } /** Contains the location of a property, by path Supported format: instance_name'outer.outer.value[optional_index] Instance name is needed if multiple UProperties of the same type exist (such as two identical structs, the instance name is one of the struct variable names) Items in arrays are indexed by [] Example setup */ TSharedPtr FDetailLayoutBuilderImpl::GetPropertyNodeInternal( const FName PropertyPath, const UClass* ClassOutermost, FName InstanceName ) const { FName PropertyName; TArray PathList; PropertyPath.ToString().ParseIntoArray( PathList, TEXT("."), true ); if( PathList.Num() == 1 ) { PropertyName = FName( *PathList[0] ); } // The class to find properties in defaults to the class currently being customized FName ClassName = CurrentCustomizationClass ? CurrentCustomizationClass->GetFName() : NAME_None; if( ClassOutermost != NULL ) { // The requested a different class ClassName = ClassOutermost->GetFName(); } // Find the outer variable name. This only matters if there are multiple instances of the same property FName OuterVariableName = CurrentCustomizationVariableName; if( InstanceName != NAME_None ) { OuterVariableName = InstanceName; } // If this fails there are no properties associated with the class name provided FClassInstanceToPropertyMap* ClassInstanceToPropertyMapPtr = PropertyMap.Find( ClassName ); if( ClassInstanceToPropertyMapPtr ) { FClassInstanceToPropertyMap& ClassInstanceToPropertyMap = *ClassInstanceToPropertyMapPtr; if( OuterVariableName == NAME_None && ClassInstanceToPropertyMap.Num() == 1 ) { // If the outer variable name still wasnt specified and there is only one instance, just use that auto FirstKey = ClassInstanceToPropertyMap.CreateIterator(); OuterVariableName = FirstKey.Key(); } FPropertyNodeMap* PropertyNodeMapPtr = ClassInstanceToPropertyMap.Find( OuterVariableName ); if( PropertyNodeMapPtr ) { FPropertyNodeMap& PropertyNodeMap = *PropertyNodeMapPtr; // Check for property name fast path first if( PropertyName != NAME_None ) { // The property name was ambiguous or not found if this fails. If ambiguous, it means there are multiple data same typed data structures(components or structs) in the class which // causes multiple properties by the same name to exist. These properties must be found via the path method. return PropertyNodeMap.PropertyNameToNode.FindRef( PropertyName ); } else { // We need to search through the tree for a property with the given path TSharedPtr PropertyNode; // Path should be in the format A[optional_index].B.C if( PathList.Num() ) { // Get the base property and index FString Property; int32 Index; GetPropertyAndIndex( PathList[0], Property, Index ); // Get the parent most property node which is the one in the map. Its children need to be searched PropertyNode = PropertyNodeMap.PropertyNameToNode.FindRef( FName( *Property ) ); if( PropertyNode.IsValid() ) { if( Index != INDEX_NONE ) { // The parent is the actual array, its children are array elements PropertyNode = PropertyNode->GetChildNode( Index ); } // Search any additional paths for the child for( int32 PathIndex = 1; PathIndex < PathList.Num(); ++PathIndex ) { GetPropertyAndIndex( PathList[PathIndex], Property, Index ); PropertyNode = FindChildPropertyNode( *PropertyNode, Property, Index ); } } } return PropertyNode; } } } return NULL; } TSharedRef FDetailLayoutBuilderImpl::GetPropertyHandle( TSharedPtr PropertyNodePtr ) { TSharedPtr PropertyHandle; if( PropertyNodePtr.IsValid() ) { TSharedRef PropertyNode = PropertyNodePtr.ToSharedRef(); FNotifyHook* NotifyHook = GetPropertyUtilities()->GetNotifyHook(); PropertyHandle = PropertyEditorHelpers::GetPropertyHandle( PropertyNode, NotifyHook, PropertyDetailsUtilities ); } else { // Invalid handle PropertyHandle = MakeShareable( new FPropertyHandleBase( NULL, NULL, NULL) ); } return PropertyHandle.ToSharedRef(); } void FDetailLayoutBuilderImpl::AddExternalRootPropertyNode( TSharedRef InExternalRootNode ) { DetailsView.AddExternalRootPropertyNode( InExternalRootNode ); } TSharedPtr FDetailLayoutBuilderImpl::GetThumbnailPool() const { return DetailsView.GetThumbnailPool(); } bool FDetailLayoutBuilderImpl::IsPropertyVisible( TSharedRef PropertyHandle ) const { if( PropertyHandle->IsValidHandle() ) { FPropertyAndParent PropertyAndParent(*PropertyHandle->GetProperty(), PropertyHandle->GetParentHandle().IsValid() ? PropertyHandle->GetParentHandle()->GetProperty() : nullptr ); return IsPropertyVisible(PropertyAndParent); } return false; } bool FDetailLayoutBuilderImpl::IsPropertyVisible( const struct FPropertyAndParent& PropertyAndParent ) const { return DetailsView.IsPropertyVisible( PropertyAndParent ); } const IDetailsView& FDetailLayoutBuilderImpl::GetDetailsView() const { return DetailsView; } void FDetailLayoutBuilderImpl::GetObjectsBeingCustomized( TArray< TWeakObjectPtr >& OutObjects ) const { OutObjects.Empty(); // The class to find properties in defaults to the class currently being customized FName ClassName = CurrentCustomizationClass ? CurrentCustomizationClass->GetFName() : NAME_None; if( ClassName != NAME_None && CurrentCustomizationVariableName != NAME_None ) { // If this fails there are no properties associated with the class name provided FClassInstanceToPropertyMap* ClassInstanceToPropertyMapPtr = PropertyMap.Find( ClassName ); if( ClassInstanceToPropertyMapPtr ) { FClassInstanceToPropertyMap& ClassInstanceToPropertyMap = *ClassInstanceToPropertyMapPtr; FPropertyNodeMap* PropertyNodeMapPtr = ClassInstanceToPropertyMap.Find( CurrentCustomizationVariableName ); if( PropertyNodeMapPtr ) { FPropertyNodeMap& PropertyNodeMap = *PropertyNodeMapPtr; FObjectPropertyNode* ParentObjectProperty = PropertyNodeMap.ParentProperty ? PropertyNodeMap.ParentProperty->AsObjectNode() : NULL; if (ParentObjectProperty) { for (int32 ObjectIndex = 0; ObjectIndex < ParentObjectProperty->GetNumObjects(); ++ObjectIndex) { OutObjects.Add(ParentObjectProperty->GetUObject(ObjectIndex)); } } } } } else { OutObjects = DetailsView.GetSelectedObjects(); } } const TSharedRef< IPropertyUtilities >& FDetailLayoutBuilderImpl::GetPropertyUtilities() const { return PropertyDetailsUtilities; } void FDetailLayoutBuilderImpl::SetCustomProperty( const TSharedPtr& PropertyNode ) { PropertyNode->SetNodeFlags( EPropertyNodeFlags::IsCustomized, true ); } void FDetailLayoutBuilderImpl::Tick( float DeltaTime ) { for( auto It = TickableNodes.CreateIterator(); It; ++It ) { (*It)->Tick( DeltaTime ); } } void FDetailLayoutBuilderImpl::AddTickableNode( IDetailTreeNode& TickableNode ) { TickableNodes.Add( &TickableNode ); } void FDetailLayoutBuilderImpl::RemoveTickableNode( IDetailTreeNode& TickableNode ) { TickableNodes.Remove( &TickableNode ); } void FDetailLayoutBuilderImpl::SaveExpansionState( const FString& NodePath, bool bIsExpanded ) { DetailsView.SaveCustomExpansionState( NodePath, bIsExpanded ); } bool FDetailLayoutBuilderImpl::GetSavedExpansionState( const FString& NodePath ) const { return DetailsView.GetCustomSavedExpansionState( NodePath ); }