Inspecting types with custom Details panels

2021/01 02 14:01

By default, UObject-derived UAssets open in the generic property editor. It looks as follows:

However, at times, you may wish for custom widgets so that you can edit the properties on your class. To facilitate this, Unreal supports Details Customization, which is the focus of this recipe.

How to do it…

  1. From the Chapter_10Editor folder, create two new files called MyCustomAssetDetailsCustomization.h and MyCustomAssetDetailsCustomization.cpp.
  2. Return to your project file and update your Visual Studio project. Once finished, open up the project in Visual Studio.
  3. Add the following #pragma and #includes to the header (MyCustomAssetDetailsCustomization.h):
#pragma once

#include "MyCustomAsset.h" #include "DetailLayoutBuilder.h" #include "IDetailCustomization.h" #include "IPropertyTypeCustomization.h"
  1. Define our customization class, as follows:
class FMyCustomAssetDetailsCustomization : public IDetailCustomization
{

public:
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;

void ColorPicked(FLinearColor SelectedColor);

static TSharedRef<IDetailCustomization> MakeInstance()
{
return MakeShareable(new FMyCustomAssetDetailsCustomization);
}

TWeakObjectPtr<class UMyCustomAsset> MyAsset;
};
  1. Below that, define the following additional class:
class FMyCustomAssetPropertyDetails : public IPropertyTypeCustomization
{
public:
void ColorPicked(FLinearColor SelectedColor);
static TSharedRef<IPropertyTypeCustomization> MakeInstance()
{
return MakeShareable(new FMyCustomAssetPropertyDetails);
}

UMyCustomAsset* MyAsset;
virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

};
  1. In the implementation file, add the following includes at the top of the file:
#include "MyCustomAssetDetailsCustomization.h" 
#include "Chapter_10Editor.h"
#include "IDetailsView.h"
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "SColorPicker.h"
#include "SBoxPanel.h"
#include "DetailWidgetRow.h"
  1. Afterwards, create an implementation for CustomizeDetails:
void FMyCustomAssetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
const TArray< TWeakObjectPtr<UObject> >& SelectedObjects = DetailBuilder.GetDetailsView()->GetSelectedObjects();

for (int32 ObjectIndex = 0; !MyAsset.IsValid() && ObjectIndex < SelectedObjects.Num(); ++ObjectIndex)
{
const TWeakObjectPtr<UObject>& CurrentObject = SelectedObjects[ObjectIndex];
if (CurrentObject.IsValid())
{
MyAsset = Cast<UMyCustomAsset>(CurrentObject.Get());
}
}

DetailBuilder.EditCategory("CustomCategory", FText::GetEmpty(), ECategoryPriority::Important)
.AddCustomRow(FText::GetEmpty())
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.VAlign(VAlign_Center)
[
SNew(SColorPicker)
.OnColorCommitted(this, &FMyCustomAssetDetailsCustomization::ColorPicked)
]
];
}
  1. Also, create a definition for ColorPicked:
void FMyCustomAssetDetailsCustomization::ColorPicked(FLinearColor SelectedColor)
{
if (MyAsset.IsValid())
{
MyAsset.Get()->ColorName = SelectedColor.ToFColor(false).ToHex();
}
}
  1. Beneath all of the scripts in MyCustomAssetDetailsCustomization.cpp, add the following code: 
void FMyCustomAssetPropertyDetails::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
}

void FMyCustomAssetPropertyDetails::CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
UObject* PropertyValue = nullptr;
auto GetValueResult = PropertyHandle->GetValue(PropertyValue);

HeaderRow.NameContent()
[
PropertyHandle->CreatePropertyNameWidget()
];
HeaderRow.ValueContent()
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.VAlign(VAlign_Center)
[
SNew(SColorPicker)
.OnColorCommitted(this, &FMyCustomAssetPropertyDetails::ColorPicked)
]
];
}

void FMyCustomAssetPropertyDetails::ColorPicked(FLinearColor SelectedColor)
{
if (MyAsset)
{
MyAsset->ColorName = SelectedColor.ToFColor(false).ToHex();
}
}
  1. In our editor module source file (Chapter_10Editor.cpp), add the following to your #includes in the Chapter_10Editor.cpp file:
#include "PropertyEditorModule.h" 
#include "MyCustomAssetDetailsCustomization.h"
#include "MyCustomAssetPinFactory.h"
  1. Add the following to the implementation of StartupModule:
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomClassLayout(UMyCustomAsset::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FMyCustomAssetDetailsCustomization::MakeInstance));
PropertyModule.RegisterCustomPropertyTypeLayout(UMyCustomAsset::StaticClass()->GetFName(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FMyCustomAssetPropertyDetails::MakeInstance));
  1. Add the following to ShutdownModule:
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomClassLayout(UMyCustomAsset::StaticClass()->GetFName());
  1. Compile your code and launch the editor. Create a new instance of MyCustomAsset via the Content Browser.
  2. Double-click on it to verify that the default editor that comes up now shows your custom layout:

How it works…

Details Customization is performed through the IDetailCustomization interface, which developers can inherit when defining a class that customizes the way assets of a certain class are displayed.

The main function that IDetailCustomization uses to allow for this process to occur is as follows:

virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;

Within our implementation of this function, we use methods on DetailBuilder that are passed in as parameters to get an array of all selected objects. The loop then scans those to ensure that at least one selected object is of the correct type.

Customizing the representation of a class is done by calling methods on the DetailBuilder object. We create a new category for our details view by using the EditCategory function.

The first parameter of the EditCategory function is the name of the category we are going to manipulate.

The second parameter is optional, and contains a potentially localized display name for the category.

The third parameter is the priority of the category. A higher priority means it is displayed further up the list.

EditCategory returns a reference to the category itself as CategoryBuilder, allowing us to chain additional method calls onto an invocation of EditCategory.

As a result, we call AddCustomRow() on CategoryBuilder, which adds a new key-value pair to be displayed in the category.

Using the Slate syntax, we then specify that the row will contain a Vertical Box with a single center-aligned slot.

Inside the slot, we create a color picker control and bind its OnColorCommitted delegate to our local ColorPicked event handler.

Of course, this requires us to define and implement ColourPicked. It has the following signature:

void FMyCustomAssetDetailsCustomization::ColorPicked(FLinearColor SelectedColor)

Inside the implementation of ColorPicked, we check to see if one of our selected assets was of the correct type, because, if at least one selected asset was correct, then MyAsset will be populated with a valid value.

Assuming we have a valid asset, we set the ColorName property to the hex string value corresponding to the color that was selected by the user.