// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundDetailCustomization.h" #include "Containers/Set.h" #include "Delegates/Delegate.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Framework/Notifications/NotificationManager.h" #include "IAudioParameterInterfaceRegistry.h" #include "IAudioParameterTransmitter.h" #include "IDetailGroup.h" #include "Input/Events.h" #include "Interfaces/MetasoundFrontendInterfaceRegistry.h" #include "MetasoundAssetBase.h" #include "MetasoundBuilderSubsystem.h" #include "MetasoundEditor.h" #include "MetasoundEditorGraphBuilder.h" #include "MetasoundEditorSettings.h" #include "MetasoundFrontend.h" #include "MetasoundFrontendController.h" #include "MetasoundFrontendSearchEngine.h" #include "MetasoundSource.h" #include "MetasoundUObjectRegistry.h" #include "PropertyCustomizationHelpers.h" #include "PropertyEditorDelegates.h" #include "PropertyHandle.h" #include "PropertyRestriction.h" #include "SGraphPalette.h" #include "ScopedTransaction.h" #include "Sound/SoundWave.h" #include "Styling/CoreStyle.h" #include "Styling/SlateColor.h" #include "Styling/SlateTypes.h" #include "Templates/Casts.h" #include "Templates/SharedPointer.h" #include "UObject/WeakObjectPtr.h" #include "UObject/WeakObjectPtrTemplates.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Notifications/SNotificationList.h" #include "Widgets/Text/STextBlock.h" #define LOCTEXT_NAMESPACE "MetaSoundEditor" 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()); } FMetasoundDetailCustomization::FMetasoundDetailCustomization(FName InDocumentPropertyName) : IDetailCustomization() , DocumentPropertyName(InDocumentPropertyName) { IsGraphEditableAttribute = TAttribute::Create([this]() { using namespace Metasound; using namespace Metasound::Frontend; if (const FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(MetaSound.Get())) { FConstGraphHandle GraphHandle = MetaSoundAsset->GetRootGraphHandle(); return GraphHandle->GetGraphStyle().bIsGraphEditable; } return false; }); } FName FMetasoundDetailCustomization::GetInterfaceVersionsPath() const { return Metasound::Editor::BuildChildPath(DocumentPropertyName, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendDocument, Interfaces)); } 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 Frontend; TArray> Objects; DetailLayout.GetObjectsBeingCustomized(Objects); // Only support modifying a single MetaSound at a time (Multiple // MetaSound editing will be covered most likely by separate tool). if (Objects.Num() > 1) { return; } MetaSound = Objects.Last(); if (!MetaSound.IsValid()) { return; } TWeakObjectPtr MetaSoundSource = Cast(MetaSound.Get()); // MetaSound patches don't have source settings, so view MetaSound settings by default EMetasoundActiveDetailView DetailsView = EMetasoundActiveDetailView::Metasound; if (MetaSoundSource.IsValid()) { // Show source settings by default unless previously set DetailsView = EMetasoundActiveDetailView::General; if (const UMetasoundEditorSettings* EditorSettings = GetDefault()) { DetailsView = EditorSettings->DetailView; } } switch (DetailsView) { case EMetasoundActiveDetailView::Metasound: { IDetailCategoryBuilder& GeneralCategoryBuilder = DetailLayout.EditCategory("MetaSound"); const FName AuthorPropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetAuthorPropertyName()); const FName CategoryHierarchyPropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetCategoryHierarchyPropertyName()); const FName DescPropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetDescriptionPropertyName()); const FName DisplayNamePropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetDisplayNamePropertyName()); const FName KeywordsPropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetKeywordsPropertyName()); const FName IsDeprecatedPropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetIsDeprecatedPropertyName()); const FName ClassNamePropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetClassNamePropertyName()); const FName ClassNameNamePropertyPath = BuildChildPath(ClassNamePropertyPath, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendClassName, Name)); const FName VersionPropertyPath = BuildChildPath(GetMetadataPropertyPath(), FMetasoundFrontendClassMetadata::GetVersionPropertyName()); const FName MajorVersionPropertyPath = BuildChildPath(VersionPropertyPath, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendVersionNumber, Major)); const FName MinorVersionPropertyPath = BuildChildPath(VersionPropertyPath, GET_MEMBER_NAME_CHECKED(FMetasoundFrontendVersionNumber, Minor)); const FName InterfaceVersionsPropertyPath = GetInterfaceVersionsPath(); TSharedPtr AuthorHandle = DetailLayout.GetProperty(AuthorPropertyPath); TSharedPtr CategoryHierarchyHandle = DetailLayout.GetProperty(CategoryHierarchyPropertyPath); TSharedPtr ClassNameHandle = DetailLayout.GetProperty(ClassNameNamePropertyPath); TSharedPtr DisplayNameHandle = DetailLayout.GetProperty(DisplayNamePropertyPath); TSharedPtr DescHandle = DetailLayout.GetProperty(DescPropertyPath); TSharedPtr KeywordsHandle = DetailLayout.GetProperty(KeywordsPropertyPath); TSharedPtr IsDeprecatedHandle = DetailLayout.GetProperty(IsDeprecatedPropertyPath); TSharedPtr InterfaceVersionsHandle = DetailLayout.GetProperty(InterfaceVersionsPropertyPath); TSharedPtr MajorVersionHandle = DetailLayout.GetProperty(MajorVersionPropertyPath); TSharedPtr MinorVersionHandle = DetailLayout.GetProperty(MinorVersionPropertyPath); // Invalid for UMetaSounds TSharedPtr OutputFormat = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UMetaSoundSource, OutputFormat)); if (OutputFormat.IsValid()) { if (MetaSoundSource.IsValid()) { OutputFormat->SetOnPropertyValuePreChange(FSimpleDelegate::CreateLambda([Source = MetaSoundSource]() { if (Source.IsValid()) { TSharedPtr ParentEditor = FGraphBuilder::GetEditorForMetasound(*Source.Get()); if (ParentEditor.IsValid()) { ParentEditor->Stop(); }; } })); OutputFormat->SetOnPropertyValueChanged(FSimpleDelegate::CreateLambda([Source = MetaSoundSource]() { if (Source.IsValid()) { TSharedPtr ParentEditor = FGraphBuilder::GetEditorForMetasound(*Source.Get()); if (ParentEditor.IsValid()) { ParentEditor->CreateAnalyzers(); }; } })); } TSharedRef OutputFormatValueWidget = OutputFormat->CreatePropertyValueWidget(); OutputFormatValueWidget->SetEnabled(IsGraphEditableAttribute); static const FText OutputFormatName = LOCTEXT("MetasoundOutputFormatPropertyName", "Output Format"); GeneralCategoryBuilder.AddCustomRow(OutputFormatName) .NameContent() [ OutputFormat->CreatePropertyNameWidget() ] .ValueContent() [ OutputFormatValueWidget ]; OutputFormat->MarkHiddenByCustomization(); } // Updates FText properties on open editors if required { FSimpleDelegate RegisterOnChange = FSimpleDelegate::CreateLambda([this]() { if (MetaSound.IsValid()) { if (FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(MetaSound.Get())) { MetaSoundAsset->GetDocumentChecked().RootGraph.Style.UpdateChangeID(); } constexpr bool bForceViewSynchronization = true; FGraphBuilder::RegisterGraphWithFrontend(*MetaSound.Get(), bForceViewSynchronization); } }); AuthorHandle->SetOnChildPropertyValueChanged(RegisterOnChange); DescHandle->SetOnPropertyValueChanged(RegisterOnChange); DisplayNameHandle->SetOnPropertyValueChanged(RegisterOnChange); KeywordsHandle->SetOnPropertyValueChanged(RegisterOnChange); KeywordsHandle->SetOnChildPropertyValueChanged(RegisterOnChange); IsDeprecatedHandle->SetOnPropertyValueChanged(RegisterOnChange); } GeneralCategoryBuilder.AddProperty(DisplayNameHandle); GeneralCategoryBuilder.AddProperty(DescHandle); GeneralCategoryBuilder.AddProperty(AuthorHandle); GeneralCategoryBuilder.AddProperty(IsDeprecatedHandle); GeneralCategoryBuilder.AddProperty(MajorVersionHandle); GeneralCategoryBuilder.AddProperty(MinorVersionHandle); static const FText ClassGuidName = LOCTEXT("MetasoundClassGuidPropertyName", "Class Guid"); GeneralCategoryBuilder.AddCustomRow(ClassGuidName).NameContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(ClassGuidName) .Font(FAppStyle::GetFontStyle(TEXT("PropertyWindow.NormalFont"))) ] ] .ValueContent() [ ClassNameHandle->CreatePropertyValueWidget() ]; GeneralCategoryBuilder.AddProperty(CategoryHierarchyHandle); GeneralCategoryBuilder.AddProperty(KeywordsHandle); DetailLayout.HideCategory("Attenuation"); DetailLayout.HideCategory("Developer"); DetailLayout.HideCategory("Effects"); DetailLayout.HideCategory("Loading"); DetailLayout.HideCategory("Modulation"); DetailLayout.HideCategory("Sound"); DetailLayout.HideCategory("Voice Management"); } break; case EMetasoundActiveDetailView::General: default: DetailLayout.HideCategory("MetaSound"); TArray>DeveloperProperties; TArray>SoundProperties; DetailLayout.EditCategory("Sound") .GetDefaultProperties(SoundProperties); DetailLayout.EditCategory("Developer") .GetDefaultProperties(DeveloperProperties); auto HideProperties = [](const TSet& PropsToHide, const TArray>& Properties) { for (TSharedRef Property : Properties) { if (PropsToHide.Contains(Property->GetProperty()->GetFName())) { Property->MarkHiddenByCustomization(); } } }; static const TSet SoundPropsToHide = { GET_MEMBER_NAME_CHECKED(USoundWave, bLooping), GET_MEMBER_NAME_CHECKED(USoundWave, SoundGroup) }; HideProperties(SoundPropsToHide, SoundProperties); static const TSet DeveloperPropsToHide = { GET_MEMBER_NAME_CHECKED(USoundBase, Duration), GET_MEMBER_NAME_CHECKED(USoundBase, MaxDistance), GET_MEMBER_NAME_CHECKED(USoundBase, TotalSamples) }; HideProperties(DeveloperPropsToHide, DeveloperProperties); break; } // Hack to hide parent structs for nested metadata properties DetailLayout.HideCategory("CustomView"); DetailLayout.HideCategory("Advanced"); DetailLayout.HideCategory("Analysis"); DetailLayout.HideCategory("Curves"); DetailLayout.HideCategory("File Path"); DetailLayout.HideCategory("Format"); DetailLayout.HideCategory("Info"); DetailLayout.HideCategory("Loading"); DetailLayout.HideCategory("Playback"); DetailLayout.HideCategory("Subtitles"); DetailLayout.HideCategory("Waveform Processing");; } FMetasoundInterfacesDetailCustomization::FMetasoundInterfacesDetailCustomization() { IsGraphEditableAttribute = TAttribute::Create([this]() { using namespace Metasound; using namespace Metasound::Frontend; if (const FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(MetaSound.Get())) { FConstGraphHandle GraphHandle = MetaSoundAsset->GetRootGraphHandle(); return GraphHandle->GetGraphStyle().bIsGraphEditable; } return false; }); } void FMetasoundInterfacesDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { TArray> Objects; DetailLayout.GetObjectsBeingCustomized(Objects); // Only support modifying a single MetaSound at a time (Multiple // MetaSound editing will be covered most likely by separate tool). if (Objects.Num() > 1) { return; } if (UMetasoundInterfacesView* InterfacesView = CastChecked(Objects.Last())) { MetaSound = InterfacesView->GetMetasound(); } UpdateInterfaceNames(); SAssignNew(InterfaceComboBox, SSearchableComboBox) .OptionsSource(&AddableInterfaceNames) .OnGenerateWidget_Lambda([](TSharedPtr InItem) { return SNew(STextBlock) .Text(FText::FromString(*InItem)); }) .OnSelectionChanged_Lambda([this](TSharedPtr NameToAdd, ESelectInfo::Type InSelectInfo) { using namespace Metasound; using namespace Metasound::Frontend; FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(MetaSound.Get()); if (!ensure(MetaSoundAsset)) { return; } if (InSelectInfo != ESelectInfo::OnNavigation) { FMetasoundFrontendInterface InterfaceToAdd; const FName InterfaceName { *NameToAdd.Get() }; if (ensure(ISearchEngine::Get().FindInterfaceWithHighestVersion(InterfaceName, InterfaceToAdd))) { const FScopedTransaction Transaction(FText::Format(LOCTEXT("AddInterfaceTransactionFormat", "Add MetaSound Interface '{0}'"), FText::FromString(InterfaceToAdd.Version.ToString()))); MetaSound.Get()->Modify(); MetaSoundAsset->GetGraphChecked().Modify(); TScriptInterface MetaSoundPatchDocInterface = MetaSound.Get(); FMetaSoundFrontendDocumentBuilder Builder(MetaSoundPatchDocInterface); FModifyInterfaceOptions Options({ }, { InterfaceToAdd }); Options.bSetDefaultNodeLocations = false; // Don't automatically add nodes to ed graph Builder.ModifyInterfaces(MoveTemp(Options)); } UpdateInterfaceNames(); InterfaceComboBox->RefreshOptions(); FGraphBuilder::RegisterGraphWithFrontend(*MetaSound.Get()); } }) .Content() [ SNew(STextBlock) .Text(LOCTEXT("UpdateInterfaceAction", "Add Interface...")) .IsEnabled(IsGraphEditableAttribute) ]; TSharedRef InterfaceUtilities = SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(2.0f) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .AutoWidth() [ InterfaceComboBox->AsShared() ] + SHorizontalBox::Slot() .Padding(2.0f) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .AutoWidth() [ PropertyCustomizationHelpers::MakeDeleteButton(FSimpleDelegate::CreateLambda([this]() { using namespace Frontend; FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(MetaSound.Get()); if (!ensure(MetaSoundAsset)) { return; } TArray ImplementedInterfaces; Algo::Transform(ImplementedInterfaceNames, ImplementedInterfaces, [](const FName& Name) { FMetasoundFrontendInterface Interface; ISearchEngine::Get().FindInterfaceWithHighestVersion(Name, Interface); return Interface; }); { const FScopedTransaction Transaction(LOCTEXT("RemoveAllInterfacesTransaction", "Remove All MetaSound Interfaces")); MetaSound.Get()->Modify(); MetaSoundAsset->GetGraphChecked().Modify(); FDocumentHandle DocumentHandle = MetaSoundAsset->GetDocumentHandle(); FModifyRootGraphInterfaces({ ImplementedInterfaces }, { }).Transform(DocumentHandle); } UpdateInterfaceNames(); InterfaceComboBox->RefreshOptions(); FGraphBuilder::RegisterGraphWithFrontend(*MetaSound.Get()); }), LOCTEXT("RemoveInterfaceTooltip1", "Removes all interfaces from the given MetaSound.")) ]; InterfaceUtilities->SetEnabled(IsGraphEditableAttribute); const FText HeaderName = LOCTEXT("InterfacesGroupDisplayName", "Interfaces"); IDetailCategoryBuilder& InterfaceCategory = DetailLayout.EditCategory("Interfaces", HeaderName); InterfaceCategory.AddCustomRow(HeaderName) [ InterfaceUtilities ]; auto CreateInterfaceEntryWidget = [&](FName InInterfaceName) -> TSharedRef { using namespace Frontend; FMetasoundFrontendInterface InterfaceEntry; if (!ensure(ISearchEngine::Get().FindInterfaceWithHighestVersion(InInterfaceName, InterfaceEntry))) { return SNullWidget::NullWidget; } TSharedRef RemoveButtonWidget = PropertyCustomizationHelpers::MakeDeleteButton(FSimpleDelegate::CreateLambda([this, InInterfaceName, InterfaceEntry]() { using namespace Frontend; FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(MetaSound.Get()); if (!ensure(MetaSoundAsset)) { return; } { const FScopedTransaction Transaction(FText::Format(LOCTEXT("RemoveInterfaceTransactionFormat", "Remove MetaSound Interface '{0}'"), FText::FromString(InterfaceEntry.Version.ToString()))); MetaSound.Get()->Modify(); MetaSoundAsset->GetGraphChecked().Modify(); FDocumentHandle DocumentHandle = MetaSoundAsset->GetDocumentHandle(); FModifyRootGraphInterfaces({ InterfaceEntry }, { }).Transform(DocumentHandle); } UpdateInterfaceNames(); InterfaceComboBox->RefreshOptions(); FGraphBuilder::RegisterGraphWithFrontend(*MetaSound.Get()); }), LOCTEXT("RemoveInterfaceTooltip2", "Removes the associated interface from the MetaSound.")); TSharedRef EntryWidget = SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(2.0f) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .AutoWidth() [ SNew(STextBlock) .Text(FText::FromName(InterfaceEntry.Version.Name)) ] + SHorizontalBox::Slot() .Padding(2.0f) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .AutoWidth() [ RemoveButtonWidget ]; EntryWidget->SetEnabled(IsGraphEditableAttribute); return EntryWidget; }; TArray InterfaceNames = ImplementedInterfaceNames.Array(); InterfaceNames.Sort([](const FName& A, const FName& B) { return A.LexicalLess(B); }); for (const FName& InterfaceName : InterfaceNames) { InterfaceCategory.AddCustomRow(FText::FromName(InterfaceName)) [ CreateInterfaceEntryWidget(InterfaceName) ]; } } void FMetasoundInterfacesDetailCustomization::UpdateInterfaceNames() { AddableInterfaceNames.Reset(); ImplementedInterfaceNames.Reset(); const UObject* MetaSoundObject = MetaSound.Get(); if (const FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(MetaSoundObject)) { auto GetVersionName = [](const FMetasoundFrontendVersion& Version) { return Version.Name; }; const UClass* MetaSoundClass = MetaSoundObject->GetClass(); auto CanAddOrRemoveInterface = [ClassName = MetaSoundClass->GetClassPathName()](const FMetasoundFrontendVersion& Version) { using namespace Frontend; const FInterfaceRegistryKey Key = GetInterfaceRegistryKey(Version); if (const IInterfaceRegistryEntry* Entry = IInterfaceRegistry::Get().FindInterfaceRegistryEntry(Key)) { if (const FMetasoundFrontendInterfaceUClassOptions* Options = Entry->GetInterface().FindClassOptions(ClassName)) { return Options->bIsModifiable; } // If no options are found for the given class, interface is modifiable by default. return true; } return false; }; const TSet& ImplementedInterfaces = MetaSoundAsset->GetDocumentChecked().Interfaces; Algo::TransformIf(ImplementedInterfaces, ImplementedInterfaceNames, CanAddOrRemoveInterface, GetVersionName); TArray Interfaces = Frontend::ISearchEngine::Get().FindAllInterfaces(); for (const FMetasoundFrontendInterface& Interface : Interfaces) { if (!ImplementedInterfaceNames.Contains(Interface.Version.Name)) { if (CanAddOrRemoveInterface(Interface.Version)) { FString Name = Interface.Version.Name.ToString(); AddableInterfaceNames.Add(MakeShared(MoveTemp(Name))); } } } AddableInterfaceNames.Sort([](const TSharedPtr& A, const TSharedPtr& B) { return A->Compare(*B) < 0; }); } } } // namespace Editor } // namespace Metasound #undef LOCTEXT_NAMESPACE