Details Panel Customization

The Details panel is now fully customizable. You can rearrange properties via a simple system, or you can fully customize them using Slate UI Framework . You can also add other UI to the details using Slate syntax.

Setup instructions

  1. Create a class for customizing properties in. This must inherit from ILayoutDetails.

    • You implement one function: void LayoutDetails( IDetailLayoutBuilder& ).

    • The purpose of this class is to encapsulate customization for a classes properties. One instance of the class will be created for each Details panel that requires it.

  2. Set up a delegate that will be called when the Details panel recognizes properties for a specific class.

    • The sole purpose of this delegate is to create an instance of your customization class for a specific UObject that has properties. Remember that there often are multiple details views up at any point and each instance of the details view gets its own customization class instance. This allows you to store per detail instance data on your layout class.

    • Example (more examples are located in DetailCustomizations.cpp):

      FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
      PropertyModule.RegisterCustomPropertyLayout( ABrush::StaticClass(), FOnGetDetailLayoutInstance::CreateRaw( &FBrushDetails::MakeInstance ) );
      
      ...
      static TSharedRef<ILayoutDetails> FBrushDetails::MakeInstance()
      {
          return MakeShareable( new FBrushDetails );
      }
  3. Implement your customization in the LayoutDetails function of the class you made in step 1.

    • If this is an engine class, you should add your customization class (if it does not already exist) to the DetailCustomizations module. This module can be recompiled and reloaded without restarting the editor, making it useful for fast tweaking of properties.

    • Bind your delegate in FDetailCustomizationsModule.StartupModule and unbind it in FDetailCustomizationsModule.ShutdownModule.

    • Game specific classes should use a game specific module.

    • See examples in this document and the DetailCustomizations module (such as PrimitiveComponentDetails.cpp and StaticMeshComponentDetails.cpp).

Customizing

You handle all customization inside the LayoutDetails function of your customization class. This function accepts an IDetailLayoutBuilder, which is your interface to properties and how you pass back customization widgets.

The primary function of IDetailLayoutBuilder is to create categories where properties and other details reside. There are some other minor functions on this class which are self explanatory (most of them you will not need). The documentation for those can be found in DetailLayoutBuilder.h.

The first step in customizing is to edit a category:

virtual void LayoutDetails( IDetailLayoutBuilder& DetailBuilder ) override
{
    // Edit the lighting category
    IDetailCategory& LightingCategory = DetailBuilder.EditCategory("Lighting", TEXT("OptionalLocalizedDisplayName") );
}

The EditCategory function takes an FName for the category where properties will reside and an optional localized display name. If the display name is specified, it will override any existing display name. The category name does not have to be the same category name specified in the UPROPERTY macro, although it will reuse the UPROPERTY category if the names are the same. The macro category name is used as the default category if the property is not customized and in the tree view.

EditCategory returns an IDetailCategoryBuilder& which is what you use to add properties to a category. There are a two ways to do this:

Multibox Style Layout

An easy example using the LightingCategory created above:

// Add a property to the category.  The first param is the name of the property and the second is an optional display name override.
LightingCategory.AddProperty("bCastStaticShadow", TEXT("Static") );
LightingCategory.AddProperty("bCastDynamicShadow", TEXT("Dynamic") );
LightingCategory.AddProperty("bCastVolumetricTranslucentShadow", TEXT("Volumetric") );

This is the most basic example. It adds 3 properties stacked vertically and overrides their display names. (The text in the examples is not localized to save space, but should always be localized in general practice.)

Note that customized properties and categories always appear above non-customized ones. You can use this simple syntax to reorganize important properties which may otherwise be buried.

The result:

multibox_layout_vertical.png

A slightly more advanced example (located in PrimitiveComponentDetails.cpp):

// Create a non-collapsible group with the display name "Shadows" which is only visible if the CastShadow property is enabled. All properties below this call will appear in the same group until EndGroup or another BeginGroup is called
LightingCategory.BeginGroup( TEXT("Shadows"), GroupImageName, "CastShadow" );
    // Begin a new line.  All properties below this call will be added to the same line until EndLine() or another BeginLine() is called
    LightingCategory.BeginLine();
            // Add properties using their default look
            LightingCategory.AddProperty("bCastStaticShadow", TEXT("Static") );
            LightingCategory.AddProperty("bCastDynamicShadow", TEXT("Dynamic") );
            LightingCategory.AddProperty("bCastVolumetricTranslucentShadow", TEXT("Volumetric") );
    LightingCategory.BeginLine();
            LightingCategory.AddProperty("bCastInsetShadow", TEXT("Inset") );
            LightingCategory.AddProperty("bCastHiddenShadow", TEXT("Hidden") );
            LightingCategory.AddProperty("bCastShadowAsTwoSided", TEXT("Two Sided") );
