Customizing Actor's Details Panel and Accessing its CDO Components

Recently I've been writing a new functionality for my plugin. To facilitate its use, I want some buttons on the Details panel where these buttons will modify the blueprint class's CDO data. The first step, accessing the blueprint class's specific component, however is not as simple as I thought. You cannot use the FindComponentByClass function or its equivalents to access the component of the blueprint class at the CDO state. The second step, customizing the Details panel, requires me to create a new editor module and implements the IDetailCustomization interface in a very notoriously clumsy way. This post introduces how to customize actor's Details panel and access its CDO components.

Step 1: Create A New Class Implementing IDetailCustomization

According to the official documentation, the IDetailCustomization interface can be used for any class that lays out details for its properties or functions.

Start by creaing a new class implementing this interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once

#include "IDetailCustomization.h"

class FMyDetail : public IDetailCustomization
{
public:
// Used to create a singleton instance.
static TSharedRef<IDetailCustomization> MakeInstance();
// Used to specifically implement your customization.
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;

private:
UActorSequenceComponent* GetActorSequenceComponent(AMyClass* Object);
};

Then in its .cpp file you should implement the two declared functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "MyDetail.h"

#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "DetailWidgetRow.h"
#include "Widgets/Input/SButton.h"
#include "Engine/SCS_Node.h"

#define LOCTEXT_NAMESPACE "MyDetail"

TSharedRef<IDetailCustomization> FKeyframeExtensionDetail::MakeInstance()
{
return MakeShared<FKeyframeExtensionDetail>();
}

void FKeyframeExtensionDetail::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
TArray< TWeakObjectPtr<UObject> > Objects;
DetailBuilder.GetObjectsBeingCustomized(Objects);
if (Objects.Num() != 1)
{
return;
}

AMyClass* Object = (AMyClass*)Objects[0].Get();
IDetailCategoryBuilder& KeyframeCategory = DetailBuilder.EditCategory("ECamera Actions", FText(), ECategoryPriority::Important);

KeyframeCategory.AddCustomRow(FText::GetEmpty())
.NameContent()
[
SNew(STextBlock)
.Text(FText::FromString("Keyframe Extension"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(0)
.AutoWidth()
[
SNew(SButton)
.ToolTipText(FText::FromString("Tootip here."))
.Content()
[
SNew(STextBlock)
.Text(FText::FromString("Toss Sequence"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.IsEnabled_Lambda([this, Object]()->bool
{
return true;
})
.OnClicked_Lambda([this, Object]()
{
if (Object)
{
// Access the CDO component. Will explain later.
UActorSequenceComponent* ActorSequenceComponent = GetActorSequenceComponent(Object);

// If this object is currently in blueprint (CDO)
if (Object->IsTemplate())
{
DoSomething(ActorSequenceComponent);
}
// If this object is currently in level (instance).
else
{
DoSomething();
}
}
return (FReply::Handled());
})
]
];
}

#undef LOCTEXT_NAMESPACE

It is quite easy to understand the code. What I am basically doing is first to get all customized objects and get the one I'm interested in. Then I specify the actor's category I want to customize. Last under this category I add a new row that includes a button with showing text "Toss Sequence".

The most core part is the action this button executes after you click it, defined in .OnClicked_Lambda. In my case I want to access the UActorSequenceComponent component, so I define a new function GetActorSequenceComponent and it returns a UActorSequenceComponent pointer. This component is exactly the one contained in the class's CDO. The next four lines first check whether this object is in blueprint or in level, and then execute different functions.

Step 2: Accessing Component in CDO

Function GetActorSequenceComponent implements accessing a specific component in CDO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
UActorSequenceComponent* FMyDetail::GetActorSequenceComponent(AMyClass* Object)
{
UActorSequenceComponent* ActorSequenceComponent = nullptr;
if (Object)
{
UBlueprintGeneratedClass* CurrentBPClass = Cast<UBlueprintGeneratedClass>(Object->GetClass());
if (CurrentBPClass && CurrentBPClass->SimpleConstructionScript)
{
for (const USCS_Node* Component : CurrentBPClass->SimpleConstructionScript->GetAllNodes())
{
UActorSequenceComponent* TemporaryComponent = Cast<UActorSequenceComponent>(Component->ComponentTemplate);
if (TemporaryComponent != nullptr)
{
ActorSequenceComponent = TemporaryComponent;
break;
}
}
}
}
return ActorSequenceComponent;
}

I don't want to dive into the details of each class and function mentioned above. To put it simply, this function first gets the blueprint generated class and then the SimpleConstructionScript member which is a graph describing the components to instantiate. Within SCS, we can visit each node and check if it is the component of interest. Last, we return the pointer.

Step 3: Registering the Customized Details Panel in .Build.cs

As the last step, you should register the custom class layout in the .Build.cs file. Basically this should be within the editor module rather than the runtime counterpart.

Register is quite simple. In your .Build.cs file, add several lines in StartupModule() and ShutdownModule().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FEasyCameraEditorModule::StartupModule()
{
auto& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomClassLayout(
// Drop all prefix such as U and A.
"MyClass",
FOnGetDetailCustomizationInstance::CreateStatic(&FMyDetail::MakeInstance)
);
PropertyModule.NotifyCustomizationModuleChanged();
}

void FEasyCameraEditorModule::ShutdownModule()
{
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
auto& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomClassLayout("MyClass");
}
}

Result

Compile and open Unreal editor. In the blueprint Details panel, you can see three new buttons have been added (I add two more buttons). Each button, when clicked, successfully executes its behaviour as exptected.