Details Panel Customization

The UE4 details panel is used all over the editor for displaying properties of actors, blueprint defaults, settings and the like. It's possible to customize which properties are displayed and how they appear, which can really help to make things easier and more intuitive for designers.

You can pretty much do whatever you want within a customization, the API is extensive and you can add whatever Slate widgets you like. This article will focus on the basics of registering a customization and accessing categories and properties.

Setup

The setup requirements are unfortunately a bit of a hassle, especially if you don't already have an editor module in your project. While the customization system is very flexible, it's a little annoying to have to go through this process when you only want to make a very minor customization.

The first step is to add an editor module to your project or plugin. The process for that is outside the scope of this article, but there's a good explanation of it on the UE4 wiki. For details customization, make sure you have the "Slate", "SlateCore", "UnrealEd" and "PropertyEditor" modules added to your dependency module names list in your editor module's .build.cs file.

Next up, add a header and cpp file to this module for the customization class. The header is very straightforward, just derive from IDetailCustomization and override the CustomizeDetails method. Note that you'll want one of these classes for each individual UCLASS that you intend to customize.

// MyCustomization.h
#pragma once

#include "IDetailCustomization.h"

class FMyCustomization: public IDetailCustomization
{
public:
    // IDetailCustomization interface
    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
    //

    static TSharedRef< IDetailCustomization > MakeInstance();
};

The MakeInstance static method is just a convenience helper.

In your cpp file, the boilerplate implementation looks as follows:

// MyCustomization.cpp
#include "MyEditorModulePCH.h"
#include "MyCustomization.h"
#include "MyClass.h" // The class we're customizing
#include "PropertyEditing.h"

#define LOCTEXT_NAMESPACE "MyEditorModule"

TSharedRef< IDetailCustomization > FMyCustomization::MakeInstance()
{
    return MakeShareable(new FMyCustomization);
}

void FMyCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
    // This is where the core of the customization code will go.
}

#undef LOCTEXT_NAMESPACE

It's also necessary to register your customization, to tell UE4 which UCLASS should use the customization. In theory this can be done anywhere, but generally you will want to add the following to your editor module's StartupModule method:

// Register detail customizations
{
    auto& PropertyModule = FModuleManager::LoadModuleChecked< FPropertyEditorModule >("PropertyEditor");

    // Register our customization to be used by a class 'UMyClass' or 'AMyClass'. Note the prefix must be dropped.
    PropertyModule.RegisterCustomClassLayout(
        "MyClass",
        FOnGetDetailCustomizationInstance::CreateStatic(&FMyCustomization::MakeInstance)
        );

    PropertyModule.NotifyCustomizationModuleChanged();
}

Note you should also #include "PropertyEditorModule.h" at the top of the file.
Ideally, unregister the customization when you're done with it - usually in the ShutdownModule method.

if(FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
    auto& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

    PropertyModule.UnregisterCustomClassLayout("MyClass");
}

Customizing

Okay, with that done, let's return to the CustomizeDetails method of your customization class. This is where you add the code that will change how your class's properties are displayed. We'll assume that the class we've customized is defined as follows:

UCLASS()
class UMyClass: public UObject
{
    GENERATED_BODY()
    
public:
    UPROPERTY(EditAnywhere, Category = "Cat A")
    FString BaseString;

    UPROPERTY(EditAnywhere, Category = "Cat A")
    int32 Count;
    
    UPROPERTY(VisibleAnywhere, Category = "Cat B")
    TArray< FString > GeneratedList;
};

Property Handles

The customization framework is built on the IPropertyHandle type, which represents a particular UPROPERTY on your class, but can potentially be linked to the value of that property on multiple instances of your class (for example, if you are viewing properties of selected actors in a level and have more than one actor selected).

Retrieve a property handle as follows:

TSharedRef< IPropertyHandle > Prop = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UMyClass, BaseString));

The GetProperty method takes an FName identifying the property. GET_MEMBER_NAME_CHECKED is not required, but is a useful macro that will protect against possible mistakes when naming properties with strings, by letting you know at compile time if no property exists with the name given.

You should generally check the resulting handle for validity (IPropertyHandle::IsValidHandle()) before using it. Properties can be unavailable in some circumstances, for example as a result of metadata specifiers used in the UPROPERTY macro.

IPropertyHandle encapsulates a lot of functionality. You can use it to get and set the underlying value, register OnChanged handlers, and access child handles in the case of structs and arrays.

Categories

Properties are divided into categories as specified by the Category metadata. You are free to reorganize property categories within a customization, to hide existing categories and to create new ones. You access a category builder by calling:

IDetailCategoryBuilder& Cat = DetailBuilder.EditCategory(TEXT("CatName"));