LightingCategory.EndGroup();
  • BeginGroup is used to create a new group of properties. It takes a name to display for the group, an optional image name (Slate brush name) to display next to the name, and an optional edit condition property which if false will hide the entire group from view so its properties cannot be changed. These edit conditions are the same as the UPROPERTY macro style edit conditions except they operate on a group of properties instead of just one. More things like this could be added in the future!

  • AddProperty adds a property using its default look. It usually only needs one parameter which is the property name. More complicated properties such as properties inside structs need additional information. See the Advanced Tips section or the documentation in DetailCategoryBuilder.h if you need this.

  • BeginLine creates a new line of properties. By default, all properties added via AddProperty are created on a new line. BeginLine ensures all properties added until the next BeginLine or EndLine are on the same line.

The result:

multibox_layout_horizontal.png

Notes About Multibox Style Layout

  • It is not very powerful, but more features will be added as needed. It is currently designed to be for quick reorganization.

  • The slate layout will require more advanced access to properties, specifically property handles, especially if you need to customize their look.

Property Handles

The two main functions of property handles are to read and write the value of a property and to identify the property to Slate customization widgets. How properties are accessed by the details view/property tree is somewhat complicated, so property handles hide all that away and handles undo/redo, pre/post edit change, package dirtying, world switching etc., for you.

To get a property handle, you must ask the IDetailCategory where you want to customize it for one. You do this by calling IDetailCategory::GetProperty. Usually you simply pass in the name of the property as follows:

IDetailCategoryBuilder& LightingCategory = DetailBuilder.EditCategory( "Lighting" );
// Get a handle to the "bOverrideLightmapRes" property
TSharedPtr<IPropertyHandle> OverrideLightmapRes = LightingCategory.GetProperty( "bOverrideLightmapRes" );

Now you have a handle to the bool property bOverrideLightmapRes.

From here, you can read and write the value of that property and/or pass it to a slate widget for customization.

Useful property handle functions (For the complete documented list see PropertyHandle.h):

Function Description
IPropertyHandle::SetValue(const ValueType& InValue) and IPropertyHandle::GetValue(ValueType& OutValue) Writes and reads property values. These are overloaded for many built in types (including vectors and rotators). For complicated types like user structs, will need to get a child handle. See the advanced section at the end of this document.
ResetToDefault() Resets a property to its default.
IsValidHandle() Returns whether or not you have a valid property handle.
AsArray() Array property values are special. See the advanced section at the end of this document.

Other notes:

  • The handle returned from GetProperty may be invalid if the property could not be found or is not going to appear in the details view. Check IsValidHandle() to be sure. Calling functions on invalid handles will not crash.

  • You should not store property handles outside of your layout class unless they are weak pointers. Internally the data they access is a weak pointer so it will not crash if you try to set or get a value on an invalid property but you have a reference to a useless object if you store it and it is not cleaned up.

  • If you try to read / write access a value type for an unsupported property (e.g., a float for a String property ), it will fail but no data will be corrupted.

Handling failure cases when accessing values.

Remember that detail views can view multiple objects at once and it is not uncommon for users to select hundreds of Actors at once. In cases like these, you will undoubtedly have multiple values for one property. GetValue and SetValue return an FPropertyAccess::Result to help you determine whether or not accessing a value was successful. FPropertyAccess::MultipleValues will be a common return value.

/**
* Potential results from accessing the values of properties                   
    */
namespace FPropertyAccess
{
        enum Result
        {
            /** Multiple values were found so the value could not be read */
            MultipleValues,
            /** Failed to set or get the value (Property is no longer available, is not a compatible type, or is edit const are the likely cases) */
            Fail,
            /** Successfully set or got the value */
            Success,
        };
}

If you are customizing a low level typed property like an int or float, you must handle the multiple values case somehow.

    INT MyInteger;
    // Get the value of the property
    FPropertyAccess::Result MyResult = MyIntHandle->GetValue(MyInteger);

If MyResult is FPropertyAccess::MultipleValues, MyInteger will not be set. Sending that to a widget that displays it will show garbage value and initializing it before hand is not much better because it is still not the correct value. How this is handled is up to the customizer. For numeric types, using SNumericEntryBox is recommended which allows you to optionally return no value in its value attribute. It will then display a label you provide instead. See SNumericEntryBox.h.

Slate Layout

Slate layout allows you to completely customize the look and arrangement of properties. You pass your layout back to a category via IDetailCategoryBuilder::AddWidget which takes an arbitrary widget from Slate. To assist you in this, there are some customization widgets available to you:

SProperty

