Files
UnrealEngineUWP/Engine/Plugins/Runtime/Metasound/Source/MetasoundEditor/Private/MetasoundNodeDetailCustomization.cpp
rob gay a5adaf2847 - Fix triggered inputs firing on initialization in subsequent SoundGenerator instances
- Caused by leak in Transmission System's Global DataChannels, wherein AudioComponents were not removing instance data, which caused new sounds played from that Component taking on last value set from prior instance.
    - Remove custom input preview Sender logic in MetasoundEditor to avoid managing multiple senders (can just use the one housed on the preview AudioComponent).
#lockdown nick.whiting
#jira UE-111897
#rb phil.popp
#preflight 606bea71d5cb3c000197c7ac

#ROBOMERGE-SOURCE: CL 15927511 in //UE5/Release-5.0-EarlyAccess/...
#ROBOMERGE-BOT: STARSHIP (Release-5.0-EarlyAccess -> Main) (v786-15839533)

[CL 15927516 by rob gay in ue5-main branch]
2021-04-06 01:41:29 -04:00

1096 lines
36 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MetasoundNodeDetailCustomization.h"
#include "Components/AudioComponent.h"
#include "Containers/Set.h"
#include "CoreMinimal.h"
#include "Delegates/Delegate.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Framework/Notifications/NotificationManager.h"
#include "IDetailChildrenBuilder.h"
#include "IDetailGroup.h"
#include "Internationalization/Text.h"
#include "MetasoundAssetBase.h"
#include "MetasoundEditorGraphBuilder.h"
#include "MetasoundEditorGraphNode.h"
#include "MetasoundEditorGraphInputNodes.h"
#include "MetasoundEditorModule.h"
#include "MetasoundFrontend.h"
#include "MetasoundFrontendController.h"
#include "MetasoundFrontendRegistries.h"
#include "MetasoundUObjectRegistry.h"
#include "PropertyCustomizationHelpers.h"
#include "PropertyEditorDelegates.h"
#include "PropertyHandle.h"
#include "PropertyRestriction.h"
#include "SlateCore/Public/Styling/SlateColor.h"
#include "Templates/Casts.h"
#include "Templates/SharedPointer.h"
#include "UObject/WeakObjectPtr.h"
#include "UObject/WeakObjectPtrTemplates.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Input/STextComboBox.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Widgets/SToolTip.h"
#include "Widgets/SWidget.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "MetaSoundEditor"
namespace Metasound
{
namespace Editor
{
namespace VariableCustomizationPrivate
{
/** Minimum size of the details title panel */
static const float DetailsTitleMinWidth = 125.f;
/** Maximum size of the details title panel */
static const float DetailsTitleMaxWidth = 300.f;
/** magic number retrieved from SGraphNodeComment::GetWrapAt() */
static const float DetailsTitleWrapPadding = 32.0f;
static const FString ArrayIdentifier = TEXT(":Array");
static const FText DataTypeNameText = LOCTEXT("Node_DataTypeName", "Type");
static const FText DefaultPropertyText = LOCTEXT("Node_DefaultPropertyName", "Default Value");
static const FText NodeTooltipText = LOCTEXT("Node_Tooltip", "Tooltip");
static const FText InputNameText = LOCTEXT("Input_Name", "Input Name");
static const FText OutputNameText = LOCTEXT("Output_Name", "Output Name");
static const FName TriggerTypeName = "Trigger";
static const FName DataTypeNameIdentifier = "DataTypeName";
static const FName ProxyGeneratorClassNameIdentifier = "GeneratorClass";
void ExecuteInputTrigger(UMetasoundEditorGraphInputLiteral* Literal)
{
if (!Literal)
{
return;
}
UMetasoundEditorGraphInput* Input = Cast<UMetasoundEditorGraphInput>(Literal->GetOuter());
if (!ensure(Input))
{
return;
}
// If modifying graph not currently being previewed, do not forward request
if (UMetasoundEditorGraph* Graph = Cast<UMetasoundEditorGraph>(Input->GetOuter()))
{
if (!Graph->IsPreviewing())
{
return;
}
}
if (UAudioComponent* PreviewComponent = GEditor->GetPreviewAudioComponent())
{
if (TScriptInterface<IAudioCommunicationInterface> CommInterface = PreviewComponent->GetCommunicationInterface())
{
// TODO: fix how identifying the parameter to update is determined. It should not be done
// with a "DisplayName" but rather the vertex Guid.
Metasound::Frontend::FConstNodeHandle NodeHandle = Input->GetConstNodeHandle();
Metasound::FVertexKey VertexKey = Metasound::FVertexKey(NodeHandle->GetDisplayName().ToString());
CommInterface->Trigger(*VertexKey);
}
}
}
}
void FMetasoundInputBoolDetailCustomization::CacheProxyData(TSharedPtr<IPropertyHandle> ProxyHandle)
{
DataTypeName = FName();
const FString* MetadataDataTypeName = ProxyHandle->GetInstanceMetaData(VariableCustomizationPrivate::DataTypeNameIdentifier);
if (ensure(MetadataDataTypeName))
{
DataTypeName = **MetadataDataTypeName;
}
}
FText FMetasoundInputBoolDetailCustomization::GetPropertyNameOverride() const
{
if (DataTypeName == VariableCustomizationPrivate::TriggerTypeName)
{
return LOCTEXT("TriggerInput_SimulateTitle", "Simulate");
}
return FText::GetEmpty();
}
TSharedRef<SWidget> FMetasoundInputBoolDetailCustomization::CreateStructureWidget(TSharedPtr<IPropertyHandle>& StructPropertyHandle) const
{
using namespace Frontend;
if (FMetasoundFrontendRegistryContainer* Registry = FMetasoundFrontendRegistryContainer::Get())
{
TSharedPtr<IPropertyHandle> ValueProperty = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMetasoundEditorGraphInputBoolRef, Value));
if (ValueProperty.IsValid())
{
// Not a trigger, so just display as underlying literal type (bool)
if (DataTypeName != VariableCustomizationPrivate::TriggerTypeName)
{
return ValueProperty->CreatePropertyValueWidget();
}
return
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(2.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(SButton)
.ButtonStyle(FAppStyle::Get(), "SimpleButton")
.OnClicked_Lambda([ValueProperty]()
{
TArray<UObject*> OuterObjects;
ValueProperty->GetOuterObjects(OuterObjects);
for (UObject* Object : OuterObjects)
{
UMetasoundEditorGraphInputLiteral* Literal = Cast<UMetasoundEditorGraphInputLiteral>(Object);
VariableCustomizationPrivate::ExecuteInputTrigger(Literal);
}
return FReply::Handled();
})
.ToolTipText(LOCTEXT("TriggerTestToolTip", "Executes trigger if currently previewing Metasound."))
.ForegroundColor(FSlateColor::UseForeground())
.ContentPadding(0)
.IsFocusable(false)
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("Icons.CircleArrowDown"))
.ColorAndOpacity(FSlateColor::UseForeground())
]
];
}
}
return SNullWidget::NullWidget;
}
void FMetasoundInputIntDetailCustomization::CacheProxyData(TSharedPtr<IPropertyHandle> ProxyHandle)
{
DataTypeName = FName();
const FString* MetadataDataTypeName = ProxyHandle->GetInstanceMetaData(VariableCustomizationPrivate::DataTypeNameIdentifier);
if (ensure(MetadataDataTypeName))
{
DataTypeName = **MetadataDataTypeName;
}
}
TSharedRef<SWidget> FMetasoundInputIntDetailCustomization::CreateStructureWidget(TSharedPtr<IPropertyHandle>& StructPropertyHandle) const
{
using namespace Frontend;
if (FMetasoundFrontendRegistryContainer* Registry = FMetasoundFrontendRegistryContainer::Get())
{
TSharedPtr<IPropertyHandle> ValueProperty = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMetasoundEditorGraphInputIntRef, Value));
if (ValueProperty.IsValid())
{
TSharedPtr<const IEnumDataTypeInterface> EnumInterface = Registry->GetEnumInterfaceForDataType(DataTypeName);
// Not an enum, so just display as underlying type (int32)
if (!EnumInterface.IsValid())
{
return ValueProperty->CreatePropertyValueWidget();
}
auto GetAll = [Interface = EnumInterface](TArray<TSharedPtr<FString>>& OutStrings, TArray<TSharedPtr<SToolTip>>& OutTooltips, TArray<bool>&)
{
for (const IEnumDataTypeInterface::FGenericInt32Entry& i : Interface->GetAllEntries())
{
OutTooltips.Emplace(SNew(SToolTip).Text(i.Tooltip));
OutStrings.Emplace(MakeShared<FString>(i.DisplayName.ToString()));
}
};
auto GetValue = [Interface = EnumInterface, Prop = ValueProperty]()
{
int32 IntValue;
if (Prop->GetValue(IntValue) != FPropertyAccess::Success)
{
IntValue = Interface->GetDefaultValue();
UE_LOG(LogMetasoundEditor, Warning, TEXT("Failed to read int Property '%s', defaulting."), *GetNameSafe(Prop->GetProperty()));
}
if (TOptional<IEnumDataTypeInterface::FGenericInt32Entry> Result = Interface->FindByValue(IntValue))
{
return Result->DisplayName.ToString();
}
UE_LOG(LogMetasoundEditor, Warning, TEXT("Failed to resolve int value '%d' to a valid enum value for enum '%s'"),
IntValue, *Interface->GetNamespace().ToString());
// Return default (should always succeed as we can't have empty Enums and we must have a default).
return Interface->FindByValue(Interface->GetDefaultValue())->DisplayName.ToString();
};
auto SelectedValue = [Interface = EnumInterface, Prop = ValueProperty](const FString& InSelected)
{
TOptional<IEnumDataTypeInterface::FGenericInt32Entry> Found =
Interface->FindEntryBy([TextSelected = FText::FromString(InSelected)](const IEnumDataTypeInterface::FGenericInt32Entry& i)
{
return i.DisplayName.EqualTo(TextSelected);
});
if (Found)
{
// Only save the changes if its different and we can read the old value to check that.
int32 CurrentValue;
bool bReadCurrentValue = Prop->GetValue(CurrentValue) == FPropertyAccess::Success;
if ((bReadCurrentValue && CurrentValue != Found->Value) || !bReadCurrentValue)
{
ensure(Prop->SetValue(Found->Value) == FPropertyAccess::Success);
}
}
else
{
UE_LOG(LogMetasoundEditor, Warning, TEXT("Failed to Set Valid Value for Property '%s' with Value of '%s', writing default."),
*GetNameSafe(Prop->GetProperty()), *InSelected);
ensure(Prop->SetValue(Interface->GetDefaultValue()) == FPropertyAccess::Success);
}
};
return PropertyCustomizationHelpers::MakePropertyComboBox(
nullptr,
FOnGetPropertyComboBoxStrings::CreateLambda(GetAll),
FOnGetPropertyComboBoxValue::CreateLambda(GetValue),
FOnPropertyComboBoxValueSelected::CreateLambda(SelectedValue)
);
}
}
return SNullWidget::NullWidget;
}
void FMetasoundInputObjectDetailCustomization::CacheProxyData(TSharedPtr<IPropertyHandle> ProxyHandle)
{
ProxyGenClass.Reset();
const FString* MetadataProxyGenClass = ProxyHandle->GetInstanceMetaData(VariableCustomizationPrivate::ProxyGeneratorClassNameIdentifier);
TSharedPtr<IPropertyHandle> MetadataHandle = ProxyHandle->GetParentHandle();
if (!ensure(MetadataProxyGenClass))
{
return;
}
const FName ClassName = FName(*MetadataProxyGenClass);
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* Class = *ClassIt;
if (!Class->IsNative())
{
continue;
}
if (Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
{
continue;
}
if (ClassIt->GetFName() != ClassName)
{
continue;
}
ProxyGenClass = *ClassIt;
return;
}
ensureMsgf(false, TEXT("Failed to find ProxyGeneratorClass. Class not set "));
}
TSharedRef<SWidget> FMetasoundInputObjectDetailCustomization::CreateStructureWidget(TSharedPtr<IPropertyHandle>& StructPropertyHandle) const
{
TSharedPtr<IPropertyHandle> PropertyHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMetasoundEditorGraphInputObjectRef, Object));
auto ValidateAsset = [InProxyGenClass = ProxyGenClass](const FAssetData& InAsset)
{
if (!InProxyGenClass.IsValid())
{
return false;
}
if (UObject* Object = InAsset.GetAsset())
{
if (UClass* Class = Object->GetClass())
{
return Class == InProxyGenClass.Get();
}
}
return false;
};
auto GetAssetPath = [PropertyHandle = PropertyHandle]()
{
UObject* Object = nullptr;
if (PropertyHandle->GetValue(Object) == FPropertyAccess::Success)
{
return Object->GetPathName();
}
return FString();
};
TArray<const UClass*> AllowedClasses;
AllowedClasses.Add(ProxyGenClass.Get());
auto FilterAsset = [InProxyGenClass = ProxyGenClass](const FAssetData& InAsset)
{
if (InProxyGenClass.IsValid())
{
if (UObject* Object = InAsset.GetAsset())
{
if (UClass* Class = Object->GetClass())
{
return Class != InProxyGenClass.Get();
}
}
}
return true;
};
return SNew(SObjectPropertyEntryBox)
.ObjectPath_Lambda(GetAssetPath)
.AllowedClass(ProxyGenClass.Get())
.OnShouldSetAsset_Lambda(ValidateAsset)
.OnShouldFilterAsset_Lambda(FilterAsset)
.PropertyHandle(PropertyHandle)
.AllowClear(true)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.DisplayThumbnail(true)
.NewAssetFactories(PropertyCustomizationHelpers::GetNewAssetFactoriesForClasses(AllowedClasses));
}
TSharedRef<SWidget> FMetasoundInputArrayDetailCustomizationBase::CreateNameWidget(TSharedPtr<IPropertyHandle> StructPropertyHandle) const
{
const FText PropertyName = GetPropertyNameOverride();
if (!PropertyName.IsEmpty())
{
return SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(PropertyName);
}
return SNew(STextBlock)
.Text(VariableCustomizationPrivate::DefaultPropertyText)
.Font(IDetailLayoutBuilder::GetDetailFont());
}
TSharedRef<SWidget> FMetasoundInputArrayDetailCustomizationBase::CreateValueWidget(TSharedPtr<IPropertyHandleArray> ParentArrayProperty, TSharedPtr<IPropertyHandle> StructPropertyHandle, bool bIsInArray) const
{
TSharedRef<SWidget> ValueWidget = CreateStructureWidget(StructPropertyHandle);
if (!bIsInArray)
{
return ValueWidget;
}
TSharedPtr<IPropertyHandle> StructPropertyPtr = StructPropertyHandle;
FExecuteAction InsertAction = FExecuteAction::CreateLambda([ParentArrayProperty, StructPropertyPtr]
{
const int32 ArrayIndex = StructPropertyPtr.IsValid() ? StructPropertyPtr->GetIndexInArray() : INDEX_NONE;
if (ParentArrayProperty.IsValid() && ArrayIndex >= 0)
{
ParentArrayProperty->Insert(ArrayIndex);
}
});
FExecuteAction DeleteAction = FExecuteAction::CreateLambda([ParentArrayProperty, StructPropertyPtr]
{
const int32 ArrayIndex = StructPropertyPtr.IsValid() ? StructPropertyPtr->GetIndexInArray() : INDEX_NONE;
if (ParentArrayProperty.IsValid() && ArrayIndex >= 0)
{
ParentArrayProperty->DeleteItem(ArrayIndex);
}
});
FExecuteAction DuplicateAction = FExecuteAction::CreateLambda([ParentArrayProperty, StructPropertyPtr]
{
const int32 ArrayIndex = StructPropertyPtr.IsValid() ? StructPropertyPtr->GetIndexInArray() : INDEX_NONE;
if (ParentArrayProperty.IsValid() && ArrayIndex >= 0)
{
ParentArrayProperty->DuplicateItem(ArrayIndex);
}
});
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(0.95f)
.Padding(1.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
[
ValueWidget
]
+ SHorizontalBox::Slot()
.FillWidth(0.05f)
.Padding(1.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
[
PropertyCustomizationHelpers::MakeInsertDeleteDuplicateButton(InsertAction, DeleteAction, DuplicateAction)
];
}
void FMetasoundInputArrayDetailCustomizationBase::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
bool bIsInArray = false;
TSharedPtr<IPropertyHandleArray> ParentArrayProperty;
TSharedPtr<IPropertyHandle> ProxyProperty = StructPropertyHandle;
{
TSharedPtr<IPropertyHandle> ParentProperty = ProxyProperty->GetParentHandle();
if (ProxyProperty.IsValid() && ParentProperty.IsValid())
{
ParentArrayProperty = ParentProperty->AsArray();
if (ParentArrayProperty.IsValid())
{
ProxyProperty = ParentProperty;
bIsInArray = true;
}
}
}
CacheProxyData(ProxyProperty);
TSharedRef<SWidget> ValueWidget = CreateValueWidget(ParentArrayProperty, StructPropertyHandle, bIsInArray);
FDetailWidgetRow& ValueRow = ChildBuilder.AddCustomRow(VariableCustomizationPrivate::DefaultPropertyText);
if (bIsInArray)
{
ValueRow.NameContent()
[
StructPropertyHandle->CreatePropertyNameWidget()
];
}
else
{
ValueRow.NameContent()
[
CreateNameWidget(StructPropertyHandle)
];
}
TArray<UObject*> OuterObjects;
StructPropertyHandle->GetOuterObjects(OuterObjects);
TArray<TWeakObjectPtr<UMetasoundEditorGraphInput>> Inputs;
for (UObject* Object : OuterObjects)
{
if (UMetasoundEditorGraphInput* Input = Cast<UMetasoundEditorGraphInput>(Object))
{
Inputs.Add(Input);
}
}
FSimpleDelegate OnLiteralChanged = FSimpleDelegate::CreateLambda([InInputs = Inputs]()
{
for (const TWeakObjectPtr<UMetasoundEditorGraphInput>& GraphInput : InInputs)
{
if (GraphInput.IsValid())
{
GraphInput->OnLiteralChanged();
}
}
});
StructPropertyHandle->SetOnChildPropertyValueChanged(OnLiteralChanged);
ValueRow.ValueContent()
[
ValueWidget
];
}
void FMetasoundInputArrayDetailCustomizationBase::CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
}
void FMetasoundVariableDataTypeSelector::AddDataTypeSelector(IDetailLayoutBuilder& InDetailLayout, const FText& InRowName, TWeakObjectPtr<UMetasoundEditorGraphVariable> InGraphVariable, bool bIsRequired)
{
DetailLayoutBuilder = &InDetailLayout;
IDetailCategoryBuilder& CategoryBuilder = InDetailLayout.EditCategory("General");
TSharedPtr<FString> CurrentTypeString;
FString CurrentTypeName = InGraphVariable->TypeName.ToString();
bool bCurrentTypeIsArray = CurrentTypeName.EndsWith(VariableCustomizationPrivate::ArrayIdentifier);
if (bCurrentTypeIsArray)
{
CurrentTypeName.LeftChopInline(VariableCustomizationPrivate::ArrayIdentifier.Len());
}
DataTypeNames.Reset();
IMetasoundEditorModule& EditorModule = FModuleManager::GetModuleChecked<IMetasoundEditorModule>("MetaSoundEditor");
EditorModule.IterateDataTypes([&](const FEditorDataType& EditorDataType)
{
const FString TypeName = EditorDataType.RegistryInfo.DataTypeName.ToString();
// Array types are handles separately via checkbox
if (TypeName.EndsWith(VariableCustomizationPrivate::ArrayIdentifier))
{
return;
}
TSharedPtr<FString> TypeStrPtr = MakeShared<FString>(TypeName);
if (TypeName == CurrentTypeName)
{
CurrentTypeString = TypeStrPtr;
}
DataTypeNames.Add(TypeStrPtr);
});
if (!ensure(CurrentTypeString.IsValid()))
{
return;
}
DataTypeNames.Sort([](const TSharedPtr<FString>& DataTypeNameL, const TSharedPtr<FString>& DataTypeNameR)
{
if (DataTypeNameL.IsValid() && DataTypeNameR.IsValid())
{
return DataTypeNameR->Compare(*DataTypeNameL.Get()) > 0;
}
return false;
});
CategoryBuilder.AddCustomRow(InRowName)
.IsEnabled(!bIsRequired)
.NameContent()
[
SNew(STextBlock)
.Text(InRowName)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(0.60f)
.Padding(1.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
[
SAssignNew(DataTypeComboBox, STextComboBox)
.OptionsSource(&DataTypeNames)
.InitiallySelectedItem(CurrentTypeString)
.OnSelectionChanged_Lambda([this, InGraphVariable](TSharedPtr<FString> ItemSelected, ESelectInfo::Type SelectInfo)
{
OnBaseDataTypeChanged(InGraphVariable, ItemSelected, SelectInfo);
})
.IsEnabled(!bIsRequired)
]
+ SHorizontalBox::Slot()
.FillWidth(0.40f)
.Padding(2.0f, 0.0f, 0.0f, 0.0f)
.VAlign(VAlign_Center)
[
SAssignNew(DataTypeArrayCheckbox, SCheckBox)
.IsChecked_Lambda([this, InGraphVariable]()
{
return OnGetDataTypeArrayCheckState(InGraphVariable);
})
.OnCheckStateChanged_Lambda([this, InGraphVariable](ECheckBoxState InNewState)
{
OnDataTypeArrayChanged(InGraphVariable, InNewState);
})
[
SNew(STextBlock)
.Text(LOCTEXT("Node_IsArray", "Is Array"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
]
];
}
ECheckBoxState FMetasoundVariableDataTypeSelector::OnGetDataTypeArrayCheckState(TWeakObjectPtr<UMetasoundEditorGraphVariable> InGraphVariable) const
{
if (InGraphVariable.IsValid())
{
FString CurrentTypeName = InGraphVariable->TypeName.ToString();
bool bCurrentTypeIsArray = CurrentTypeName.EndsWith(VariableCustomizationPrivate::ArrayIdentifier);
return bCurrentTypeIsArray ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
}
return ECheckBoxState::Undetermined;
}
void FMetasoundInputDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
{
using namespace Frontend;
TMetasoundVariableDetailCustomization<UMetasoundEditorGraphInput>::CustomizeDetails(DetailLayout);
if (!GraphVariable.IsValid())
{
return;
}
IDetailCategoryBuilder& CategoryBuilder = DetailLayout.EditCategory("General");
const bool bIsRequired = IsRequired();
DisplayNameEditableTextBox = SNew(SEditableTextBox)
.Text(this, &FMetasoundInputDetailCustomization::GetDisplayName)
.OnTextChanged(this, &FMetasoundInputDetailCustomization::OnDisplayNameChanged)
.OnTextCommitted(this, &FMetasoundInputDetailCustomization::OnDisplayNameCommitted)
.IsReadOnly(bIsRequired)
.Font(IDetailLayoutBuilder::GetDetailFont());
CategoryBuilder.AddCustomRow(VariableCustomizationPrivate::InputNameText)
.EditCondition(!bIsRequired, nullptr)
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFontBold())
.Text(VariableCustomizationPrivate::InputNameText)
.ToolTipText(TAttribute<FText>::Create([GraphVariable = this->GraphVariable]()
{
if (GraphVariable.IsValid())
{
FNodeHandle NodeHandle = GraphVariable->GetNodeHandle();
FMetasoundFrontendNodeStyle NodeStyle = NodeHandle->GetNodeStyle();
return NodeHandle->GetDescription();
}
return FText::GetEmpty();
}))
]
.ValueContent()
[
DisplayNameEditableTextBox.ToSharedRef()
];
CategoryBuilder.AddCustomRow(VariableCustomizationPrivate::NodeTooltipText)
.EditCondition(!bIsRequired, nullptr)
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFontBold())
.Text(VariableCustomizationPrivate::NodeTooltipText)
]
.ValueContent()
[
SNew(SMultiLineEditableTextBox)
.Text(this, &FMetasoundInputDetailCustomization::GetTooltip)
.OnTextCommitted(this, &FMetasoundInputDetailCustomization::OnTooltipCommitted)
.IsReadOnly(bIsRequired)
.ModiferKeyForNewLine(EModifierKey::Shift)
.RevertTextOnEscape(true)
.WrapTextAt(VariableCustomizationPrivate::DetailsTitleMaxWidth - VariableCustomizationPrivate::DetailsTitleWrapPadding)
.Font(IDetailLayoutBuilder::GetDetailFont())
];
AddDataTypeSelector(DetailLayout, VariableCustomizationPrivate::DataTypeNameText, GraphVariable, bIsRequired);
CategoryBuilder.AddCustomRow(LOCTEXT("InputPrivate", "Private"))
.Visibility(TAttribute<EVisibility>(this, &FMetasoundInputDetailCustomization::ExposePrivateVisibility))
.NameContent()
[
SNew(STextBlock)
.Text(LOCTEXT("InputPrivate", "Private"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
[
SNew(SCheckBox)
.IsChecked(this, &FMetasoundInputDetailCustomization::OnGetPrivateCheckboxState)
.OnCheckStateChanged(this, &FMetasoundInputDetailCustomization::OnPrivateChanged)
];
FNodeHandle NodeHandle = GraphVariable->GetNodeHandle();
const TArray<FOutputHandle>& Outputs = NodeHandle->GetOutputs();
if (!ensure(!Outputs.IsEmpty()))
{
return;
}
IDetailCategoryBuilder& DefaultCategoryBuilder = DetailLayout.EditCategory("DefaultValue");
TSharedPtr<IPropertyHandle> LiteralHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UMetasoundEditorGraphInput, Literal));
if (ensure(GraphVariable.IsValid()) && ensure(LiteralHandle.IsValid()))
{
TSharedPtr<IPropertyHandle> DefaultValueHandle;
UObject* LiteralObject = nullptr;
if (LiteralHandle->GetValue(LiteralObject) == FPropertyAccess::Success)
{
if (ensure(LiteralObject))
{
LiteralHandle->MarkHiddenByCustomization();
if (IDetailPropertyRow* Row = DefaultCategoryBuilder.AddExternalObjectProperty(TArray<UObject*>({ LiteralObject }), "Default"))
{
DefaultValueHandle = Row->GetPropertyHandle();
if (DefaultValueHandle.IsValid())
{
SetDefaultPropertyMetaData(DefaultValueHandle.ToSharedRef());
FSimpleDelegate OnLiteralChanged = FSimpleDelegate::CreateLambda([GraphVariable = this->GraphVariable]()
{
if (GraphVariable.IsValid())
{
GraphVariable->OnLiteralChanged();
}
});
DefaultValueHandle->SetOnPropertyValueChanged(OnLiteralChanged);
DefaultValueHandle->SetOnChildPropertyValueChanged(OnLiteralChanged);
TSharedPtr<IPropertyHandleArray> DefaultValueArray = DefaultValueHandle->AsArray();
if (DefaultValueArray.IsValid())
{
DefaultValueArray->SetOnNumElementsChanged(OnLiteralChanged);
}
}
}
}
else
{
DefaultCategoryBuilder.AddProperty(LiteralHandle);
}
}
}
}
void FMetasoundVariableDataTypeSelector::OnDataTypeArrayChanged(TWeakObjectPtr<UMetasoundEditorGraphVariable> InGraphVariable, ECheckBoxState InNewState)
{
if (InGraphVariable.IsValid())
{
TSharedPtr<FString> DataTypeRoot = DataTypeComboBox->GetSelectedItem();
if (ensure(DataTypeRoot.IsValid()))
{
FString DataTypeString = *DataTypeRoot.Get();
if (InNewState == ECheckBoxState::Checked)
{
DataTypeString += VariableCustomizationPrivate::ArrayIdentifier;
}
// Have to stop playback to avoid attempting to change live edit data on invalid input type.
check(GEditor);
GEditor->ResetPreviewAudioComponent();
InGraphVariable->SetDataType(FName(DataTypeString));
// Required to rebuild the literal details customization.
// This is seemingly dangerous (as the Builder's raw ptr is cached),
// but the builder cannot be accessed any other way and instances of
// this type are always built from and managed by the parent DetailLayoutBuilder.
check(DetailLayoutBuilder);
DetailLayoutBuilder->ForceRefreshDetails();
}
}
}
void FMetasoundVariableDataTypeSelector::OnBaseDataTypeChanged(TWeakObjectPtr<UMetasoundEditorGraphVariable> InGraphVariable, TSharedPtr<FString> ItemSelected, ESelectInfo::Type SelectInfo)
{
if (ItemSelected.IsValid() && !ItemSelected->IsEmpty() && InGraphVariable.IsValid())
{
FString DataTypeString = *ItemSelected.Get();
if (DataTypeArrayCheckbox->GetCheckedState() == ECheckBoxState::Checked)
{
DataTypeString += VariableCustomizationPrivate::ArrayIdentifier;
}
// Have to stop playback to avoid attempting to change live edit data on invalid input type.
check(GEditor);
GEditor->ResetPreviewAudioComponent();
InGraphVariable->SetDataType(FName(DataTypeString));
// Required to rebuild the literal details customization.
// This is seemingly dangerous (as the Builder's raw ptr is cached),
// but the builder cannot be accessed any other way and instances of
// this type are always built from and managed by the parent DetailLayoutBuilder.
check(DetailLayoutBuilder);
DetailLayoutBuilder->ForceRefreshDetails();
}
}
void FMetasoundInputDetailCustomization::SetDefaultPropertyMetaData(TSharedRef<IPropertyHandle> InDefaultPropertyHandle) const
{
using namespace Frontend;
if (!GraphVariable.IsValid())
{
return;
}
FMetasoundFrontendRegistryContainer* Registry = FMetasoundFrontendRegistryContainer::Get();
if (!ensure(Registry))
{
return;
}
const FName TypeName = GetLiteralDataType();
if (TypeName.IsNone())
{
return;
}
FString TypeNameString = TypeName.ToString();
if (TypeNameString.EndsWith(VariableCustomizationPrivate::ArrayIdentifier))
{
TypeNameString = TypeNameString.LeftChop(VariableCustomizationPrivate::ArrayIdentifier.Len());
}
InDefaultPropertyHandle->SetInstanceMetaData(VariableCustomizationPrivate::DataTypeNameIdentifier, TypeNameString);
FDataTypeRegistryInfo DataTypeInfo;
if (!ensure(Registry->GetInfoForDataType(TypeName, DataTypeInfo)))
{
return;
}
const EMetasoundFrontendLiteralType LiteralType = GetMetasoundFrontendLiteralType(DataTypeInfo.PreferredLiteralType);
if (LiteralType != EMetasoundFrontendLiteralType::UObject && LiteralType != EMetasoundFrontendLiteralType::UObjectArray)
{
return;
}
UClass* ProxyGenClass = DataTypeInfo.ProxyGeneratorClass;
if (ProxyGenClass)
{
const FString ClassName = ProxyGenClass->GetName();
InDefaultPropertyHandle->SetInstanceMetaData(VariableCustomizationPrivate::ProxyGeneratorClassNameIdentifier, ClassName);
}
}
FName FMetasoundInputDetailCustomization::GetLiteralDataType() const
{
using namespace Frontend;
FName TypeName;
// Just take last type. If more than one, all types are the same.
FConstNodeHandle NodeHandle = GraphVariable->GetConstNodeHandle();
NodeHandle->IterateConstOutputs([InTypeName = &TypeName](FConstOutputHandle OutputHandle)
{
*InTypeName = OutputHandle->GetDataType();
});
return TypeName;
}
void FMetasoundInputDetailCustomization::OnDisplayNameChanged(const FText& InNewName)
{
using namespace Frontend;
bIsNameInvalid = false;
DisplayNameEditableTextBox->SetError(FText::GetEmpty());
if (!ensure(GraphVariable.IsValid()))
{
return;
}
if (InNewName.IsEmpty())
{
bIsNameInvalid = true;
DisplayNameEditableTextBox->SetError(FText::Format(LOCTEXT("InputRenameInvalid_NameEmpty", "{0} cannot be empty string."), InNewName));
return;
}
FConstNodeHandle NodeHandle = GraphVariable->GetConstNodeHandle();
FConstGraphHandle GraphHandle = NodeHandle->GetOwningGraph();
const FGuid NodeID = NodeHandle->GetID();
GraphHandle->IterateConstNodes([this, NodeID, InNewName](FConstNodeHandle NodeToCompare)
{
// Disregard display name collisions with hidden nodes
const bool bIsVisible = NodeToCompare->GetNodeStyle().Display.Visibility == EMetasoundFrontendNodeStyleDisplayVisibility::Visible;
if (NodeID != NodeToCompare->GetID() && bIsVisible)
{
if (InNewName.CompareToCaseIgnored(NodeToCompare->GetDisplayName()) == 0)
{
bIsNameInvalid = true;
DisplayNameEditableTextBox->SetError(FText::Format(LOCTEXT("InputRenameInvalid_NameTaken", "{0} is already in use"), InNewName));
}
}
}, EMetasoundFrontendClassType::Input);
}
void FMetasoundOutputDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
{
using namespace Frontend;
TMetasoundVariableDetailCustomization<UMetasoundEditorGraphOutput>::CustomizeDetails(DetailLayout);
if (!GraphVariable.IsValid())
{
return;
}
IDetailCategoryBuilder& CategoryBuilder = DetailLayout.EditCategory("General");
const bool bIsRequired = IsRequired();
DisplayNameEditableTextBox = SNew(SEditableTextBox)
.Text(this, &FMetasoundOutputDetailCustomization::GetDisplayName)
.OnTextChanged(this, &FMetasoundOutputDetailCustomization::OnDisplayNameChanged)
.OnTextCommitted(this, &FMetasoundOutputDetailCustomization::OnDisplayNameCommitted)
.IsReadOnly(bIsRequired)
.Font(IDetailLayoutBuilder::GetDetailFont());
CategoryBuilder.AddCustomRow(VariableCustomizationPrivate::OutputNameText)
.EditCondition(!bIsRequired, nullptr)
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFontBold())
.Text(VariableCustomizationPrivate::OutputNameText)
.ToolTipText(TAttribute<FText>::Create([GraphVariable = this->GraphVariable]()
{
if (GraphVariable.IsValid())
{
FNodeHandle NodeHandle = GraphVariable->GetNodeHandle();
return NodeHandle->GetDescription();
}
return FText::GetEmpty();
}))
]
.ValueContent()
[
DisplayNameEditableTextBox.ToSharedRef()
];
CategoryBuilder.AddCustomRow(VariableCustomizationPrivate::NodeTooltipText)
.EditCondition(!bIsRequired, nullptr)
.NameContent()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFontBold())
.Text(VariableCustomizationPrivate::NodeTooltipText)
]
.ValueContent()
[
SNew(SMultiLineEditableTextBox)
.Text(this, &FMetasoundOutputDetailCustomization::GetTooltip)
.OnTextCommitted(this, &FMetasoundOutputDetailCustomization::OnTooltipCommitted)
.IsReadOnly(bIsRequired)
.ModiferKeyForNewLine(EModifierKey::Shift)
.RevertTextOnEscape(true)
.WrapTextAt(VariableCustomizationPrivate::DetailsTitleMaxWidth - VariableCustomizationPrivate::DetailsTitleWrapPadding)
.Font(IDetailLayoutBuilder::GetDetailFont())
];
AddDataTypeSelector(DetailLayout, VariableCustomizationPrivate::DataTypeNameText, GraphVariable, bIsRequired);
CategoryBuilder.AddCustomRow(LOCTEXT("OutputPrivate", "Private"))
.Visibility(TAttribute<EVisibility>(this, &FMetasoundOutputDetailCustomization::ExposePrivateVisibility))
.NameContent()
[
SNew(STextBlock)
.Text(LOCTEXT("OutputPrivate", "Private"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
[
SNew(SCheckBox)
.IsChecked(this, &FMetasoundOutputDetailCustomization::OnGetPrivateCheckboxState)
.OnCheckStateChanged(this, &FMetasoundOutputDetailCustomization::OnPrivateChanged)
];
}
void FMetasoundOutputDetailCustomization::SetDefaultPropertyMetaData(TSharedRef<IPropertyHandle> InDefaultPropertyHandle) const
{
using namespace Frontend;
if (!GraphVariable.IsValid())
{
return;
}
FMetasoundFrontendRegistryContainer* Registry = FMetasoundFrontendRegistryContainer::Get();
if (!ensure(Registry))
{
return;
}
const FName TypeName = GetLiteralDataType();
if (TypeName.IsNone())
{
return;
}
FString TypeNameString = TypeName.ToString();
if (TypeNameString.EndsWith(VariableCustomizationPrivate::ArrayIdentifier))
{
TypeNameString = TypeNameString.LeftChop(VariableCustomizationPrivate::ArrayIdentifier.Len());
}
InDefaultPropertyHandle->SetInstanceMetaData(VariableCustomizationPrivate::DataTypeNameIdentifier, TypeNameString);
FDataTypeRegistryInfo DataTypeInfo;
if (!ensure(Registry->GetInfoForDataType(TypeName, DataTypeInfo)))
{
return;
}
const EMetasoundFrontendLiteralType LiteralType = GetMetasoundFrontendLiteralType(DataTypeInfo.PreferredLiteralType);
if (LiteralType != EMetasoundFrontendLiteralType::UObject && LiteralType != EMetasoundFrontendLiteralType::UObjectArray)
{
return;
}
UClass* ProxyGenClass = DataTypeInfo.ProxyGeneratorClass;
if (ProxyGenClass)
{
const FString ClassName = ProxyGenClass->GetName();
InDefaultPropertyHandle->SetInstanceMetaData(VariableCustomizationPrivate::ProxyGeneratorClassNameIdentifier, ClassName);
}
}
FName FMetasoundOutputDetailCustomization::GetLiteralDataType() const
{
using namespace Frontend;
FName TypeName;
// Just take last type. If more than one, all types are the same.
FConstNodeHandle NodeHandle = GraphVariable->GetConstNodeHandle();
NodeHandle->IterateConstInputs([InTypeName = &TypeName](FConstInputHandle InputHandle)
{
*InTypeName = InputHandle->GetDataType();
});
return TypeName;
}
void FMetasoundOutputDetailCustomization::OnDisplayNameChanged(const FText& InNewName)
{
using namespace Frontend;
bIsNameInvalid = false;
DisplayNameEditableTextBox->SetError(FText::GetEmpty());
if (!ensure(GraphVariable.IsValid()))
{
return;
}
if (InNewName.IsEmpty())
{
bIsNameInvalid = true;
DisplayNameEditableTextBox->SetError(FText::Format(LOCTEXT("OutputRenameInvalid_NameEmpty", "{0} cannot be empty string."), InNewName));
return;
}
FConstNodeHandle NodeHandle = GraphVariable->GetConstNodeHandle();
FConstGraphHandle GraphHandle = NodeHandle->GetOwningGraph();
const FGuid NodeID = NodeHandle->GetID();
GraphHandle->IterateConstNodes([this, NodeID, InNewName](FConstNodeHandle NodeToCompare)
{
if (NodeID != NodeToCompare->GetID())
{
if (InNewName.CompareToCaseIgnored(NodeToCompare->GetDisplayName()) == 0)
{
bIsNameInvalid = true;
DisplayNameEditableTextBox->SetError(FText::Format(LOCTEXT("OutputRenameInvalid_NameTaken", "{0} is already in use"), InNewName));
}
}
}, EMetasoundFrontendClassType::Output);
}
} // namespace Editor
} // namespace Metasound
#undef LOCTEXT_NAMESPACE