Note that for UCLASS customizations, any properties that you don't specifically modify or hide will be added to the details panel below those that you do customize, within their default category.

Basic Operations

// Note hiding is done using the DetailBuilder, not the CategoryBuilder
DetailBuilder.HideProperty(Prop);

// Hide an entire category
DetailBuilder.HideCategory(TEXT("CatName"));

// Add a property to a category (properties will be shown in the order you add them)
Cat.AddProperty(Prop);

Dynamic State

Using Slate attributes, it's easy to have property state such as visibility and enabled state determined dynamically. The AddProperty method returns a reference to an IDetailPropertyRow interface that provides this functionality. Unfortunately sometimes you're forced to write some rather ugly boilerplate...

auto OnGetPropVisibility = [] { return /* Query some state here */ ? EVisibility::Visible : EVisibility::Collapsed; };
auto PropVisibilityAttr = TAttribute< EVisibility >::Create(TAttribute< EVisibility >::FGetter::CreateLambda(OnGetPropVisibility));

Cat.AddProperty(Prop).Visibility(PropVisibilityAttr);

With the above code, the engine will call back into the OnGetPropVisibility lambda each frame to determine whether the property should be shown or not.

Accessing the Customized Object(s)

Some simple customizations may not require direct access to the objects being customized, but often it's useful. Remember that the details panel may be displaying multiple objects at any one time.

TArray< TWeakObjectPtr< UObject > > Objects;
DetailBuilder.GetObjectsBeingCustomized(Objects);

In practice, I've found that for most non-trivial customizations, it makes sense to restrict the customization to a single object at a time. The following check (along with the above two lines of code) at the top of your CustomizeDetails override can be used to fall back onto the default details display whenever multiple objects are being viewed.

if (Objects.Num() != 1)
{
    return;
}

You'll then generally want to cast the single object to the class type for which you've registered your customization. Using a TWeakObjectPtr here is useful for being able to safely capture the object in any callback lambdas you may create.

TWeakObjectPtr< UMyClass > MyObject = Cast< UMyClass >(Objects[0].Get());

If you're not a fan of lambdas, you may want to store it in a member variable on your customization class. If you do so, be sure to store it as a TWeakObjectPtr and check for validity when accessing it in event handlers.

Custom Rows

If you're writing a customization, you probably want to do more than just rearrange properties. Custom rows let you add arbitrary Slate widgets to the details panel. Here's an example based on the class definition given above.

/*
Showing a warning message about invalid property values.
(Note that customizations can also be used to enforce validation on user-entered property values).
*/
auto OnGetWarningVisibility = [MyObject]
{
    return MyObject.IsValid() && MyObject->BaseString.IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed;
};
auto WarningVisibilityAttr = TAttribute< EVisibility >::Create(TAttribute< EVisibility >::FGetter::CreateLambda(OnGetWarningVisibility));

Cat.AddCustomRow(LOCTEXT("MyWarningRowFilterString", "Search Filter Keywords"))
.Visibility(WarningVisibilityAttr)
.WholeRowContent()
    [
        SNew(STextBlock)
        .Text(LOCTEXT("MyWarningTest", "BaseString should not be empty!"))
    ];

/*
Displaying a button that triggers editor-time processing.
*/
auto OnRegenerate = [MyObject]
{
    if(MyObject.IsValid())
    {
        MyObject->GeneratedList.Empty();
        for(int32 i = 0; i < MyObject->Count; ++i)
        {
            MyObject->GeneratedList.Add(MyObject->BaseString + TEXT("_") + (MyObject->Count + 1));
        }
    }
    
    return FReply::Handled();
};

Cat.AddCustomRow(LOCTEXT("MyButtonRowFilterString", "Search Filter Keywords"))
.WholeRowContent()
    [
        SNew(SButton)
        .Text(LOCTEXT("RegenerateBtnText", "Regenerate List"))
        .OnClicked_Lambda(OnRegenerate)
    ];

Refreshing

For most cases, using dynamic updates as above is the easiest. Once in a while though, you may just want to force the details panel to refresh and call your CustomizeDetails method again from scratch. You'll generally want to do this from within a handler that you've added to one of your custom controls, or perhaps a property changed event.

DetailBuilder.ForceRefreshDetails();

The above will require you to either capture the DetailBuilder reference in your lambda, or if using method delegates rather than lambdas, store a pointer to it inside your customization class.

Further Info

That turned out to be rather long, and yet it really only touched the surface. For more details, I'd recommend checking out the various interface types mentioned above in the API reference, starting here. There's also the offical docs page here, which has some great info but is unfortunately rather out of date when it comes to the code.

Also, this wiki article covers some aspects of customization that I haven't, for example USTRUCT customization.

Any questions, just post in the comments.