This is the customization widget. You use this widget to customize a property and/or embed the property in other slate declarative syntax. You create an SProperty using SNew but you also provide a handle to the property so it knows what to build. The handle is a non-optional parameter to SNew: SNew( SProperty, HandleToTheProperty )

Example:

// Edit the lighting category
IDetailCategoryBuilder& LightingCategory = DetailBuilder.EditCategory( "Lighting" );

// Get a handle to the bOverrideLightmapRes property
TSharedPtr<IPropertyHandle> OverrideLightmapRes = LightingCategory.GetProperty( "bOverrideLightmapRes" );

LightingCategory.AddWidget()
[
    SNew( SHorizontalBox )
    + SHorizontalBox::Slot()
    [
        // Make a new SProperty
        SNew( SProperty, EnableOverrideLightmapRes )
    ]
    + SHorizontalBox::Slot()
    .Padding( 4.0f, 0.0f )
    .MaxWidth( 50 )
    [
        SNew( SProperty, LightingCategory.GetProperty("OverriddenLightMapRes") )
        .NamePlacement( EPropertyNamePlacement::Hidden ) // Hide the name
    ]
];

The result:

sproperty.png

SProperty by default will generate a widget for the property. There are some basic customization attributes on SProperty for customizing the default look (such as the name). If you want to customize a property, you use the CustomWidget slot. Once you use the CustomWidget slot, SProperty no longer knows anything about how to set and get the value since you have made a custom widget. You need to use your property handle to get and set the value.

