// Copyright Epic Games, Inc. All Rights Reserved. #include "SoundSubmixGraph/SoundSubmixGraphSchema.h" #include "AssetRegistry/AssetData.h" #include "Containers/EnumAsByte.h" #include "Containers/Set.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphSchema.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Framework/Commands/GenericCommands.h" #include "GraphEditorActions.h" #include "GraphEditorSettings.h" #include "HAL/PlatformCrt.h" #include "ISoundfieldEndpoint.h" #include "ISoundfieldFormat.h" #include "Internationalization/Internationalization.h" #include "Misc/AssertionMacros.h" #include "ScopedTransaction.h" #include "Sound/AudioSettings.h" #include "Sound/SoundSubmix.h" #include "SoundSubmixDefaultColorPalette.h" #include "SoundSubmixEditor.h" #include "SoundSubmixEditorUtilities.h" #include "SoundSubmixGraph/SoundSubmixGraph.h" #include "SoundSubmixGraph/SoundSubmixGraphNode.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Templates/Casts.h" #include "Templates/SharedPointer.h" #include "Templates/UniquePtr.h" #include "ToolMenu.h" #include "ToolMenuSection.h" #include "UObject/ObjectPtr.h" #include "UObject/SoftObjectPath.h" class FSlateRect; class FSlateWindowElementList; #define LOCTEXT_NAMESPACE "SoundSubmixSchema" FConnectionDrawingPolicy* FSoundSubmixGraphConnectionDrawingPolicyFactory::CreateConnectionPolicy( const UEdGraphSchema* Schema, int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) const { if (Schema->IsA(USoundSubmixGraphSchema::StaticClass())) { return new FSoundSubmixGraphConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, ZoomFactor, InClippingRect, InDrawElements, InGraphObj); } return nullptr; } FSoundSubmixGraphConnectionDrawingPolicy::FSoundSubmixGraphConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) : FConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, ZoomFactor, InClippingRect, InDrawElements) , GraphObj(InGraphObj) { ActiveWireThickness = Settings->TraceAttackWireThickness; InactiveWireThickness = Settings->TraceReleaseWireThickness; } // Give specific editor modes a chance to highlight this connection or darken non-interesting connections void FSoundSubmixGraphConnectionDrawingPolicy::DetermineWiringStyle(UEdGraphPin* OutputPin, UEdGraphPin* InputPin, FConnectionParams& OutParams) { if (!(OutputPin && InputPin && GraphObj)) { return; } OutParams.AssociatedPin1 = InputPin; OutParams.AssociatedPin2 = OutputPin; // Get the schema and grab the default color from it const UEdGraphSchema* Schema = GraphObj->GetSchema(); OutParams.WireColor = Schema->GetPinTypeColor(OutputPin->PinType); bool bExecuted = false; USoundSubmixBase* InputSubmix = OutputPin ? CastChecked(OutputPin->GetOwningNode())->SoundSubmix : nullptr; USoundSubmixBase* OutputSubmix = InputPin ? CastChecked(InputPin->GetOwningNode())->SoundSubmix : nullptr; // Run through the predecessors, and on if (FExecPairingMap* PredecessorMap = PredecessorNodes.Find(OutputPin->GetOwningNode())) { if (FTimePair* Times = PredecessorMap->Find(InputPin->GetOwningNode())) { bExecuted = true; OutParams.WireThickness = ActiveWireThickness; OutParams.WireColor = Audio::GetColorForSubmixType(OutputSubmix); OutParams.bDrawBubbles = Audio::IsConnectionPerformingSoundfieldConversion(InputSubmix, OutputSubmix); } } if (!bExecuted) { OutParams.WireColor = Audio::GetColorForSubmixType(InputSubmix); OutParams.WireThickness = InactiveWireThickness; } } UEdGraphNode* FSoundSubmixGraphSchemaAction_NewNode::PerformAction(UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2f& Location, bool bSelectNewNode/* = true*/) { FSoundSubmixEditorUtilities::CreateSoundSubmix(ParentGraph, FromPin, FDeprecateSlateVector2D(Location), NewSoundSubmixName); return nullptr; } USoundSubmixGraphSchema::USoundSubmixGraphSchema(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } bool USoundSubmixGraphSchema::ConnectionCausesLoop(const UEdGraphPin* InputPin, const UEdGraphPin* OutputPin) const { USoundSubmixGraphNode* InputNode = CastChecked(InputPin->GetOwningNode()); USoundSubmixGraphNode* OutputNode = CastChecked(OutputPin->GetOwningNode()); // Master Submix cannot be an input as it would create an inferred loop for submixes without an explicit parent if (const UAudioSettings* Settings = GetDefault()) { if (USoundSubmix* MasterSubmix = Cast(Settings->MasterSubmix.TryLoad())) { if (OutputNode->SoundSubmix == MasterSubmix) { return true; } if (SubmixUtils::FindInGraph(MasterSubmix, OutputNode->SoundSubmix, false)) { return true; } } } return SubmixUtils::FindInGraph(OutputNode->SoundSubmix, InputNode->SoundSubmix, false); } void USoundSubmixGraphSchema::GetAssetsGraphHoverMessage(const TArray& Assets, const UEdGraph* HoverGraph, FString& OutTooltipText, bool& OutOkIcon) const { OutOkIcon = true; OutTooltipText = TEXT("Add Submix to Graph."); if (HoverGraph) { TArray SubmixNodes; HoverGraph->GetNodesOfClass(SubmixNodes); for (USoundSubmixGraphNode* SubmixNode : SubmixNodes) { if (SubmixNode && SubmixNode->SoundSubmix) { auto MatchesSubmix = [&](const FAssetData& Asset) { return Asset.GetFullName() == SubmixNode->SoundSubmix->GetFullName(); }; if (Assets.ContainsByPredicate(MatchesSubmix)) { OutOkIcon = false; OutTooltipText = TEXT("Selected asset or assets already in graph."); break; } } } } for (const FAssetData& Data : Assets) { if (!Data.IsInstanceOf(USoundSubmixBase::StaticClass())) { OutOkIcon = false; OutTooltipText = TEXT("Asset(s) must all be Submixes."); break; } } } void USoundSubmixGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const { const FText Name = LOCTEXT("NewSoundSubmix", "New Sound Submix"); const FText ToolTip = LOCTEXT("NewSoundSubmixTooltip", "Create a new sound submix"); TSharedPtr NewAction(new FSoundSubmixGraphSchemaAction_NewNode(FText::GetEmpty(), Name, ToolTip, 0)); ContextMenuBuilder.AddAction(NewAction); } void USoundSubmixGraphSchema::GetContextMenuActions(UToolMenu* Menu, UGraphNodeContextMenuContext* Context) const { if (Context->Node) { const USoundSubmixGraphNode* SoundGraphNode = Cast(Context->Node); { FToolMenuSection& Section = Menu->AddSection("SoundSubmixGraphSchemaNodeActions", LOCTEXT("ClassActionsMenuHeader", "SoundSubmix Actions")); Section.AddMenuEntry(FGraphEditorCommands::Get().BreakNodeLinks); Section.AddMenuEntry(FGenericCommands::Get().Delete); } } // No Super call so Node comments option is not shown } const FPinConnectionResponse USoundSubmixGraphSchema::CanCreateConnection(const UEdGraphPin* PinA, const UEdGraphPin* PinB) const { // Make sure the pins are not on the same node if (PinA->GetOwningNode() == PinB->GetOwningNode()) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("ConnectionSameNode", "Both are on the same node")); } // Compare the directions const UEdGraphPin* InputPin = nullptr; const UEdGraphPin* OutputPin = nullptr; if (!CategorizePinsByDirection(PinA, PinB, /*out*/ InputPin, /*out*/ OutputPin)) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("ConnectionIncompatible", "Directions are not compatible")); } // Note- are input pin and output pin swapped here? Am I losing it? USoundSubmixBase* InputSubmix = CastChecked(OutputPin->GetOwningNode())->SoundSubmix; USoundSubmixBase* OutputSubmix = CastChecked(InputPin->GetOwningNode())->SoundSubmix; // Forbid connecting dynamic submixes to other submixes. if (InputSubmix->IsDynamic( false /*bIncludeAncestors*/ )) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("SubmixIsDynamic", "Submix you are trying to connect from is dynamic and shouldn't have any static parents")); } // Check to see if this is an endpoint submix. if (!InputSubmix->IsA()) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("SubmixIsEndpoint", "Submix you are trying to connect from is an endpoint.")); } // If we're trying to make a connection between two soundfield submixes, ensure that we can transcode between the two. if (InputSubmix->IsA() && (OutputSubmix->IsA() || OutputSubmix->IsA())) { USoundfieldSubmix* InputSoundfieldSubmix = Cast(InputSubmix); USoundfieldSubmix* OutputSoundfieldSubmix = Cast(OutputSubmix); ISoundfieldFactory* InputFactory = InputSoundfieldSubmix->GetSoundfieldFactoryForSubmix(); ISoundfieldFactory* OutputFactory = nullptr; const USoundfieldEncodingSettingsBase* InputEncodingSettings = InputSoundfieldSubmix->GetEncodingSettings(); const USoundfieldEncodingSettingsBase* OutputEncodingSettings = nullptr; if (!OutputSoundfieldSubmix) { USoundfieldEndpointSubmix* SoundfieldEndpointSubmixB = CastChecked(OutputSubmix); OutputFactory = SoundfieldEndpointSubmixB->GetSoundfieldEndpointForSubmix(); OutputEncodingSettings = SoundfieldEndpointSubmixB->GetEncodingSettings(); } else { OutputFactory = OutputSoundfieldSubmix->GetSoundfieldFactoryForSubmix(); OutputEncodingSettings = OutputSoundfieldSubmix->GetEncodingSettings(); } if (InputFactory && OutputFactory) { if (!InputEncodingSettings) { InputEncodingSettings = InputFactory->GetDefaultEncodingSettings(); } if (!InputEncodingSettings) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("SoundfieldSubmixSourceIsInvalid", "Submix you are trying to connect from does not specify default settings. Please implement ISoundfieldFactory::GetDefaultEncodingSettings.")); } TUniquePtr InputEncodingSettingsProxy = InputEncodingSettings->GetProxy(); if (!InputEncodingSettingsProxy) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("SourceSoundfieldEncodingSettingsAreInvalid", "Submix you are trying to connect from failed to generate a proxy of it's settings. Please check USoundfieldEncodingSettingsBase::GetProxy().")); } if (!OutputEncodingSettings) { OutputEncodingSettings = OutputFactory->GetDefaultEncodingSettings(); } if (!OutputEncodingSettings) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("SoundfieldSubmixDestIsInvalid", "Submix you are trying to connect to does not specify default settings. Please implement ISoundfieldFactory::GetDefaultEncodingSettings.")); } TUniquePtr OutputEncodingSettingsProxy = OutputEncodingSettings->GetProxy(); if (!OutputEncodingSettingsProxy) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("DestSoundfieldEncodingSettingsAreInvalid", "Submix you are trying to connect to failed to generate a proxy of it's settings. Please check USoundfieldEncodingSettingsBase::GetProxy().")); } const bool bAreSoundfieldsCompatible = InputFactory->CanTranscodeToSoundfieldFormat(OutputFactory->GetSoundfieldFormatName(), *OutputEncodingSettingsProxy) || OutputFactory->CanTranscodeFromSoundfieldFormat(InputFactory->GetSoundfieldFormatName(), *InputEncodingSettingsProxy); if (!bAreSoundfieldsCompatible) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("SoundfieldSubmixesAreIncompatible", "These two submixes have incompatible types.")); } } } if (ConnectionCausesLoop(InputPin, OutputPin)) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("ConnectionLoop", "Connection would cause loop")); } // Break existing connections on outputs only - multiple input connections are acceptable if (OutputPin->LinkedTo.Num() > 0) { ECanCreateConnectionResponse ReplyBreakInputs; if (OutputPin == PinA) { ReplyBreakInputs = CONNECT_RESPONSE_BREAK_OTHERS_A; } else { ReplyBreakInputs = CONNECT_RESPONSE_BREAK_OTHERS_B; } return FPinConnectionResponse(ReplyBreakInputs, LOCTEXT("ConnectionReplace", "Replace existing connections")); } return FPinConnectionResponse(CONNECT_RESPONSE_MAKE, FText::GetEmpty()); } bool USoundSubmixGraphSchema::TryCreateConnection(UEdGraphPin* PinA, UEdGraphPin* PinB) const { check(PinA); check(PinB); bool bModified = UEdGraphSchema::TryCreateConnection(PinA, PinB); if (bModified) { USoundSubmixGraph* Graph = CastChecked(PinA->GetOwningNode()->GetGraph()); Graph->LinkSoundSubmixes(); USoundSubmixBase* SubmixA = CastChecked(PinA->GetOwningNode())->SoundSubmix; USoundSubmixBase* SubmixB = CastChecked(PinB->GetOwningNode())->SoundSubmix; USoundSubmixWithParentBase* SubmixWithParentA = Cast(SubmixA); USoundSubmixWithParentBase* SubmixWithParentB = Cast(SubmixA); // If re-basing root, re-open editor. This will force the root to be the primary edited node if (Graph->GetRootSoundSubmix() == SubmixA && SubmixWithParentA && SubmixWithParentA->ParentSubmix != nullptr) { Graph->SetRootSoundSubmix(SubmixWithParentA->ParentSubmix); } else if (Graph->GetRootSoundSubmix() == SubmixB && SubmixWithParentB && SubmixWithParentB->ParentSubmix != nullptr) { Graph->SetRootSoundSubmix(SubmixWithParentB->ParentSubmix); } } return bModified; } bool USoundSubmixGraphSchema::ShouldHidePinDefaultValue(UEdGraphPin* Pin) const { return true; } FLinearColor USoundSubmixGraphSchema::GetPinTypeColor(const FEdGraphPinType& PinType) const { return Audio::GetColorForSubmixType(PinType.PinCategory); } void USoundSubmixGraphSchema::BreakNodeLinks(UEdGraphNode& TargetNode) const { Super::BreakNodeLinks(TargetNode); CastChecked(TargetNode.GetGraph())->LinkSoundSubmixes(); } void USoundSubmixGraphSchema::BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotifcation) const { const FScopedTransaction Transaction( NSLOCTEXT("UnrealEd", "GraphEd_BreakPinLinks", "Break Pin Links") ); Super::BreakPinLinks(TargetPin, bSendsNodeNotifcation); // if this would notify the node then we need to re-link sound classes if (bSendsNodeNotifcation) { if (USoundSubmixGraphNode* GraphNode = Cast(TargetPin.GetOwningNode())) { // If TargetPin is an input, We should break links to all child submixes of the submix that owns this pin. if (TargetPin.Direction == EEdGraphPinDirection::EGPD_Input) { // Iterate through all child submixes USoundSubmixBase* OutputSubmix = GraphNode->SoundSubmix; // Note: If we ever support multiple parents for submixes, this will need to be modified. for (USoundSubmixBase* InputSubmix : OutputSubmix->ChildSubmixes) { if (USoundSubmixWithParentBase* SubmixWithParent = Cast(InputSubmix)) { SubmixWithParent->ParentSubmix = nullptr; SubmixWithParent->PostEditChange(); } } OutputSubmix->ChildSubmixes.Reset(); OutputSubmix->PostEditChange(); } else if (TargetPin.Direction == EEdGraphPinDirection::EGPD_Output) { // If this is an output pin, break the connection between this submix and it's parent. USoundSubmixWithParentBase* InputSubmix = CastChecked(GraphNode->SoundSubmix); USoundSubmixBase* OutputSubmix = InputSubmix->ParentSubmix; check(OutputSubmix); OutputSubmix->ChildSubmixes.Remove(InputSubmix); InputSubmix->ParentSubmix = nullptr; OutputSubmix->PostEditChange(); InputSubmix->PostEditChange(); } } CastChecked(TargetPin.GetOwningNode()->GetGraph())->LinkSoundSubmixes(); } } void USoundSubmixGraphSchema::BreakSinglePinLink(UEdGraphPin* SourcePin, UEdGraphPin* TargetPin) const { const FScopedTransaction Transaction( NSLOCTEXT("UnrealEd", "GraphEd_BreakSinglePinLink", "Break Pin Link") ); Super::BreakSinglePinLink(SourcePin, TargetPin); // Compare the directions UEdGraphPin* InputPin = nullptr; UEdGraphPin* OutputPin = nullptr; if (!CategorizePinsByDirection(SourcePin, TargetPin, /*out*/ InputPin, /*out*/ OutputPin)) { return; } // Note- are input pin and output pin swapped here? Am I losing it? USoundSubmixBase* InputSubmix = CastChecked(OutputPin->GetOwningNode())->SoundSubmix; USoundSubmixBase* OutputSubmix = CastChecked(InputPin->GetOwningNode())->SoundSubmix; if (USoundSubmixWithParentBase* SubmixWithParent = CastChecked(InputSubmix)) { SubmixWithParent->ParentSubmix = nullptr; SubmixWithParent->PostEditChange(); } CastChecked(SourcePin->GetOwningNode()->GetGraph())->LinkSoundSubmixes(); } void USoundSubmixGraphSchema::DroppedAssetsOnGraph(const TArray& Assets, const FVector2f& GraphPosition, UEdGraph* Graph) const { check(GEditor); check(Graph); USoundSubmixGraph* SoundSubmixGraph = CastChecked(Graph); TSet Editors; TSet UndisplayedSubmixes; for (const FAssetData& Asset : Assets) { if (USoundSubmixBase* SoundSubmix = Cast(Asset.GetAsset())) { // Walk to the root submix USoundSubmixWithParentBase* SubmixWithParent = Cast(SoundSubmix); while (SubmixWithParent && SubmixWithParent->ParentSubmix != nullptr) { SoundSubmix = SubmixWithParent->ParentSubmix; SubmixWithParent = Cast(SoundSubmix); } if (!SoundSubmixGraph->IsSubmixDisplayed(SoundSubmix)) { UAssetEditorSubsystem* EditorSubsystem = GEditor->GetEditorSubsystem(); TArray SubmixEditors = EditorSubsystem->FindEditorsForAsset(SoundSubmix); for (IAssetEditorInstance* Editor : SubmixEditors) { if (Editor) { Editors.Add(Editor); } } UndisplayedSubmixes.Add(SoundSubmix); } } } if (UndisplayedSubmixes.Num() > 0) { const FScopedTransaction Transaction(LOCTEXT("SoundSubmixEditorDropSubmixes", "Sound Submix Editor: Drag and Drop Sound Submix")); for (IAssetEditorInstance* Editor : Editors) { check(Editor); FSoundSubmixEditor* SubmixEditor = static_cast(Editor); // Close editors with dropped (and undisplayed) submix branches as they are now displayed locally in this graph // (to avoid modification of multiple graph editors representing the same branch of submixes) if (SubmixEditor->GetGraph() != Graph) { Editor->CloseWindow(EAssetEditorCloseReason::AssetUnloadingOrInvalid); } } // If editor is this graph's editor, update editable objects and select dropped submixes. if (USoundSubmixBase* RootSubmix = SoundSubmixGraph->GetRootSoundSubmix()) { UAssetEditorSubsystem* EditorSubsystem = GEditor->GetEditorSubsystem(); if (IAssetEditorInstance* EditorInstance = EditorSubsystem->FindEditorForAsset(RootSubmix, false /* bFocusIfOpen */)) { FSoundSubmixEditor* SubmixEditor = static_cast(EditorInstance); SoundSubmixGraph->AddDroppedSoundSubmixes(UndisplayedSubmixes, GraphPosition.X, GraphPosition.Y); SubmixEditor->AddMissingEditableSubmixes(); SubmixEditor->SelectSubmixes(UndisplayedSubmixes); } } } } #undef LOCTEXT_NAMESPACE