How do I show a component's details in the actor details?

I’m encountering a challenging problem.

I am trying to create a designer friendly camera system. Currently, the goal is to create a system where a developer can just add a component to a camera that allows them to create custom camera sequences that trigger when the player enters specified volumes. In order to clearly illustrate the link between a sequence and a trigger every sequence has a colour that is automatically applied to every trigger associated with that sequence.

The problem is that when a component is added in a blueprint it does not show up in the actor details when in a level and when selecting a specific component on an actor every volume loses it’s colour and becomes grey so you can’t see the colour you are changing the triggers to. Here is what I want:

Here is what I’ve done so far:

Firstly, I have a struct called sequence data that I have contained inside an array on my component:

// The sequence data relating to 1 camera sequence a cine camera sequence does
USTRUCT(BlueprintType)
struct FSequenceData
{
public:
	GENERATED_BODY()

	FSequenceData()
	{
		SequenceColour = FColor::MakeRandomColor();
		BlendFunction = EViewTargetBlendFunction::VTBlend_Linear;
		bLockOutgoing = false;
	}

	static FSequenceData Empty;

	// The colour signifying the sequence. It will be applied to volumes
	UPROPERTY(EditAnywhere, meta = (DisplayPriority = 0))
	FColor SequenceColour = FColor();

	//UPROPERTY(EditAnywhere, meta = (DisplayPriority = 2))
	//TObjectPtr<UAkAudioEvent> SequenceAudioEvent = nullptr;

	// The array of trigger volumes that activate this sequence
	UPROPERTY(EditAnywhere, meta = (DisplayPriority = 3))
	TArray< TObjectPtr<AVolume>> TriggerVolumes;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	float BlendTime = 0.f;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	TEnumAsByte<EViewTargetBlendFunction> BlendFunction;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	float BlendExp = 0.f;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	uint8 bLockOutgoing : 1;

protected:

	UPROPERTY(VisibleAnywhere, meta = (DisplayPriority = 1))
	int32 SequenceID = -1;

	UPROPERTY()
	int32 SequenceAudioEventID = -1;

public:

	
	FORCEINLINE int32 GetSequenceID() const { return SequenceID; }

	FORCEINLINE void SetSequenceID(int32 InSequenceID) { SequenceID = InSequenceID; }
};

Inside the actor component’s protected section:

	// The array of sequences this camera has
	UPROPERTY(EditAnywhere)
	TArray<FSequenceData> Sequences;

Then, so I can respond to changes to the struct from the editor I create a struct inheriting from IPropertyTypeCustomization that simply goes through the properties inside of the sequence data struct and binds delegates to their property change and child property change events in order to update the volumes’ colour in real time:

.h file

#if WITH_EDITOR

#include "IPropertyTypeCustomization.h"
#include "Components/Cinematics/BaseSequenceComponent.h"

class AVolume;

class FSequenceDataCustomisation : public IPropertyTypeCustomization
{
public:	
	// Sets default values for this actor's properties
	FSequenceDataCustomisation();
	~FSequenceDataCustomisation();

	static TSharedRef<IPropertyTypeCustomization> MakeInstance();

	/**
	 * Called when the header of the property (the row in the details panel where the property is shown)
	 * If nothing is added to the row, the header is not displayed
	 *
	 * @param PropertyHandle			Handle to the property being customized
	 * @param HeaderRow					A row that widgets can be added to
	 * @param StructCustomizationUtils	Utilities for customization
	 */
	virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

	/**
	 * Called when the children of the property should be customized or extra rows added
	 *
	 * @param PropertyHandle			Handle to the property being customized
	 * @param StructBuilder				A builder for adding children
	 * @param StructCustomizationUtils	Utilities for customization
	 */
	virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

	void UpdateTriggers(TSharedPtr<FColor> SequenceColour, TSharedPtr<TArray<AVolume*>> Triggers);

private:

	FSequenceData SequenceData;

	TArray<TSharedRef<FSimpleDelegate>> Delegates;
};

#endif // WITH_EDITOR

.cpp file:

#include "DetailsCustomisation/SequenceDataCustomisation.h"

#if WITH_EDITOR

#include "Editor/PropertyEditor/Public/IDetailChildrenBuilder.h"
#include "Editor/PropertyEditor/Public/PropertyHandle.h"
#include "Editor/PropertyEditor/Public/PropertyCustomizationHelpers.h"

FSequenceDataCustomisation::FSequenceDataCustomisation()
{
}

FSequenceDataCustomisation::~FSequenceDataCustomisation()
{
	for (TSharedRef<FSimpleDelegate> Delegate : Delegates)
	{
		Delegate.Get().Unbind();
	}

	Delegates.Empty();
}

TSharedRef<IPropertyTypeCustomization> FSequenceDataCustomisation::MakeInstance()
{
	return MakeShareable(new FSequenceDataCustomisation);
}

void FSequenceDataCustomisation::CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
	
}

