DetailCustomization | Only show specific properties | ISinglePropertyView?

Hey there,

I’m currently struggling with setting up a DetailView of my custom Actors.
I setup a custom DetailCustomization which I registered and use an “IDetailsView” in my custom widget to display the selected Actors data.

That works, but it also shows ALL of the standard Actor properties.

What would be the best way to only show specific propeties but still use the DetailCustomization and teh IDetailsView?

You probably have to use IDetailLayoutBuilder for that, making your own Slate widgets.
Then you can set IPropertyHandle->MarkHiddenByCustomization() on the ones you wish to hide.

For now I set all Categories despite a few and mine to be hidden (by hand). Which is a huge list, as I don’t know how to retrieve the categories all at once to loop over them.
If category X has a Property Y and I hide the Property, would the category also be removed?

EDIT: So I tried these things now, but they also don’t work correctly.



for (TFieldIterator<UProperty> PropIt(Class); PropIt; ++PropIt)
{
	UProperty* Property = *PropIt;
	FString CategoryName = Property->GetMetaData(FName("Category"));

	UE_LOG(LogTemp, Warning, TEXT("Property Name: %s | Category: %s"), *Property->GetNameCPP(), *CategoryName);
        // Only display Categories that have "Exposed" in them
	if (!CategoryName.Contains(FString("Exposed")))
	{
                // Test 1: Let the DetailBuilder hide the Properties and Categories?
		DetailBuilder.HideCategory(FName(*CategoryName));
		DetailBuilder.HideProperty(FName(*Property->GetNameCPP()));

                // Test2: Use an IPropertyHandle and make the Property as hidden?
		TSharedRef<IPropertyHandle> PropHandle = DetailBuilder.GetProperty(FName(*Property->GetNameCPP()));
		PropHandle->MarkHiddenByCustomization();
	}
}


EDIT2: Above code is also succesfully only not calling the inside of the “if(!CategoryName…” for my own test variable. So it filters correctly. Just the hiding isn’t correct.

If you can the code inside ‘CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)’ it should work , really strange maybe check if the Class is correct or if the Category contains “Exposed”

Well, I got this code here to throw me a list of some proporties. The rest seem to be not created correctly.



void STrafficEditorModeWidget::OnEditorSelectionChanged(class UObject* Object)
{
	auto Selection = Cast<USelection>(Object);

	if (Object == GEditor->GetSelectedActors())
	{
		UObject* LastActor = Selection->GetSelectedObject(GEditor->GetSelectedActorCount() - 1);
		if (Cast<AStreetVertex>(LastActor))
		{
			if (PropertyList.IsValid())
			{
				PropertyList->ClearChildren();

				FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

				for (TFieldIterator<UProperty> PropIt(AStreetVertex::StaticClass()); PropIt; ++PropIt)
				{
					UProperty* Property = *PropIt;
					FString CategoryName = Property->GetMetaData(FName("Category"));

					UE_LOG(LogTemp, Warning, TEXT("Property Name: %s | Category: %s"), *Property->GetNameCPP(), *CategoryName);

					TSharedPtr<ISinglePropertyView> PropertyView = PropertyModule.CreateSingleProperty(LastActor, Property->GetFName(), FSinglePropertyParams());

					if (PropertyView.IsValid())
					{
						PropertyList->AddSlot()
							
								PropertyView->AsShared()
							];
					}
				}
			}
		}
	}
}


This is the list I get:

There are obviously more properties in an Actor, as the UE_LOG line prints me this:



LogTemp:Warning: Property Name: SceneComponent | Category: Components
LogTemp:Warning: Property Name: StaticMeshComponent | Category: Components
LogTemp:Warning: Property Name: ConnectedVectices | Category: Exposed
LogTemp:Warning: Property Name: PrimaryActorTick | Category: Tick
LogTemp:Warning: Property Name: CustomTimeDilation | Category: Misc
LogTemp:Warning: Property Name: bHidden | Category: Rendering
LogTemp:Warning: Property Name: bNetTemporary | Category: 
LogTemp:Warning: Property Name: bNetStartup | Category: 
LogTemp:Warning: Property Name: bOnlyRelevantToOwner | Category: Replication
LogTemp:Warning: Property Name: bAlwaysRelevant | Category: Replication
LogTemp:Warning: Property Name: bReplicateMovement | Category: Replication
LogTemp:Warning: Property Name: bTearOff | Category: 
LogTemp:Warning: Property Name: bExchangedRoles | Category: 
LogTemp:Warning: Property Name: bPendingNetUpdate | Category: 
LogTemp:Warning: Property Name: bNetLoadOnClient | Category: Replication
LogTemp:Warning: Property Name: bNetUseOwnerRelevancy | Category: Replication
LogTemp:Warning: Property Name: bBlockInput | Category: Input
LogTemp:Warning: Property Name: bAllowTickBeforeBeginPlay | Category: Tick
LogTemp:Warning: Property Name: bActorEnableCollision | Category: 
LogTemp:Warning: Property Name: bReplicates | Category: Replication
LogTemp:Warning: Property Name: NetDriverName | Category: 
LogTemp:Warning: Property Name: RemoteRole | Category: 
LogTemp:Warning: Property Name: Owner | Category: 
LogTemp:Warning: Property Name: ReplicatedMovement | Category: Replication
LogTemp:Warning: Property Name: AttachmentReplication | Category: 
LogTemp:Warning: Property Name: Role | Category: 
LogTemp:Warning: Property Name: AutoReceiveInput | Category: Input
LogTemp:Warning: Property Name: InputPriority | Category: Input
LogTemp:Warning: Property Name: InputComponent | Category: 
LogTemp:Warning: Property Name: InputConsumeOption_DEPRECATED | Category: 
LogTemp:Warning: Property Name: NetCullDistanceSquared | Category: Replication
LogTemp:Warning: Property Name: NetTag | Category: 
LogTemp:Warning: Property Name: NetUpdateTime | Category: 
LogTemp:Warning: Property Name: NetUpdateFrequency | Category: Replication
LogTemp:Warning: Property Name: MinNetUpdateFrequency | Category: Replication
LogTemp:Warning: Property Name: NetPriority | Category: Replication
LogTemp:Warning: Property Name: LastNetUpdateTime | Category: 
LogTemp:Warning: Property Name: bAutoDestroyWhenFinished | Category: Actor
LogTemp:Warning: Property Name: bCanBeDamaged | Category: Actor
LogTemp:Warning: Property Name: bActorIsBeingDestroyed | Category: 
LogTemp:Warning: Property Name: bCollideWhenPlacing | Category: 
LogTemp:Warning: Property Name: bFindCameraComponentWhenViewTarget | Category: Actor
LogTemp:Warning: Property Name: bRelevantForNetworkReplays | Category: 
LogTemp:Warning: Property Name: bGenerateOverlapEventsDuringLevelStreaming | Category: Actor
LogTemp:Warning: Property Name: SpawnCollisionHandlingMethod | Category: Actor
LogTemp:Warning: Property Name: Instigator | Category: Actor
LogTemp:Warning: Property Name: Children | Category: 
LogTemp:Warning: Property Name: RootComponent | Category: 
LogTemp:Warning: Property Name: PivotOffset | Category: Actor
LogTemp:Warning: Property Name: ControllingMatineeActors | Category: 
LogTemp:Warning: Property Name: InitialLifeSpan | Category: Actor
LogTemp:Warning: Property Name: Layers | Category: 
LogTemp:Warning: Property Name: ParentComponentActor_DEPRECATED | Category: 
LogTemp:Warning: Property Name: ParentComponent | Category: 
LogTemp:Warning: Property Name: GroupActor | Category: 
LogTemp:Warning: Property Name: SpriteScale | Category: Rendering
LogTemp:Warning: Property Name: ActorLabel | Category: 
LogTemp:Warning: Property Name: FolderPath | Category: 
LogTemp:Warning: Property Name: bActorLabelEditable | Category: 
LogTemp:Warning: Property Name: bHiddenEd | Category: 
LogTemp:Warning: Property Name: bEditable | Category: 
LogTemp:Warning: Property Name: bListedInSceneOutliner | Category: 
LogTemp:Warning: Property Name: bHiddenEdLayer | Category: 
LogTemp:Warning: Property Name: bHiddenEdTemporary | Category: 
LogTemp:Warning: Property Name: bHiddenEdLevel | Category: 
LogTemp:Warning: Property Name: bLockLocation | Category: 
LogTemp:Warning: Property Name: bAllowReceiveTickEventOnDedicatedServer | Category: 
LogTemp:Warning: Property Name: bActorSeamlessTraveled | Category: 
LogTemp:Warning: Property Name: bIgnoresOriginShifting | Category: Actor
LogTemp:Warning: Property Name: bEnableAutoLODGeneration | Category: Actor
LogTemp:Warning: Property Name: Tags | Category: Actor
LogTemp:Warning: Property Name: HiddenEditorViews | Category: 
LogTemp:Warning: Property Name: OnTakeAnyDamage | Category: Game|Damage
LogTemp:Warning: Property Name: OnTakePointDamage | Category: Game|Damage
LogTemp:Warning: Property Name: OnActorBeginOverlap | Category: Collision
LogTemp:Warning: Property Name: OnActorEndOverlap | Category: Collision
LogTemp:Warning: Property Name: OnBeginCursorOver | Category: Input|Mouse Input
LogTemp:Warning: Property Name: OnEndCursorOver | Category: Input|Mouse Input
LogTemp:Warning: Property Name: OnClicked | Category: Input|Mouse Input
LogTemp:Warning: Property Name: OnReleased | Category: Input|Mouse Input
LogTemp:Warning: Property Name: OnInputTouchBegin | Category: Input|Touch Input
LogTemp:Warning: Property Name: OnInputTouchEnd | Category: Input|Touch Input
LogTemp:Warning: Property Name: OnInputTouchEnter | Category: Input|Touch Input
LogTemp:Warning: Property Name: OnInputTouchLeave | Category: Input|Touch Input
LogTemp:Warning: Property Name: OnActorHit | Category: Collision
LogTemp:Warning: Property Name: OnDestroyed | Category: Game
LogTemp:Warning: Property Name: OnEndPlay | Category: Game
LogTemp:Warning: Property Name: BlueprintCreatedComponents | Category: 
LogTemp:Warning: Property Name: InstanceComponents | Category:


LogTemp:Warning: Property Name: ConnectedVectices | Category: Exposed

Is the one I actually want (and more in the future) as this is the one I created in my AStreetVertex class. But that’s not even created in as a Widget, god knows why.

EDIT: SOOOOOOO. ISinglePropertyView filters Arrays and Structs. Fair enough, but would do I use then?



bIsAcceptableProperty = true;
// not an array property (dynamic or static)
bIsAcceptableProperty &= !( Property->IsA( UArrayProperty::StaticClass() ) || (Property->ArrayDim > 1 && ValueNode->GetArrayIndex() == INDEX_NONE) );
// not a struct property unless its a built in type like a vector
bIsAcceptableProperty &= ( !Property->IsA( UStructProperty::StaticClass() ) || PropertyEditorHelpers::IsBuiltInStructProperty( Property ) );


Came to a point where I solved it with multiple things involved.

  1. I use a DetailCustomization and spawn an IDetailView (while this whole thing also affects the standard details tab)
  2. I iterate over the properties of the object that the DetailView is viewing and call “DetailBuilder.HideProperty(PropHandle);” when the category of that property does not contain a specific word (“TrafficSystem” here)
  3. To not have empty categories and to get rid of categories that I can’t hide, I let the class hide these by itself with a Class Specifier “UCLASS(HideCategories=(“TrafficSystem|Components”, “Transform”))”
  4. All other categories are currently hidden through simply “hardcoding” “DetailBuilder.HideCategory(“Rendering”);” etc inside of the DetailCustomization

I have no idea if that’s optimal, but now I have this as a result:

For those who are interested into the code:



