Files
2025-05-18 13:04:45 +08:00

299 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "InterchangeTestFunctionLayout.h"
#include "Framework/Views/TableViewMetadata.h"
#include "InterchangeTestFunction.h"
#include "IDetailChildrenBuilder.h"
#include "IPropertyUtilities.h"
#include "DetailWidgetRow.h"
#include "DetailLayoutBuilder.h"
#include "Editor.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SComboBox.h"
#include "ScopedTransaction.h"
#define LOCTEXT_NAMESPACE "InterchangeTestFunctionLayout"
TSharedRef<IPropertyTypeCustomization> FInterchangeTestFunctionLayout::MakeInstance()
{
return MakeShared<FInterchangeTestFunctionLayout>();
}
FInterchangeTestFunctionLayout::FInterchangeTestFunctionLayout()
{
}
FInterchangeTestFunctionLayout::~FInterchangeTestFunctionLayout()
{
FEditorDelegates::PostUndoRedo.RemoveAll(this);
}
void FInterchangeTestFunctionLayout::CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
// Perform initialization here rather than in the constructor
// (adding SP delegates doesn't work if the object hasn't yet been assigned)
AssetClasses = FInterchangeTestFunction::GetAvailableAssetClasses();
FEditorDelegates::PostUndoRedo.AddSP(this, &FInterchangeTestFunctionLayout::RefreshLayout);
// Ensure that the details layout is refreshed when resetting the entire struct to default, so that custom data can be rebuilt
StructProperty = StructPropertyHandle;
StructProperty->SetOnPropertyResetToDefault(FSimpleDelegate::CreateSP(this, &FInterchangeTestFunctionLayout::RefreshLayout));
// Cache all the child properties we're interested in manipulating
AssetClassProperty = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FInterchangeTestFunction, AssetClass));
OptionalAssetNameProperty = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FInterchangeTestFunction, OptionalAssetName));
CheckFunctionProperty = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FInterchangeTestFunction, CheckFunction));
ParametersProperty = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FInterchangeTestFunction, Parameters));
PropertyUtilities = StructCustomizationUtils.GetPropertyUtilities();
// The FInterchangeTestFunction defines a specific test which will be carried out on the imported asset.
// The three important properties are:
// 1) AssetClass - the class of the asset object we are expecting, and which we will perform our test on.
// 2) TestFunction - a UFunction* specifying the function we will call to perform the test
// 3) Parameters - a list of specific parameter values to call the function with
//
// The Parameters property is a simple (name, valueString) map.
// However this is not a useful form for interacting with in a properties view, so we import the parameter values to binary here.
// Note that this binary blob is owned by, and only of interest to, this layout class.
FInterchangeTestFunction* InterchangeTestFunction = GetStruct();
GetFunctionsForClass(InterchangeTestFunction->AssetClass);
ParamData = InterchangeTestFunction->ImportParameters();
// The header of the struct customization contains the class and function combo boxes.
// These will determine which children should be populated underneath.
HeaderRow
.NameContent()
[
StructPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SComboBox<UClass*>)
.OptionsSource(&AssetClasses)
.OnGenerateWidget(this, &FInterchangeTestFunctionLayout::OnGenerateClassComboWidget)
.OnSelectionChanged(this, &FInterchangeTestFunctionLayout::OnClassComboSelectionChanged)
.InitiallySelectedItem(InterchangeTestFunction->AssetClass)
[
OnGenerateClassComboWidget(InterchangeTestFunction->AssetClass)
]
]
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SEditableTextBox)
.Font(IDetailLayoutBuilder::GetDetailFont())
.HintText(LOCTEXT("OptionalAssetName", "Optional Asset Name"))
.Text_Lambda([this] { FString Value; OptionalAssetNameProperty->GetValue(Value); return FText::FromString(Value); })
.OnTextCommitted_Lambda([this](const FText& Val, ETextCommit::Type TextCommitType) { OptionalAssetNameProperty->SetValue(Val.ToString()); })
.ToolTipText(LOCTEXT("OptionalAssetNameTooltip", "Fill out this field with the name of an imported asset which you wish to test, if there are multiple assets of the same type to choose from."))
]
]
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SComboBox<UFunction*>)
.OptionsSource(&Functions)
.OnGenerateWidget(this, &FInterchangeTestFunctionLayout::OnGenerateFunctionComboWidget)
.OnSelectionChanged(this, &FInterchangeTestFunctionLayout::OnFunctionComboSelectionChanged)
.InitiallySelectedItem(InterchangeTestFunction->CheckFunction)
[
OnGenerateFunctionComboWidget(InterchangeTestFunction->CheckFunction)
]
]
]
];
}
void FInterchangeTestFunctionLayout::GetFunctionsForClass(UClass* AssetClass)
{
// This populates an array with all the UFunction* available as tests for the given asset class
Functions = FInterchangeTestFunction::GetAvailableFunctions(AssetClass);
}
void FInterchangeTestFunctionLayout::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
// The children of the struct customization represent all the editable input parameters to bind to the test function
// This needs to be rebuilt every time a new function is selected.
FInterchangeTestFunction* InterchangeTestFunction = GetStruct();
// Iterate through the parameters of interest
for (FProperty* ParamProperty : InterchangeTestFunction->GetParameters())
{
FName ParamName = ParamProperty->GetFName();
IDetailPropertyRow* Row = StructBuilder.AddExternalStructureProperty(ParamData.ToSharedRef(), ParamName, FAddPropertyParams().ForceShowProperty());
TSharedPtr<IPropertyHandle> PropertyHandle = Row->GetPropertyHandle();
// Note: we need to hook both of these events.
// SetOnPropertyValueChanged is called when the property itself is modified.
// SetOnChildPropertyValueChanged is called when a child property is modified, e.g. the component of a vector.
PropertyHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FInterchangeTestFunctionLayout::OnParameterChanged, PropertyHandle));
PropertyHandle->SetOnChildPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FInterchangeTestFunctionLayout::OnParameterChanged, PropertyHandle));
// We have a notion of parameter defaults from the C++ function prototype.
// Hook a custom handler for Reset To Default for these parameter properties so that it does something useful.
Row->OverrideResetToDefault(FResetToDefaultOverride::Create(FSimpleDelegate::CreateSP(this, &FInterchangeTestFunctionLayout::OnParameterResetToDefault, PropertyHandle)));
}
}
FInterchangeTestFunction* FInterchangeTestFunctionLayout::GetStruct() const
{
// Get address of the FInterchangeTestFunction struct being viewed.
// We only ever expect the property handle to be linked to a single instance.
void* StructPtr = nullptr;
StructProperty->GetValueData(StructPtr);
return static_cast<FInterchangeTestFunction*>(StructPtr);
}
void FInterchangeTestFunctionLayout::OnParameterChanged(TSharedPtr<IPropertyHandle> PropertyHandle)
{
// When we change a function parameter property in the property editor, this will change the binary copy we maintain here.
// Here we need to propagate the change to the struct's (name, value) map.
FString Value;
PropertyHandle->GetValueAsFormattedString(Value);
FName PropertyName = PropertyHandle->GetProperty()->GetFName();
void* MapPtr = nullptr;
ParametersProperty->GetValueData(MapPtr);
// Note that we are not accessing the map property directly through the handle, so we need to call these various notifies ourselves.
// This allows transactions to work correctly.
ParametersProperty->NotifyPreChange();
static_cast<decltype(FInterchangeTestFunction::Parameters)*>(MapPtr)->Add(PropertyName, Value);
ParametersProperty->NotifyPostChange(EPropertyChangeType::ValueSet);
ParametersProperty->NotifyFinishedChangingProperties();
}
void FInterchangeTestFunctionLayout::OnParameterResetToDefault(TSharedPtr<IPropertyHandle> PropertyHandle)
{
// The property editor doesn't start a transaction if ResetToDefault is custom. We have to do it ourselves!
FScopedTransaction Transaction(FText::Format(LOCTEXT("ResetParameterToDefault", "Reset {0} to Default"), PropertyHandle->GetPropertyDisplayName()));
FName PropertyName = PropertyHandle->GetProperty()->GetFName();
// The way we implement ResetToDefault is by just removing the parameter from the map completely.
// (note the requirement to call the notifies manually)
// The default value will be set in the binary copy we maintain when the parameter is imported.
void* MapPtr = nullptr;
ParametersProperty->GetValueData(MapPtr);
ParametersProperty->NotifyPreChange();
static_cast<decltype(FInterchangeTestFunction::Parameters)*>(MapPtr)->Remove(PropertyName);
ParametersProperty->NotifyPostChange(EPropertyChangeType::ValueSet);
ParametersProperty->NotifyFinishedChangingProperties();
FInterchangeTestFunction* InterchangeTestFunction = GetStruct();
InterchangeTestFunction->ImportParameter(*ParamData, PropertyHandle->GetProperty());
}
void FInterchangeTestFunctionLayout::RefreshLayout()
{
if (PropertyUtilities.IsValid())
{
PropertyUtilities->ForceRefresh();
}
}
TSharedRef<SWidget> FInterchangeTestFunctionLayout::OnGenerateClassComboWidget(UClass* InClass)
{
return SNew(SBox)
[
SNew(STextBlock)
.Text(InClass ? InClass->GetDisplayNameText() : LOCTEXT("SelectAssetClass", "<Expected asset class>"))
.ToolTipText(InClass ? InClass->GetToolTipText() : FText())
.Font(IDetailLayoutBuilder::GetDetailFont())
];
}
void FInterchangeTestFunctionLayout::OnClassComboSelectionChanged(UClass* InSelectedItem, ESelectInfo::Type SelectInfo)
{
FInterchangeTestFunction* InterchangeTestFunction = GetStruct();
if (InterchangeTestFunction->AssetClass != InSelectedItem)
{
// When the asset class combo box of the header changes, this changes the entire state of the struct.
// The TestFunction needs to be reset (as the old one is most likely unsuitable), and so does the stored parameter map.
// So wrap this entire operation in a single high-level transaction, and force the layout to be refreshed afterwards.
FScopedTransaction Transaction(LOCTEXT("EditTestType", "Change test to perform"));
AssetClassProperty->SetValue(InSelectedItem);
UFunction* NullFunction = nullptr;
CheckFunctionProperty->SetValue(NullFunction);
ParametersProperty->AsMap()->Empty();
RefreshLayout();
}
}
TSharedRef<SWidget> FInterchangeTestFunctionLayout::OnGenerateFunctionComboWidget(UFunction* InFunction)
{
FInterchangeTestFunction* InterchangeTestFunction = GetStruct();
// If a test function doesn't have the correct signature, it will appear in the list, but greyed out and unselectable.
// This will act as a signal to the developer to fix the incorrect signature.
return SNew(SBox)
[
SNew(STextBlock)
.Text(InFunction ? InFunction->GetDisplayNameText() : LOCTEXT("SelectTest", "<Please select a test>"))
.ToolTipText(InFunction ? InFunction->GetToolTipText() : FText())
.Font(IDetailLayoutBuilder::GetDetailFont())
.IsEnabled(InterchangeTestFunction->IsValid(InFunction))
];
}
void FInterchangeTestFunctionLayout::OnFunctionComboSelectionChanged(UFunction* InSelectedItem, ESelectInfo::Type SelectInfo)
{
FInterchangeTestFunction* InterchangeTestFunction = GetStruct();
if (InterchangeTestFunction->IsValid(InSelectedItem) && InterchangeTestFunction->CheckFunction != InSelectedItem)
{
// When the function combo box of the header changes, we need to repopulate the parameter map.
// So wrap this entire operation in a single high-level transaction, and force the layout to be refreshed afterwards.
FScopedTransaction Transaction(LOCTEXT("EditTestType", "Change test to perform"));
CheckFunctionProperty->SetValue(InSelectedItem);
ParametersProperty->AsMap()->Empty();
RefreshLayout();
}
}
#undef LOCTEXT_NAMESPACE