void FSequenceDataCustomisation::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
	IPropertyHandle& PropertyHandleReference = PropertyHandle.Get();

	uint32 NumChildren = 0;
	PropertyHandle->GetNumChildren(NumChildren);

	TArray<TSharedRef<IPropertyHandle>> BindProperties;

	TSharedPtr<FColor> SequenceColourPtr = nullptr;
	TSharedPtr<TArray<AVolume*>> TriggerVolumesPtr = nullptr;
	for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex)
	{
		TSharedRef<IPropertyHandle> ChildPropertyHandle = PropertyHandle->GetChildHandle(ChildIndex).ToSharedRef();

		ChildBuilder.AddProperty(ChildPropertyHandle);

		FProperty* ChildProperty = ChildPropertyHandle.Get().GetProperty();

		FName ChildPropertyName = (ChildProperty != nullptr) ? ChildProperty->GetFName() : NAME_None;

		bool bIsColour = ChildPropertyName == GET_MEMBER_NAME_CHECKED(FSequenceData, SequenceColour);
		bool bIsVolumes = ChildPropertyName == GET_MEMBER_NAME_CHECKED(FSequenceData, TriggerVolumes);
		if (bIsColour || bIsVolumes)
		{
			BindProperties.Add(ChildPropertyHandle);

			void* Data = nullptr;
			PropertyHandle.Get().GetValueData(Data);

			if (bIsColour)
			{
				SequenceColourPtr = MakeShareable<FColor>(ChildProperty->ContainerPtrToValuePtr<FColor>(Data));
				
			}
			
			if (bIsVolumes)
			{
				TriggerVolumesPtr = MakeShareable<TArray<AVolume*>>(ChildProperty->ContainerPtrToValuePtr<TArray<AVolume*>>(Data));
			}
		}
	}

	for (TSharedRef<IPropertyHandle> BindProperty : BindProperties)
	{
		TSharedRef<FSimpleDelegate> Delegate = MakeShared<FSimpleDelegate>();
		Delegate.Get().BindSP(this, &FSequenceDataCustomisation::UpdateTriggers, SequenceColourPtr, TriggerVolumesPtr);
		BindProperty.Get().SetOnPropertyValueChanged(Delegate.Get());
		BindProperty.Get().SetOnChildPropertyValueChanged(Delegate.Get());

		Delegates.Add(Delegate);
	}

	//PropertyHandle.Get();
}

void FSequenceDataCustomisation::UpdateTriggers(TSharedPtr<FColor, ESPMode::ThreadSafe> SequenceColour, TSharedPtr<TArray<AVolume*>, ESPMode::ThreadSafe> Triggers)
{
	if (!SequenceColour.IsValid() || !Triggers.IsValid()) return;

	// Loop through the trigger volumes
	for (AVolume* TriggerVolume : *Triggers)
	{
		// Check if the trigger volume is not valid
		if (!IsValid(TriggerVolume)) continue;

		// Set the trigger volume brush colour to the one for the sequence
		TriggerVolume->BrushColor = *SequenceColour;

		// Mark render state dirty so the colour of the volume changes immediately 
		TriggerVolume->MarkComponentsRenderStateDirty();
	}
}

#endif

Afterwards, I override the OnRegister function inside of my actor component to grab the outer object’s class then use that to find the actor component as a FProperty where I change the property flags in an attempt to get it to display like I want:

void UBaseSequenceComponent::OnRegister()
{
	Super::OnRegister();

	if (UObject* OuterObject = GetOuter(); UClass * OuterClass = OuterObject->GetClass())
	{
		UE_LOG(LogTemp, Warning, TEXT("The FName is: %s"), *GetFName().ToString());

		FString NameAsString = GetFName().ToString();

		NameAsString.RemoveSpacesInline();

		FName SpacelessFName = FName(*NameAsString);

		FProperty* Property = OuterClass->FindPropertyByName(SpacelessFName);

		if (Property != nullptr)
		{
			UE_LOG(LogTemp, Warning, TEXT("Here we are!"));
			Property->SetPropertyFlags(
				EPropertyFlags::CPF_BlueprintVisible | 
				EPropertyFlags::CPF_ExportObject | 
				EPropertyFlags::CPF_Edit | 
				EPropertyFlags::CPF_EditConst | 
				EPropertyFlags::CPF_ExposeOnSpawn);
		}
	}
}

However that simply results in this:

I know it has something to do with the blueprint add component execution path because regardless of whether it is a blueprint or C++ component it ends up looking like this. I know there’s something the create default subobject path does that I need to replicate, I just don’t know what.

Any help would be extremely appreciated.


	UPROPERTY(Instanced,VisibleAnywhere, Category = "My Component", BlueprintReadWrite)
	TObjectPtr<UMyActorComponent> Component;

inside of the component’s header example uproperties (the pipe | gives sub categories)

	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="My Component|Settings", meta=(AllowPrivateAccess=true))
	float TraversalSpeed  = 100.0f;

	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="My Component|Settings", meta=(AllowPrivateAccess=true))
	float JumpHeight  = 5.0f;

	

Make sure to create the component in the CDO
in my case

Component = CreateDefaultSubobject<UMyActorComponent>(TEXT("My Component"));

Thank you for the reply. The system I’m creating will be camera agnostic as in it may be a cinematic camera or just a regular camera I can’t create the camera as a C++ class and then add the component there. The user may be blueprint only and therefore only add the component in blueprints. Is there anyway to get the C++ added component look in editor through adding a component inside of a blueprint?

Not that I am aware of. The only way for pure blueprint projects to work with c++ is to make a custom plugin and pack it, then add it to a blueprint project.

You can just use a ACameraActor as the class

UPROPERTY(BlueprintReadWrite, EditAnywhere)
TObjectPtr<ACameraActor> Camera;

The cinematic camera is derived from the camera actor class so it will work with the uproperty.

I think in the end I have just shifted towards using an Editor Utility widget to display the component’s details. I appreciate your suggestions though