void FTrafficActorDetailCustomization::CustomizeDetails(IDetailLayoutBuilder & DetailBuilder)
{
	// Hardcoded hide all Categories we don't want
	DetailBuilder.HideCategory(FName("StaticMesh"));
	DetailBuilder.HideCategory(FName("Materials"));
	DetailBuilder.HideCategory(FName("Physics"));
	DetailBuilder.HideCategory(FName("Tags"));
	DetailBuilder.HideCategory(FName("Collision"));
	DetailBuilder.HideCategory(FName("Lighting"));
	DetailBuilder.HideCategory(FName("Activation"));
	DetailBuilder.HideCategory(FName("Variable"));
	DetailBuilder.HideCategory(FName("Rendering"));
	DetailBuilder.HideCategory(FName("Cooking"));
	DetailBuilder.HideCategory(FName("Tick"));
	DetailBuilder.HideCategory(FName("Replication"));
	DetailBuilder.HideCategory(FName("Input"));
	DetailBuilder.HideCategory(FName("Actor"));
	DetailBuilder.HideCategory(FName("Mobile"));
	DetailBuilder.HideCategory(FName("ComponentReplication"));

	TSet<UClass*> Classes;

        // Get all currently viewed/customized Objects
	TArray<TWeakObjectPtr<UObject>> ObjectsBeingCustomized;
	DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized);

        // Loop over the viewed objects and save their Class to use later
	for (auto WeakObject : ObjectsBeingCustomized)
	{
		if (UObject* Instance = WeakObject.Get())
		{
			Classes.Add(Instance->GetClass());
		}
	}

        // Loop over all currently view Classes
	for (UClass* Class : Classes)
	{
                // Loop over their Properties
		for (TFieldIterator<UProperty> PropIt(Class); PropIt; ++PropIt)
		{
			UProperty* Property = *PropIt;

                        // Create a Handle from the Property Name (needed?)
			TSharedRef<IPropertyHandle> PropHandle = DetailBuilder.GetProperty(Property->GetFName());
			if (PropHandle->IsValidHandle())
			{
                                // Get the Category from the Property through its MetaData (PropHandle needed?)
				FString Category = PropHandle->GetProperty()->GetMetaData("Category");

                                // If the Category contains our specific one, don't hide it!
				if (!Category.Contains("TrafficSystem"))
				{
					DetailBuilder.HideProperty(PropHandle);
				}
			}
		}
	}

	// Create a new Category for Commands
	IDetailCategoryBuilder& Category = DetailBuilder.EditCategory(TEXT("Commands"));
        
        // Loop over all viewed Classes
	for (UClass* Class : Classes)
	{
                // Loop over their Functions
		for (TFieldIterator<UFunction> FuncIt(Class); FuncIt; ++FuncIt)
		{
			UFunction* Function = *FuncIt;

                        // If we found an EXEC function, we create a new CustomRow entry to the Category with a Button in the ValueContent
			if (Function->HasAnyFunctionFlags(FUNC_Exec) && (Function->NumParms == 0))
			{
				const FString FunctionName = Function->GetName();
				const FText ButtonCaption = FText::FromString(FunctionName);
				const FString FilterString = FunctionName;

				Category.AddCustomRow(FText::FromString(FilterString))
					.NameContent()
					
						SNew(STextBlock)
						.Text(LOCTEXT("ButtonCaption", "Actor Function"))
					]
					.ValueContent()
					
						SNew(SButton)
						.Text(ButtonCaption)
						.OnClicked(FOnClicked::CreateStatic(&FTrafficActorDetailCustomization::ExecuteToolCommand, &DetailBuilder, Function))
					];
			}
		}
	}
}


Why the UCLASS stuff? Because “HideCategory” did not hide the Transform, nor did it hide nested categories. So “TrafficSystem|Components” was still visible, but empty.

You solved it , great !
I know that feeling when you get something to work , but you know that you are doing something wrong :slight_smile:

Yeah, still not sure if that’s the way to go. Would really like to just create "SExpandableArea"s and put the Properties in there myself.
But no idea what Widgets I would use for the Property rows as “ISinglePropertyView” only support values and not Structs and Arrays.
I couldn’t even find something to reate one for Arrays.

Looking into the rows of the an “IDetailView” shows a lot of stuff which looks like it needs to be created exactly how it’s done in the process of creating the DetailView.
So it’s not really supporting to just rip out the row widget and use that alone.

Means I’m either missing a cool widget or I need to recreate that stuff myself.

To hide transform category you should use


DetailBuilder.HideCategory(FName("TransformCommon"));

today u can do it like this.

TArray<FName> CATEGORY_NAMES;

DetailBuilder.GetCategoryNames(CATEGORY_NAMES);

for (auto CATEGORY_NAME : CATEGORY_NAMES)
DetailBuilder.HideCategory(CATEGORY_NAME);