// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundDetailCustomization.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 "IDetailGroup.h" #include "MetasoundAssetBase.h" #include "MetasoundFrontend.h" #include "MetasoundFrontendController.h" #include "MetasoundUObjectRegistry.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/Notifications/SNotificationList.h" #include "Widgets/Text/STextBlock.h" #define LOCTEXT_NAMESPACE "MetasoundEditor" static int32 ShowLiteralMetasoundInputsInEditorCVar = 0; FAutoConsoleVariableRef CVarShowLiteralMetasoundInputsInEditor( TEXT("au.Debug.Editor.Metasounds.ShowLiteralInputs"), ShowLiteralMetasoundInputsInEditorCVar, TEXT("Show literal inputs in the Metasound Editor.\n") TEXT("0: Disabled (default), !0: Enabled"), ECVF_Default); namespace Metasound { namespace Editor { FName BuildChildPath(const FString& InBasePath, FName InPropertyName) { return FName(InBasePath + TEXT(".") + InPropertyName.ToString()); } FName BuildChildPath(const FName& InBasePath, FName InPropertyName) { return FName(InBasePath.ToString() + TEXT(".") + InPropertyName.ToString()); } TSet GetLiteralInputs(IDetailLayoutBuilder& InDetailLayout) { TSet LiteralInputs; TArray> Objects; InDetailLayout.GetObjectsBeingCustomized(Objects); if (Objects.IsEmpty() || !Objects[0].IsValid()) { return LiteralInputs; } UObject* Metasound = Objects[0].Get(); if (FMetasoundAssetBase* MetasoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(Metasound)) { Frontend::FGraphHandle GraphHandle = MetasoundAsset->GetRootGraphHandle(); TArray InputNodes = GraphHandle->GetInputNodes(); for (Frontend::FNodeHandle& NodeHandle : InputNodes) { if (NodeHandle->GetNodeStyle().Display.Visibility == EMetasoundFrontendNodeStyleDisplayVisibility::Hidden) { LiteralInputs.Add(NodeHandle->GetNodeName()); } } } return LiteralInputs; } template void BuildIOFixedArray(IDetailLayoutBuilder& InDetailLayout, FName InCategoryName, FName InPropertyName, const TSet& InRequiredValues) { const bool bIsInput = InCategoryName == "Inputs"; IDetailCategoryBuilder& CategoryBuilder = InDetailLayout.EditCategory(InCategoryName); TSharedPtr ParentProperty = InDetailLayout.GetProperty(InPropertyName); TSharedPtr ArrayHandle = ParentProperty->AsArray(); TSet LiteralInputs; if (bIsInput && !ShowLiteralMetasoundInputsInEditorCVar) { LiteralInputs = GetLiteralInputs(InDetailLayout); } uint32 NumElements = 0; ArrayHandle->GetNumElements(NumElements); for (int32 i = 0; i < static_cast(NumElements); ++i) { TSharedRef ArrayItemHandle = ArrayHandle->GetElement(i); const FName TypeNamePropertyName = GET_MEMBER_NAME_CHECKED(T, TypeName); const FName NamePropertyName = GET_MEMBER_NAME_CHECKED(T, Name); const FName ToolTipPropertyName = GET_MEMBER_NAME_CHECKED(FMetasoundFrontendVertexMetadata, Description); const FName DisplayNamePropertyName = GET_MEMBER_NAME_CHECKED(FMetasoundFrontendVertexMetadata, DisplayName); TSharedPtr TypeProperty = ArrayItemHandle->GetChildHandle(TypeNamePropertyName); TSharedPtr NameProperty = ArrayItemHandle->GetChildHandle(NamePropertyName); TSharedPtr ToolTipProperty = ArrayItemHandle->GetChildHandle(ToolTipPropertyName, true /* bRecurse */); TSharedPtr DisplayNameProperty = ArrayItemHandle->GetChildHandle(DisplayNamePropertyName, true /* bRecurse */); FString Name; const bool bNameFound = NameProperty->GetValue(Name) == FPropertyAccess::Success; const bool bIsRequired = bNameFound && InRequiredValues.Contains(Name); // Hide literal members if (LiteralInputs.Contains(Name)) { continue; } FText DisplayName; DisplayNameProperty->GetValue(DisplayName); DisplayNameProperty->SetInstanceMetaData("LastValidName", DisplayName.ToString()); FSimpleDelegate NameChangeDelegate = FSimpleDelegate::CreateLambda([ChangedIndex = i, DisplayNameProperty, ArrayHandle, DisplayNamePropertyName] { FText MatchDisplayName; if (DisplayNameProperty->GetValue(MatchDisplayName) != FPropertyAccess::Success) { return; } if (const FString* LastValidName = DisplayNameProperty->GetInstanceMetaData("LastValidName")) { uint32 NumElements = 0; ArrayHandle->GetNumElements(NumElements); for (int32 Elem = 0; Elem < static_cast(NumElements); ++Elem) { TSharedRef ElemHandle = ArrayHandle->GetElement(Elem); TSharedPtr ElemNameProperty = ElemHandle->GetChildHandle(DisplayNamePropertyName, true /* bRecurse */); FText ElemDisplayName; if (ElemNameProperty->GetValue(ElemDisplayName) == FPropertyAccess::Success) { if (Elem != ChangedIndex && !ElemDisplayName.CompareTo(MatchDisplayName)) { DisplayNameProperty->SetValue(FText::FromString(*LastValidName)); FNotificationInfo Info(LOCTEXT("MetasoundEditor_InvalidRename", "Rename failed: Input/output with name already exists.")); Info.bFireAndForget = true; Info.ExpireDuration = 2.0f; Info.bUseThrobber = true; FSlateNotificationManager::Get().AddNotification(Info); return; } } } } DisplayNameProperty->SetInstanceMetaData("LastValidName", MatchDisplayName.ToString()); }); DisplayNameProperty->SetOnPropertyValueChanged(NameChangeDelegate); CategoryBuilder.AddCustomRow(ParentProperty->GetPropertyDisplayName()) .EditCondition(!bIsRequired, nullptr) .NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFontBold()) .Text(TAttribute::Create([i, bIsRequired, DisplayNameProperty, TypeProperty]() { FName TypeName; TypeProperty->GetValue(TypeName); FString TypeNameString = TypeName.ToString(); // Remove namespace info to keep concise TypeNameString.RightChopInline(TypeNameString.Find(TEXT(":"), ESearchCase::IgnoreCase, ESearchDir::FromEnd) + 1); FText DisplayName; DisplayNameProperty->GetValue(DisplayName); if (bIsRequired) { return FText::Format(LOCTEXT("MetasoundEditor_FixedIOArrayRequiredEntry_Format", "{0}. {1} ({2}, Required)"), FText::AsNumber(i + 1), DisplayName, FText::FromString(TypeNameString)); } else { return FText::Format(LOCTEXT("MetasoundEditor_FixedIOArray_Format", "{0}. {1} ({2})"), FText::AsNumber(i + 1), DisplayName, FText::FromString(TypeNameString)); } })) .ToolTipText(TAttribute::Create([ToolTipProperty]() { FText ToolTip; ToolTipProperty->GetValue(ToolTip); return ToolTip; })) ]; if (!bIsRequired) { CategoryBuilder.AddProperty(DisplayNameProperty); CategoryBuilder.AddProperty(ToolTipProperty); } if (bIsInput) { const FName DefaultsPropertyName = GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassInput, Defaults); TSharedPtr DefaultsProperty = ArrayItemHandle->GetChildHandle(DefaultsPropertyName); TSharedPtr DefaultsArrayHandle = DefaultsProperty->AsArray(); uint32 NumDefaults = 0; DefaultsArrayHandle->GetNumElements(NumDefaults); for (uint32 InputLiteralIndex = 0; InputLiteralIndex < NumDefaults; ++InputLiteralIndex) { TSharedPtr LiteralHandle = DefaultsArrayHandle->GetElement(InputLiteralIndex)->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMetasoundFrontendVertexLiteral, Value)); CategoryBuilder.AddProperty(LiteralHandle); } } } FSimpleDelegate RefreshDelegate = FSimpleDelegate::CreateLambda([DetailLayout = &InDetailLayout]() { DetailLayout->ForceRefreshDetails(); }); ArrayHandle->SetOnNumElementsChanged(RefreshDelegate); }; FMetasoundDetailCustomization::FMetasoundDetailCustomization(FName InDocumentPropertyName) : IDetailCustomization() , DocumentPropertyName(InDocumentPropertyName) { } FName FMetasoundDetailCustomization::GetMetadataRootClassPath() const { return Metasound::Editor::BuildChildPath(DocumentPropertyName, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendDocument, RootGraph)); } FName FMetasoundDetailCustomization::GetMetadataPropertyPath() const { const FName RootClass = FName(GetMetadataRootClassPath()); return Metasound::Editor::BuildChildPath(RootClass, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClass, Metadata)); } void FMetasoundDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { using namespace Metasound::Editor; // General Category IDetailCategoryBuilder& GeneralCategoryBuilder = DetailLayout.EditCategory("General"); TArray> ObjectsCustomized; DetailLayout.GetObjectsBeingCustomized(ObjectsCustomized); const FName AuthorPropertyPath = BuildChildPath(GetMetadataPropertyPath(), GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassMetadata, Author)); const FName DescPropertyPath = BuildChildPath(GetMetadataPropertyPath(), GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassMetadata, Description)); const FName NodeTypePropertyPath = BuildChildPath(GetMetadataPropertyPath(), GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassMetadata, Type)); const FName VersionPropertyPath = BuildChildPath(GetMetadataPropertyPath(), GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassMetadata, Version)); const FName MajorVersionPropertyPath = BuildChildPath(VersionPropertyPath, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendVersionNumber, Major)); const FName MinorVersionPropertyPath = BuildChildPath(VersionPropertyPath, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendVersionNumber, Minor)); TSharedPtr AuthorHandle = DetailLayout.GetProperty(AuthorPropertyPath); TSharedPtr DescHandle = DetailLayout.GetProperty(DescPropertyPath); TSharedPtr NodeTypeHandle = DetailLayout.GetProperty(NodeTypePropertyPath); TSharedPtr MajorVersionHandle = DetailLayout.GetProperty(MajorVersionPropertyPath); TSharedPtr MinorVersionHandle = DetailLayout.GetProperty(MinorVersionPropertyPath); GeneralCategoryBuilder.AddProperty(NodeTypeHandle); GeneralCategoryBuilder.AddProperty(AuthorHandle); GeneralCategoryBuilder.AddProperty(DescHandle); GeneralCategoryBuilder.AddProperty(MajorVersionHandle); GeneralCategoryBuilder.AddProperty(MinorVersionHandle); // Input/Output Categories // If editing multiple metasound objects, all should be the same type, so safe to just check first in array for // required inputs/outputs TArray> Objects; TSet RequiredInputs; TSet RequiredOutputs; DetailLayout.GetObjectsBeingCustomized(Objects); if (Objects.Num() > 0) { FMetasoundAssetBase* MetasoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(Objects[0].Get()); check(MetasoundAsset); Frontend::FDocumentHandle DocumentHandle = MetasoundAsset->GetDocumentHandle(); for (const FMetasoundFrontendClassVertex& Desc : DocumentHandle->GetRequiredInputs()) { RequiredInputs.Add(Desc.Name); } for (const FMetasoundFrontendClassVertex& Desc : DocumentHandle->GetRequiredOutputs()) { RequiredOutputs.Add(Desc.Name); } } const FName InterfacePropertyPath = BuildChildPath(GetMetadataRootClassPath(), GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClass, Interface)); const FName InputsPropertyPath = BuildChildPath(InterfacePropertyPath, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassInterface, Inputs)); const FName OutputsPropertyPath = BuildChildPath(InterfacePropertyPath, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassInterface, Outputs)); BuildIOFixedArray(DetailLayout, "Inputs", InputsPropertyPath, RequiredInputs); BuildIOFixedArray(DetailLayout, "Outputs", OutputsPropertyPath, RequiredOutputs); // Hack to hide parent structs for nested metadata properties DetailLayout.HideCategory("CustomView"); // Hack to hide categories brought in from UMetasoundSource inherited from USoundBase DetailLayout.HideCategory("Analysis"); DetailLayout.HideCategory("Attenuation"); DetailLayout.HideCategory("Curves"); DetailLayout.HideCategory("Debug"); DetailLayout.HideCategory("Developer"); DetailLayout.HideCategory("Effects"); DetailLayout.HideCategory("File Path"); DetailLayout.HideCategory("Format"); DetailLayout.HideCategory("Info"); DetailLayout.HideCategory("Loading"); DetailLayout.HideCategory("Modulation"); DetailLayout.HideCategory("Playback"); DetailLayout.HideCategory("Sound"); DetailLayout.HideCategory("SoundWave"); DetailLayout.HideCategory("Subtitles"); DetailLayout.HideCategory("Voice Management"); } } // namespace Editor } // namespace Metasound #undef LOCTEXT_NAMESPACE