Example:

  // Customizes the OverridenLightMapRes property to display some text and a spinbox 
  TSharedPtr<IPropertyHandle> LightMapResValue = LightingCategory.GetProperty("OverriddenLightMapRes")
  SNew( SProperty, LightMapResValue )
  .CustomWidget()
  [
        SNew( SHorizontalBox )
        + SHorizontalBox::Slot()
        .VAlign( VAlign_Center )
        .Padding( 2.0f )
        [
              SNew( STextBlock )
              .Text( TEXT("Lightmap Res") )
        ]
        + SHorizontalBox::Slot()
        [
              SNew( SSpinBox )
              .MinSliderValue( 0 )
              .MaxSliderValue( 1024 )
              .OnValueCommitted( &SetValueOnProperty )  
              .Value( &GetValueFromProperty
        ]
  ]
  ...
  FLOAT GetValueFromProperty()
  {
        // Using the property handle created above, get its value and send it to the spinbox
        INT Value; // note lightmap res is an integer so it must be accessed as such.
        LightMapResValue.GetValue( Value );
        // Note HANDLE FAILURE CASES
        return Value;
  }

  void SetValueOnProperty( FLOAT NewValue )
  {
        // Using the property handle, set its value
        LightMapResValue.SetValue( NewValue )
  }
SProperty Notes
  • SProperty always displays reset to default even if you make a custom widget. There is an argument on the SProperty which toggles this behavior. For example, if you have a row of properties, you may want to tell it not to display a reset to default for each one but make a big menu at the end. See SResetToDefaultMenu below.

  • If you pass an invalid handle to SProperty, it will simply not show up.

In addition to SProperty there are some other customization widgets that can be used.

SAssetProperty

SAssetProperty is an SProperty that displays a thumbnail of the asset as well as an entry box for changing the asset. You can change the size of the thumbnail as well. You can use this on UObject properties that have renderable thumbnails. If you use this on other types, it will not display anything.

sassetproperty.png

SFilterableDetail

SFilterableDetail is a widget that does not draw anything but filters everything in its content slot when a user types in the search box of the details view. This widget is useful for non-property based details. SProperty is already filtered so you do not need to set up an SFilterableDetail for those unless you want to group their filtering.

// Create a widget which will filter out everything in the content slot when "Create Blocking Volume" is not matched with a user's search term
// Note: The second parameter is the localized search term that matches the filter and the third parameter is the category where this filter should reside
SNew( SFilterableDetail, NSLOCTEXT("StaticMeshDetails", "BlockingVolumeMenu", "Create Blocking Volume"), &StaticMeshCategory )
.Content()
[
      // Create blocking volume menu
      SNew( SComboButton )
      .ButtonContent()
      [
            SNew( STextBlock )
            .Text( NSLOCTEXT("StaticMeshDetails", "BlockingVolumeMenu", "Create Blocking Volume") ) 
            .Font( IDetailLayoutBuilder::GetDetailFont() )
      ]
      .MenuContent()
      [
            BlockingVolumeBuilder.MakeWidget()
      ]
]

SResetToDefaultMenu

SResetToDefaultMenu is a menu which displays the yellow reset to default arrow. By default, SProperty adds a reset to default menu, but sometimes it makes sense to group more than one property into the same menu. (e.g Vector properties). You can add SProperty widgets to an SResetToDefaultMenu to handle this for you. Simply call AddProperty on SResetToDefaultMenu and then place the menu in any declarative syntax!

SArrayProperty

This widget allows you to customize an array of properties. You create one just like SProperty and also hook up a delegate which is called each time a widget for an array element is needed.

Example:

void FMeshComponentDetails::LayoutDetails( IDetailLayoutBuilder& DetailLayout )
{
      IDetailCategoryBuilder& DetailCategory = DetailLayout.EditCategory("Rendering");
      TSharedRef<IPropertyHandle> MaterialProperty = DetailCategory.GetProperty( "Materials" );

      DetailCategory.AddWidget()
      [
            SNew( SArrayProperty, MaterialProperty )
            // This delegate is called for each array element to generate a widget for it
            .OnGenerateArrayElementWidget( this, &FMeshComponentDetails::OnGenerateElementForMaterials )
      ];
}
...
/**
* Generates a widget for a materials element
* 
 * @param ElementProperty     A handle to the array element we need to generate
* @param ElementIndex        The index of the element we are generating
*/
TSharedRef<SWidget> FMeshComponentDetails::OnGenerateElementForMaterials( TSharedRef<IPropertyHandle> ElementProperty, INT ElementIndex )
{
      return 
            SNew( SAssetProperty, ElementProperty )
            .ThumbnailSize( FIntPoint(32,32) );
}

The result:

sarrayproperty.png

Customization Dos and Dont's

  • Do check error cases when customizing properties and reading/writing values. Remember that details views can often be viewing multiple objects at once where each object has different values. Customized properties should be able to handle the common case of multiple values.

  • Do store any data about the selection on your customization class. Some non-property details will need the selected Actors for a customization. You can get the selected Actors from IDetailLayoutBuilder. You can store this selection set or anything selection sensitive on your customization class. It is guaranteed to be around while the selection remains the same in its details view.

  • Do not use FActorIterator, FSelectedActorIterator, or GEditor->GetSelectedActorIterator. Remember that the Details panel can be locked and these things operate on global selection sets or lists of Actors which is not the same as the selected Actors in a Details panel if it is locked! Using these will access different data. You can get the list of selected Actors valid for you from IDetailLayoutBuilder.

  • Do not hold a strong reference to your layout class or property handles (you should not need to anyway). Remember that details views (especially level editor ones) can change at any time based on user selection so any references you have to layout classes can easily become invalid. The shared pointers to layout classes are checked for uniqueness when customizing details to prevent this from happening.

Advanced Tips

Accessing complicated properties.

A complicated property is defined as anything that cannot be resolved with just a property name. Usually this involves properties inside structs.

There are two ways to access complicated properties:

  • Functions that return a property handle or add a property to a category take optional parameters for resolving properties.

    Example:

    TSharedPtr<IPropertyHandle> IDetailCategoryBuilder::GetProperty(  FName PropertyPath, UClass* ClassOutermost , FName InstanceName) 
    Parameter Description
    Path The path to the property. Can be just a name of the property or a path in the format outer.outer.value[optional_index_for_static_arrays].
    ClassOutermost Optional outer class if accessing a property outside of the current class being customized.
    InstanceName Optional instance name if multiple UProperty's of the same type exist (Such as two identical structs, the instance name is one of the struct variable names).

    Examples:

    struct MyStruct
    { 
        INT StaticArray[3];
        FLOAT FloatVar;
    }
    
    class MyActor
    { 
        MyStruct Struct1;
        MyStruct Struct2;
        FLOAT MyFloat
    }
    • To access StaticArray at index 2 from Struct2 in MyActor, your path would be "MyStruct.StaticArray[2]" and your instance name is "Struct2".

    • To access the same StaticArray outside of MyActor customization functions you would do the same as above but ClassOutermost would be MyActor::StaticClass().

    • To access MyFloat in MyActor you can just pass in "MyFloat" because the name of the property is unambiguous.

  • If you have a property handle, you can get a child property handle by name from it:

    TSharedPtr<IPropertyHandle> IPropertyHandle::GetChildHandle( FName ChildName )
    Parameter Description
    ChildName The property name of the child. This will recurse until found. Paths are not supported and children of arrays cannot be accessed in this way.

Accessing Arrays

You can access arrays via IPropertyHandle::AsArray. If the property handle is an array, this will return an IPropertyHandleArray which has functions for adding, removing, inserting, duplicating, and getting the number of elements of an array.

Hiding properties

You can hide properties altogether by calling IDetailLayoutBuilder::HideProperty. It takes either a name/path or a property handle.