diff --git a/Config/BaseFlow.ini b/Config/BaseFlow.ini index f42430df5..7ec2bf1d9 100644 --- a/Config/BaseFlow.ini +++ b/Config/BaseFlow.ini @@ -1,3 +1,5 @@ [CoreRedirects] +ClassRedirects=(OldName="/Script/Flow.FlowNode_CustomEvent",NewName="/Script/Flow.FlowNode_CustomInput") +PropertyRedirects=(OldName="FlowAsset.CustomEvents",NewName="CustomInputs") ++StructRedirects=(OldName="/Script/Flow.FlowBreakpoint",NewName="/Script/Flow.FlowPinTrait") ++PropertyRedirects=(OldName="FlowPinTrait.bHasBreakpoint",NewName="bAllowed") \ No newline at end of file diff --git a/Config/DefaultFlow.ini b/Config/DefaultFlow.ini index f42430df5..7ec2bf1d9 100644 --- a/Config/DefaultFlow.ini +++ b/Config/DefaultFlow.ini @@ -1,3 +1,5 @@ [CoreRedirects] +ClassRedirects=(OldName="/Script/Flow.FlowNode_CustomEvent",NewName="/Script/Flow.FlowNode_CustomInput") +PropertyRedirects=(OldName="FlowAsset.CustomEvents",NewName="CustomInputs") ++StructRedirects=(OldName="/Script/Flow.FlowBreakpoint",NewName="/Script/Flow.FlowPinTrait") ++PropertyRedirects=(OldName="FlowPinTrait.bHasBreakpoint",NewName="bAllowed") \ No newline at end of file diff --git a/Flow.uplugin b/Flow.uplugin index e8919641e..8328fd44e 100644 --- a/Flow.uplugin +++ b/Flow.uplugin @@ -1,15 +1,14 @@ { "FileVersion" : 3, - "Version" : 1.0, + "Version" : 1.5, "FriendlyName" : "Flow", "Description" : "Design-agnostic node editor for scripting game’s flow.", "Category" : "Gameplay", - "CreatedBy" : "Krzysztof Justyński", - "CreatedByURL" : "https://twitter.com/MothDoctor", + "CreatedByURL" : "https://github.com/MothCocoon/FlowGraph/graphs/contributors", "DocsURL" : "https://github.com/MothCocoon/FlowGraph/wiki", "MarketplaceURL" : "", "SupportURL": "https://discord.gg/zMtMQ2vUUa", - "EngineVersion": "4.26.0", + "EngineAssociation": "5.0", "EnabledByDefault" : true, "CanContainContent" : false, "IsBetaVersion" : false, @@ -26,5 +25,15 @@ "Type" : "Editor", "LoadingPhase" : "Default" } + ], + "Plugins": [ + { + "Name": "AssetSearch", + "Enabled": true + }, + { + "Name": "EditorScriptingUtilities", + "Enabled": true + } ] } \ No newline at end of file diff --git a/README.md b/README.md index 92374bc1c..2f4856bdf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Concept -Flow plug-in for Unreal Engine provides a graph editor tailored for scripting flow of events in virtual worlds. It's based on a decade of experience with designing and implementing narrative in video games. All we need here is simplicity. +Flow plug-in for Unreal Engine is a design-agnostic event node editor. It provides a graph editor tailored for scripting flow of events in virtual worlds. It's based on a decade of experience with designing and implementing narrative in video games. All we need here is simplicity. -It's a design-agnostic event node editor. +The aim of publishing it as open-source project is to let people tell great stories and construct immersive worlds easier. That allows us to enrich video game storytelling so we can inspire people and make our world a better place. ![Flow101](https://user-images.githubusercontent.com/5065057/103543817-6d924080-4e9f-11eb-87d9-15ab092c3875.png) @@ -15,13 +15,15 @@ It's a design-agnostic event node editor. * It's easy to include plugin in your own project, follow this short [Getting Started](https://github.com/MothCocoon/FlowGraph/wiki/Getting-Started) guide. ## In-depth video presentation -This 24-minute presentation breaks down the concept of the Flow Graph. It goes through everything written in this ReadMe but in greater detail. +This 24-minute presentation breaks down the concept of the Flow Graph. Trust me, you want to understand concept properly before diving into implementation. -[![Introducing Flow Graph for Unreal Engine](https://img.youtube.com/vi/Rj76JP1f-I4/0.jpg)](https://www.youtube.com/watch?v=Rj76JP1f-I4) +[![Introducing Flow Graph for Unreal Engine](https://img.youtube.com/vi/BAqhccgKx_k/0.jpg)](https://www.youtube.com/watch?v=BAqhccgKx_k) ## Acknowledgements -I feel it's important to mention that I didn't invent anything new here, with the Flow Graph. It's an old and proven concept. I'm just one of many developers who decided it would be crazy useful to adopt it for Unreal Engine. And this time, also to make it publically available as the open-source project. +I got an opportunity to work on something like the Flow Graph at Reikon Games. They shared my enthusiasm for providing the plugin as open source and as such allowed me to publish this work and keep expanding it as a personal project. Kudos, guys! +Reikon badly wanted to build a better tool for implementing game flow rather than level blueprints or existing Marketplace plug-ins. I was very much interested in this since the studio was just starting with the production of a new title. And we did exactly that, created a node editor dedicated to scripting game flow. Kudos to Dariusz Murawski - a programmer who spent a few months with me to establish the working system and editor. And who had to endure my never-ending feedback and requests. + +I feel it's important to mention that I didn't invent anything new here, with the Flow Graph. It's an old and proven concept. I'm just one of many developers who decided it would be crazy useful to adopt it for Unreal Engine. And this time, also to make it publicly available as an open-source project. * Such simple graph-based tools for scripting game screenplay are utilized for a long time. Traditionally, RPG games needed such tools as there a lot of stories, quests, dialogues. * The best narrative toolset I had the opportunity to work with is what CD Projekt RED built for The Witcher series. Sadly, you can't download the modding toolkit for The Witcher 2 - yeah, it was publically available for some time. Still... you can watch the GDC talk by Piotr Tomsiński on [Cinematic Dialogues in The Witcher 3: Wild Hunt](https://www.youtube.com/watch?v=chf3REzAjgI) - it includes a brief presentation how Quest and Dialogue editors look like. It wouldn't be possible to create such an amazing narrative game without this kind of toolset. I did miss that so much when I moved to the Unreal Engine... -* Finally got an opportunity to work on something like this at [Reikon Games](http://www.reikongames.com/). They badly wanted to build a better tool for implementing game flow than level blueprints or existing Marketplace plug-ins. I was very much interested in this since the studio was just starting with the production of the new title. And we did exactly that, created a node editor dedicated to scripting game flow. Kudos to Dariusz Murawski - a programmer who spent a few months with me to establish the working system and editor. And who had to endure my never-ending feedback and requests. * At some point I felt comfortable enough with programming editor tools so I decided to build my own version of such toolset, meant to be published as an open-source project. I am thankful to Reikon bosses they see no issues with me releasing Flow Graph, which is "obviously" similar to our internal tool in many ways. I mean, it's so simple concept of "single node representing a single game feature"... and it's based on the same UE4 node graph API. Some corporations might have an issue with that. diff --git a/Source/Flow/Flow.Build.cs b/Source/Flow/Flow.Build.cs index a1c5597a9..5896761f9 100644 --- a/Source/Flow/Flow.Build.cs +++ b/Source/Flow/Flow.Build.cs @@ -1,28 +1,38 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + using UnrealBuildTool; public class Flow : ModuleRules { - public Flow(ReadOnlyTargetRules Target) : base(Target) + public Flow(ReadOnlyTargetRules target) : base(target) { - PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new[] + { + "LevelSequence" + }); - PrivateDependencyModuleNames.AddRange(new[] + PrivateDependencyModuleNames.AddRange(new[] { - "Core", + "Core", "CoreUObject", - "DeveloperSettings", + "DeveloperSettings", "Engine", - "GameplayTags", - "LevelSequence", + "GameplayTags", "MovieScene", "MovieSceneTracks", - "Slate", - "SlateCore" - }); + "Slate", + "SlateCore" + }); - if (Target.Type == TargetType.Editor) - { - PublicDependencyModuleNames.Add("UnrealEd"); - } - } -} + if (target.Type == TargetType.Editor) + { + PublicDependencyModuleNames.AddRange(new[] + { + "MessageLog", + "UnrealEd" + }); + } + } +} \ No newline at end of file diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 3449b6ae5..c542f016c 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -1,25 +1,43 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowAsset.h" + +#include "FlowMessageLog.h" +#include "FlowModule.h" #include "FlowSettings.h" #include "FlowSubsystem.h" #include "Nodes/FlowNode.h" #include "Nodes/Route/FlowNode_CustomInput.h" #include "Nodes/Route/FlowNode_Start.h" -#include "Nodes/Route/FlowNode_Finish.h" #include "Nodes/Route/FlowNode_SubGraph.h" +#include "Engine/World.h" #include "Serialization/MemoryReader.h" #include "Serialization/MemoryWriter.h" +#if WITH_EDITOR +FString UFlowAsset::ValidationError_NodeClassNotAllowed = TEXT("Node class {0} is not allowed in this asset."); +#endif + UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bWorldBound(true) #if WITH_EDITOR , FlowGraph(nullptr) #endif + , AllowedNodeClasses({UFlowNode::StaticClass()}) + , AllowedInSubgraphNodeClasses({UFlowNode_SubGraph::StaticClass()}) + , bStartNodePlacedAsGhostNode(false) , TemplateAsset(nullptr) - , StartNode(nullptr) , FinishPolicy(EFlowFinishPolicy::Keep) { + if (!AssetGuid.IsValid()) + { + AssetGuid = FGuid::NewGuid(); + } + + ExpectedOwnerClass = UFlowSettings::Get()->GetDefaultExpectedOwnerClass(); } #if WITH_EDITOR @@ -48,22 +66,101 @@ void UFlowAsset::PostDuplicate(bool bDuplicateForPIE) if (!bDuplicateForPIE) { + AssetGuid = FGuid::NewGuid(); Nodes.Empty(); } } -EDataValidationResult UFlowAsset::IsDataValid(TArray& ValidationErrors) +EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) +{ + // validate nodes + for (const TPair& Node : Nodes) + { + if (Node.Value) + { + if (!IsNodeClassAllowed(Node.Value->GetClass())) + { + const FString ErrorMsg = FString::Format(*ValidationError_NodeClassNotAllowed, {*Node.Value->GetClass()->GetName()}); + MessageLog.Error(*ErrorMsg, Node.Value); + } + + Node.Value->ValidationLog.Messages.Empty(); + if (Node.Value->ValidateNode() == EDataValidationResult::Invalid) + { + MessageLog.Messages.Append(Node.Value->ValidationLog.Messages); + } + } + } + + return MessageLog.Messages.Num() > 0 ? EDataValidationResult::Invalid : EDataValidationResult::Valid; +} + +bool UFlowAsset::IsNodeClassAllowed(const UClass* FlowNodeClass) const { - for (const TPair& NodePair : Nodes) + if (FlowNodeClass == nullptr) + { + return false; + } + + UFlowNode* NodeDefaults = FlowNodeClass->GetDefaultObject(); + + // UFlowNode class limits which UFlowAsset class can use it + { + for (const UClass* DeniedAssetClass : NodeDefaults->DeniedAssetClasses) + { + if (DeniedAssetClass && GetClass()->IsChildOf(DeniedAssetClass)) + { + return false; + } + } + + if (NodeDefaults->AllowedAssetClasses.Num() > 0) + { + bool bAllowedInAsset = false; + for (const UClass* AllowedAssetClass : NodeDefaults->AllowedAssetClasses) + { + if (AllowedAssetClass && GetClass()->IsChildOf(AllowedAssetClass)) + { + bAllowedInAsset = true; + break; + } + } + if (!bAllowedInAsset) + { + return false; + } + } + } + + // UFlowAsset class can limit which UFlowNode classes can be used { - const EDataValidationResult Result = NodePair.Value->IsDataValid(ValidationErrors); - if (Result == EDataValidationResult::Invalid) + for (const UClass* DeniedNodeClass : DeniedNodeClasses) + { + if (DeniedNodeClass && FlowNodeClass->IsChildOf(DeniedNodeClass)) + { + return false; + } + } + + if (AllowedNodeClasses.Num() > 0) { - return EDataValidationResult::Invalid; + bool bAllowedInAsset = false; + for (const UClass* AllowedNodeClass : AllowedNodeClasses) + { + if (AllowedNodeClass && FlowNodeClass->IsChildOf(AllowedNodeClass)) + { + bAllowedInAsset = true; + break; + } + } + if (!bAllowedInAsset) + { + return false; + } } } - return EDataValidationResult::Valid; + return true; } TSharedPtr UFlowAsset::FlowGraphInterface = nullptr; @@ -99,11 +196,11 @@ void UFlowAsset::UnregisterNode(const FGuid& NodeGuid) HarvestNodeConnections(); MarkPackageDirty(); } -#endif void UFlowAsset::HarvestNodeConnections() { TMap Connections; + bool bGraphDirty = false; // last moment to remove invalid nodes for (auto NodeIt = Nodes.CreateIterator(); NodeIt; ++NodeIt) @@ -112,51 +209,159 @@ void UFlowAsset::HarvestNodeConnections() if (Pair.Value == nullptr) { NodeIt.RemoveCurrent(); + bGraphDirty = true; } } for (const TPair& Pair : Nodes) { UFlowNode* Node = Pair.Value; - Connections.Empty(); + TMap FoundConnections; for (const UEdGraphPin* ThisPin : Node->GetGraphNode()->Pins) { if (ThisPin->Direction == EGPD_Output && ThisPin->LinkedTo.Num() > 0) { - if (UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) + if (const UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) { const UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode(); - Connections.Add(ThisPin->PinName, FConnectedPin(LinkedNode->NodeGuid, LinkedPin->PinName)); + FoundConnections.Add(ThisPin->PinName, FConnectedPin(LinkedNode->NodeGuid, LinkedPin->PinName)); } } } -#if WITH_EDITOR - Node->SetFlags(RF_Transactional); - Node->Modify(); + // This check exists to ensure that we don't mark graph dirty, if none of connections changed + // Optimization: we need check it only until the first node would be marked dirty, as this already marks Flow Asset package dirty + if (bGraphDirty == false) + { + if (FoundConnections.Num() != Node->Connections.Num()) + { + bGraphDirty = true; + } + else + { + for (const TPair& FoundConnection : FoundConnections) + { + if (const FConnectedPin* OldConnection = Node->Connections.Find(FoundConnection.Key)) + { + if (FoundConnection.Value != *OldConnection) + { + bGraphDirty = true; + break; + } + } + else + { + bGraphDirty = true; + break; + } + } + } + } + + if (bGraphDirty) + { + Node->SetFlags(RF_Transactional); + Node->Modify(); + + Node->SetConnections(FoundConnections); + Node->PostEditChange(); + } + } +} #endif - Node->SetConnections(Connections); + +UFlowNode* UFlowAsset::GetDefaultEntryNode() const +{ + UFlowNode* FirstStartNode = nullptr; + + for (const TPair& Node : Nodes) + { + if (UFlowNode_Start* StartNode = Cast(Node.Value)) + { + if (StartNode->GetConnectedNodes().Num() > 0) + { + return StartNode; + } + else if (FirstStartNode == nullptr) + { + FirstStartNode = StartNode; + } + } + } + + // If none of the found start nodes have connections, fallback to the first start node we found + return FirstStartNode; +} #if WITH_EDITOR - Node->PostEditChange(); -#endif +void UFlowAsset::AddCustomInput(const FName& EventName) +{ + if (!CustomInputs.Contains(EventName)) + { + CustomInputs.Add(EventName); } } -UFlowNode* UFlowAsset::GetNode(const FGuid& Guid) const +void UFlowAsset::RemoveCustomInput(const FName& EventName) { - if (UFlowNode* Node = Nodes.FindRef(Guid)) + if (CustomInputs.Contains(EventName)) { - return Node; + CustomInputs.Remove(EventName); + } +} + +void UFlowAsset::AddCustomOutput(const FName& EventName) +{ + if (!CustomOutputs.Contains(EventName)) + { + CustomOutputs.Add(EventName); + } +} + +void UFlowAsset::RemoveCustomOutput(const FName& EventName) +{ + if (CustomOutputs.Contains(EventName)) + { + CustomOutputs.Remove(EventName); + } +} +#endif + +UFlowNode_CustomInput* UFlowAsset::TryFindCustomInputNodeByEventName(const FName& EventName) const +{ + for (UFlowNode_CustomInput* InputNode : CustomInputNodes) + { + if (IsValid(InputNode) && InputNode->GetEventName() == EventName) + { + return InputNode; + } } return nullptr; } -void UFlowAsset::AddInstance(UFlowAsset* NewInstance) +TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) { - ActiveInstances.Add(NewInstance); + TArray FoundNodes; + GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); + + // filter out nodes by class + for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) + { + if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) + { + FoundNodes.RemoveAt(i); + } + } + FoundNodes.Shrink(); + + return FoundNodes; +} + +void UFlowAsset::AddInstance(UFlowAsset* Instance) +{ + ActiveInstances.Add(Instance); } int32 UFlowAsset::RemoveInstance(UFlowAsset* Instance) @@ -195,7 +400,7 @@ void UFlowAsset::ClearInstances() #if WITH_EDITOR void UFlowAsset::GetInstanceDisplayNames(TArray>& OutDisplayNames) const { - for (UFlowAsset* Instance : ActiveInstances) + for (const UFlowAsset* Instance : ActiveInstances) { OutDisplayNames.Emplace(MakeShareable(new FName(Instance->GetDisplayName()))); } @@ -222,9 +427,19 @@ void UFlowAsset::SetInspectedInstance(const FName& NewInspectedInstanceName) } } - BroadcastRegenerateToolbars(); + BroadcastDebuggerRefresh(); } -#endif + +void UFlowAsset::BroadcastDebuggerRefresh() const +{ + RefreshDebuggerEvent.Broadcast(); +} + +void UFlowAsset::BroadcastRuntimeMessageAdded(const TSharedRef& Message) +{ + RuntimeMessageEvent.Broadcast(this, Message); +} +#endif // WITH_EDITOR void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset* InTemplateAsset) { @@ -236,18 +451,11 @@ void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlow UFlowNode* NewNodeInstance = NewObject(this, Node.Value->GetClass(), NAME_None, RF_Transient, Node.Value, false, nullptr); Node.Value = NewNodeInstance; - // there can be only one, automatically added while creating graph - if (UFlowNode_Start* InNode = Cast(NewNodeInstance)) - { - StartNode = InNode; - } - if (UFlowNode_CustomInput* CustomInput = Cast(NewNodeInstance)) { - const FName& EventName = CustomInput->EventName; - if (!EventName.IsNone() && !CustomInputNodes.Contains(CustomInput->EventName)) + if (!CustomInput->EventName.IsNone()) { - CustomInputNodes.Emplace(CustomInput->EventName, CustomInput); + CustomInputNodes.Emplace(CustomInput); } } @@ -255,33 +463,22 @@ void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlow } } -void UFlowAsset::PreloadNodes() +void UFlowAsset::DeinitializeInstance() { - TArray GraphEntryNodes = {StartNode}; - for (const TPair& CustomEvent : CustomInputNodes) + for (const TPair& Node : Nodes) { - GraphEntryNodes.Emplace(CustomEvent.Value); + if (IsValid(Node.Value)) + { + Node.Value->DeinitializeInstance(); + } } - // NOTE: this is just the example algorithm of gathering nodes for pre-load - for (UFlowNode* EntryNode : GraphEntryNodes) + if (TemplateAsset) { - for (const TPair, int32>& Node : UFlowSettings::Get()->DefaultPreloadDepth) + const int32 ActiveInstancesLeft = TemplateAsset->RemoveInstance(this); + if (ActiveInstancesLeft == 0 && GetFlowSubsystem()) { - if (Node.Value > 0) - { - TArray FoundNodes; - UFlowNode::RecursiveFindNodesByClass(EntryNode, Node.Key, Node.Value, FoundNodes); - - for (UFlowNode* FoundNode : FoundNodes) - { - if (!PreloadedNodes.Contains(FoundNode)) - { - FoundNode->TriggerPreload(); - PreloadedNodes.Emplace(FoundNode); - } - } - } + GetFlowSubsystem()->RemoveInstancedTemplate(TemplateAsset); } } } @@ -293,11 +490,13 @@ void UFlowAsset::PreStartFlow() #if WITH_EDITOR if (TemplateAsset->ActiveInstances.Num() == 1) { + // this instance is the only active one, set it directly as Inspected Instance TemplateAsset->SetInspectedInstance(GetDisplayName()); } else { - TemplateAsset->BroadcastRegenerateToolbars(); + // request to refresh list to show newly created instance + TemplateAsset->BroadcastDebuggerRefresh(); } #endif } @@ -306,12 +505,14 @@ void UFlowAsset::StartFlow() { PreStartFlow(); - ensureAlways(StartNode); - RecordedNodes.Add(StartNode); - StartNode->TriggerFirstOutput(true); + if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) + { + RecordedNodes.Add(ConnectedEntryNode); + ConnectedEntryNode->TriggerFirstOutput(true); + } } -void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) +void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance /*= true*/) { FinishPolicy = InFinishPolicy; @@ -329,35 +530,68 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) } PreloadedNodes.Empty(); - // clear instance entries - const int32 ActiveInstancesLeft = TemplateAsset->RemoveInstance(this); - if (ActiveInstancesLeft == 0 && GetFlowSubsystem()) + // provides option to finish game-specific logic prior to removing asset instance + if (bRemoveInstance) { - GetFlowSubsystem()->RemoveInstancedTemplate(TemplateAsset); + DeinitializeInstance(); } } +bool UFlowAsset::HasStartedFlow() const +{ + return RecordedNodes.Num() > 0; +} + +AActor* UFlowAsset::TryFindActorOwner() const +{ + const UActorComponent* OwnerAsComponent = Cast(GetOwner()); + if (IsValid(OwnerAsComponent)) + { + return Cast(OwnerAsComponent->GetOwner()); + } + + return nullptr; +} + TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const { return ActiveSubGraphs.FindRef(SubGraphNode); } -void UFlowAsset::TriggerCustomEvent(UFlowNode_SubGraph* Node, const FName& EventName) +void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* Node, const FName& EventName) const { const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(Node); if (FlowInstance.IsValid()) { - if (UFlowNode_CustomInput* CustomEvent = FlowInstance->CustomInputNodes.FindRef(EventName)) + FlowInstance->TriggerCustomInput(EventName); + } +} + +void UFlowAsset::TriggerCustomInput(const FName& EventName) +{ + for (UFlowNode_CustomInput* CustomInput : CustomInputNodes) + { + if (CustomInput->EventName == EventName) { - RecordedNodes.Add(CustomEvent); - CustomEvent->TriggerFirstOutput(true); + RecordedNodes.Add(CustomInput); + CustomInput->ExecuteInput(EventName); } } } -void UFlowAsset::TriggerCustomOutput(const FName& EventName) const +void UFlowAsset::TriggerCustomOutput(const FName& EventName) { - NodeOwningThisAssetInstance->TriggerOutput(EventName); + if (NodeOwningThisAssetInstance.IsValid()) // it's a SubGraph + { + NodeOwningThisAssetInstance->TriggerOutput(EventName); + } + else // it's a Root Flow, so the intention here might be to call event on the Flow Component + { + if (UFlowComponent* FlowComponent = Cast(GetOwner())) + { + FlowComponent->OnTriggerRootFlowOutputEventDispatcher(this, EventName); + } + } } void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName) @@ -381,9 +615,16 @@ void UFlowAsset::FinishNode(UFlowNode* Node) ActiveNodes.Remove(Node); // if graph reached Finish and this asset instance was created by SubGraph node - if (Node->GetClass()->IsChildOf(UFlowNode_Finish::StaticClass()) && NodeOwningThisAssetInstance.IsValid()) + if (Node->CanFinishGraph()) { - NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); + if (NodeOwningThisAssetInstance.IsValid()) + { + NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); + } + else + { + FinishFlow(EFlowFinishPolicy::Keep); + } } } } @@ -413,31 +654,29 @@ UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const return NodeOwningThisAssetInstance.Get(); } -UFlowAsset* UFlowAsset::GetMasterInstance() const +UFlowAsset* UFlowAsset::GetParentInstance() const { return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; } -UFlowNode* UFlowAsset::GetNodeInstance(const FGuid Guid) const -{ - return Nodes.FindRef(Guid); -} - FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlowInstances) { FFlowAssetSaveData AssetRecord; - - //FSoftObjectPtr FlowAsset; - //FArchiveUObject::SerializeSoftObjectPtr(AssetRecord.AssetClass, FlowAsset); + AssetRecord.WorldName = IsBoundToWorld() ? GetWorld()->GetName() : FString(); AssetRecord.InstanceName = GetName(); + // opportunity to collect data before serializing asset OnSave(); - for (const TPair& Node : Nodes) + // iterate nodes + TArray NodesInExecutionOrder; + GetNodesInExecutionOrder(GetDefaultEntryNode(), NodesInExecutionOrder); + for (UFlowNode* Node : NodesInExecutionOrder) { - if (Node.Value && Node.Value->ActivationState == EFlowNodeState::Active) + if (Node && Node->ActivationState == EFlowNodeState::Active) { - if (UFlowNode_SubGraph* SubGraphNode = Cast(Node.Value)) + // iterate SubGraphs + if (UFlowNode_SubGraph* SubGraphNode = Cast(Node)) { const TWeakObjectPtr SubFlowInstance = GetFlowInstance(SubGraphNode); if (SubFlowInstance.IsValid()) @@ -448,17 +687,20 @@ FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlo } FFlowNodeSaveData NodeRecord; - Node.Value->SaveInstance(NodeRecord); + Node->SaveInstance(NodeRecord); AssetRecord.NodeRecords.Emplace(NodeRecord); } } + // serialize asset FMemoryWriter MemoryWriter(AssetRecord.AssetData, true); FFlowArchive Ar(MemoryWriter); Serialize(Ar); + // write archive to SaveGame SavedFlowInstances.Emplace(AssetRecord); + return AssetRecord; } @@ -470,17 +712,32 @@ void UFlowAsset::LoadInstance(const FFlowAssetSaveData& AssetRecord) PreStartFlow(); - for (const FFlowNodeSaveData& NodeRecord : AssetRecord.NodeRecords) + // iterate graph "from the end", backward to execution order + // prevents issue when the preceding node would instantly fire output to a not-yet-loaded node + for (int32 i = AssetRecord.NodeRecords.Num() - 1; i >= 0; i--) { - if (UFlowNode* Node = Nodes.FindRef(NodeRecord.NodeGuid)) + if (UFlowNode* Node = Nodes.FindRef(AssetRecord.NodeRecords[i].NodeGuid)) { - Node->LoadInstance(NodeRecord); + Node->LoadInstance(AssetRecord.NodeRecords[i]); } } OnLoad(); } +void UFlowAsset::OnActivationStateLoaded(UFlowNode* Node) +{ + if (Node->ActivationState != EFlowNodeState::NeverActivated) + { + RecordedNodes.Emplace(Node); + } + + if (Node->ActivationState == EFlowNodeState::Active) + { + ActiveNodes.Emplace(Node); + } +} + void UFlowAsset::OnSave_Implementation() { } @@ -488,3 +745,55 @@ void UFlowAsset::OnSave_Implementation() void UFlowAsset::OnLoad_Implementation() { } + +bool UFlowAsset::IsBoundToWorld_Implementation() +{ + return bWorldBound; +} + +#if WITH_EDITOR +void UFlowAsset::LogError(const FString& MessageToLog, UFlowNode* Node) +{ + // this is runtime log which is should be only called on runtime instances of asset + if (TemplateAsset == nullptr) + { + UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on template asset %s"), *MessageToLog); + } + + if (RuntimeLog.Get()) + { + const TSharedRef TokenizedMessage = RuntimeLog.Get()->Error(*MessageToLog, Node); + BroadcastRuntimeMessageAdded(TokenizedMessage); + } +} + +void UFlowAsset::LogWarning(const FString& MessageToLog, UFlowNode* Node) +{ + // this is runtime log which is should be only called on runtime instances of asset + if (TemplateAsset == nullptr) + { + UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on template asset %s"), *MessageToLog); + } + + if (RuntimeLog.Get()) + { + const TSharedRef TokenizedMessage = RuntimeLog.Get()->Warning(*MessageToLog, Node); + BroadcastRuntimeMessageAdded(TokenizedMessage); + } +} + +void UFlowAsset::LogNote(const FString& MessageToLog, UFlowNode* Node) +{ + // this is runtime log which is should be only called on runtime instances of asset + if (TemplateAsset == nullptr) + { + UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on template asset %s"), *MessageToLog); + } + + if (RuntimeLog.Get()) + { + const TSharedRef TokenizedMessage = RuntimeLog.Get()->Note(*MessageToLog, Node); + BroadcastRuntimeMessageAdded(TokenizedMessage); + } +} +#endif diff --git a/Source/Flow/Private/FlowComponent.cpp b/Source/Flow/Private/FlowComponent.cpp index 813707933..7864a6461 100644 --- a/Source/Flow/Private/FlowComponent.cpp +++ b/Source/Flow/Private/FlowComponent.cpp @@ -1,10 +1,15 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowComponent.h" #include "FlowAsset.h" +#include "FlowModule.h" #include "FlowSettings.h" #include "FlowSubsystem.h" +#include "Engine/Engine.h" #include "Engine/GameInstance.h" +#include "Engine/ViewportStatsSubsystem.h" #include "Engine/World.h" #include "Net/UnrealNetwork.h" #include "Serialization/MemoryReader.h" @@ -67,7 +72,7 @@ void UFlowComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - FlowSubsystem->FinishRootFlow(this, EFlowFinishPolicy::Keep); + FlowSubsystem->FinishAllRootFlows(this, EFlowFinishPolicy::Keep); FlowSubsystem->UnregisterComponent(this); } @@ -206,6 +211,43 @@ void UFlowComponent::OnRep_RemovedIdentityTags() } } +void UFlowComponent::VerifyIdentityTags() const +{ + if (IdentityTags.IsEmpty() && UFlowSettings::Get()->bWarnAboutMissingIdentityTags) + { + FString Message = TEXT("Missing Identity Tags on the Flow Component creating Flow Asset instance! This gonna break loading SaveGame for this component!"); + Message.Append(LINE_TERMINATOR).Append(TEXT("If you're not using SaveSystem, you can silence this warning by unchecking bWarnAboutMissingIdentityTags flag in Flow Settings.")); + LogError(Message); + } +} + +void UFlowComponent::LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType) const +{ + Message += TEXT(" --- Flow Component in actor ") + GetOwner()->GetName(); + + if (OnScreenMessageType == EFlowOnScreenMessageType::Permanent) + { + if (GetWorld()) + { + if (UViewportStatsSubsystem* StatsSubsystem = GetWorld()->GetSubsystem()) + { + StatsSubsystem->AddDisplayDelegate([this, Message](FText& OutText, FLinearColor& OutColor) + { + OutText = FText::FromString(Message); + OutColor = FLinearColor::Red; + return IsValid(this); + }); + } + } + } + else + { + GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, Message); + } + + UE_LOG(LogFlow, Error, TEXT("%s"), *Message); +} + void UFlowComponent::NotifyGraph(const FGameplayTag NotifyTag, const EFlowNetMode NetMode /* = EFlowNetMode::Authority*/) { if (IsFlowNetMode(NetMode) && NotifyTag.IsValid() && HasBegunPlay()) @@ -235,7 +277,7 @@ void UFlowComponent::BulkNotifyGraph(const FGameplayTagContainer NotifyTags, con { // save recently notify, this allow for the retroactive check in nodes // if retroactive check wouldn't be performed, this is only used by the network replication - RecentlySentNotifyTags = NotifyTags; + RecentlySentNotifyTags = ValidatedTags; OnRep_SentNotifyTags(); } @@ -265,14 +307,14 @@ void UFlowComponent::NotifyFromGraph(const FGameplayTagContainer& NotifyTags, co if (ValidatedTags.Num() > 0) { - for (const FGameplayTag& NotifyTag : NotifyTags) + for (const FGameplayTag& ValidatedTag : ValidatedTags) { - ReceiveNotify.Broadcast(nullptr, NotifyTag); + ReceiveNotify.Broadcast(nullptr, ValidatedTag); } if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) { - NotifyTagsFromGraph = NotifyTags; + NotifyTagsFromGraph = ValidatedTags; } } } @@ -290,9 +332,9 @@ void UFlowComponent::NotifyActor(const FGameplayTag ActorTag, const FGameplayTag { if (IsFlowNetMode(NetMode) && NotifyTag.IsValid() && HasBegunPlay()) { - if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + if (const UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - for (TWeakObjectPtr& Component : FlowSubsystem->GetComponents(ActorTag)) + for (const TWeakObjectPtr& Component : FlowSubsystem->GetComponents(ActorTag)) { Component->ReceiveNotify.Broadcast(this, NotifyTag); } @@ -308,11 +350,11 @@ void UFlowComponent::NotifyActor(const FGameplayTag ActorTag, const FGameplayTag void UFlowComponent::OnRep_NotifyTagsFromAnotherComponent() { - if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + if (const UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { for (const FNotifyTagReplication& Notify : NotifyTagsFromAnotherComponent) { - for (TWeakObjectPtr& Component : FlowSubsystem->GetComponents(Notify.ActorTag)) + for (const TWeakObjectPtr& Component : FlowSubsystem->GetComponents(Notify.ActorTag)) { Component->ReceiveNotify.Broadcast(this, Notify.NotifyTag); } @@ -326,29 +368,53 @@ void UFlowComponent::StartRootFlow() { if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { + VerifyIdentityTags(); + FlowSubsystem->StartRootFlow(this, RootFlow, bAllowMultipleInstances); } } } -void UFlowComponent::FinishRootFlow(const EFlowFinishPolicy FinishPolicy) +void UFlowComponent::FinishRootFlow(UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy) { if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - FlowSubsystem->FinishRootFlow(this, FinishPolicy); + FlowSubsystem->FinishRootFlow(this, TemplateAsset, FinishPolicy); } } -UFlowAsset* UFlowComponent::GetRootFlowInstance() +TSet UFlowComponent::GetRootInstances(const UObject* Owner) const { - if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + const UObject* OwnerToCheck = IsValid(Owner) ? Owner : this; + + if (const UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + { + return FlowSubsystem->GetRootInstancesByOwner(OwnerToCheck); + } + + return TSet(); +} + +UFlowAsset* UFlowComponent::GetRootFlowInstance() const +{ + if (const UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - return FlowSubsystem->GetRootFlow(this); + const TSet Result = FlowSubsystem->GetRootInstancesByOwner(this); + if (Result.Num() > 0) + { + return Result.Array()[0]; + } } return nullptr; } +void UFlowComponent::OnTriggerRootFlowOutputEventDispatcher(UFlowAsset* RootFlowInstance, const FName& EventName) +{ + BP_OnTriggerRootFlowOutputEvent(RootFlowInstance, EventName); + OnTriggerRootFlowOutputEvent(RootFlowInstance, EventName); +} + void UFlowComponent::SaveRootFlow(TArray& SavedFlowInstances) { if (UFlowAsset* FlowAssetInstance = GetRootFlowInstance()) @@ -365,6 +431,8 @@ void UFlowComponent::LoadRootFlow() { if (RootFlow && !SavedAssetInstanceName.IsEmpty() && GetFlowSubsystem()) { + VerifyIdentityTags(); + GetFlowSubsystem()->LoadRootFlow(this, RootFlow, SavedAssetInstanceName); SavedAssetInstanceName = FString(); } @@ -376,8 +444,10 @@ FFlowComponentSaveData UFlowComponent::SaveInstance() ComponentRecord.WorldName = GetWorld()->GetName(); ComponentRecord.ActorInstanceName = GetOwner()->GetName(); + // opportunity to collect data before serializing component OnSave(); + // serialize component FMemoryWriter MemoryWriter(ComponentRecord.ComponentData, true); FFlowArchive Ar(MemoryWriter); Serialize(Ar); diff --git a/Source/Flow/Private/FlowMessageLog.cpp b/Source/Flow/Private/FlowMessageLog.cpp new file mode 100644 index 000000000..f4e1ef621 --- /dev/null +++ b/Source/Flow/Private/FlowMessageLog.cpp @@ -0,0 +1,87 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowMessageLog.h" +#include "Nodes/FlowNode.h" +#include "FlowAsset.h" + +#if WITH_EDITOR + +#define LOCTEXT_NAMESPACE "FlowMessageLog" + +const FName FFlowMessageLog::LogName(TEXT("FlowGraph")); + +FFlowGraphToken::FFlowGraphToken(const UFlowAsset* InFlowAsset) +{ + CachedText = FText::FromString(InFlowAsset->GetClass()->GetPathName()); +} + +FFlowGraphToken::FFlowGraphToken(const UFlowNode* InFlowNode) + : GraphNode(InFlowNode->GetGraphNode()) +{ + CachedText = InFlowNode->GetNodeTitle(); +} + +FFlowGraphToken::FFlowGraphToken(UEdGraphNode* InGraphNode, const UEdGraphPin* InPin) + : GraphNode(InGraphNode) + , GraphPin(InPin) +{ + if (InPin) + { + CachedText = InPin->GetDisplayName(); + if (CachedText.IsEmpty()) + { + CachedText = LOCTEXT("UnnamedPin", ""); + } + } + else + { + CachedText = GraphNode->GetNodeTitle(ENodeTitleType::ListView); + } +} + +TSharedPtr FFlowGraphToken::Create(const UFlowAsset* InFlowAsset, FTokenizedMessage& Message) +{ + if (InFlowAsset) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InFlowAsset))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +TSharedPtr FFlowGraphToken::Create(const UFlowNode* InFlowNode, FTokenizedMessage& Message) +{ + if (InFlowNode) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InFlowNode))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +TSharedPtr FFlowGraphToken::Create(UEdGraphNode* InGraphNode, FTokenizedMessage& Message) +{ + if (InGraphNode) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InGraphNode, nullptr))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +TSharedPtr FFlowGraphToken::Create(const UEdGraphPin* InPin, FTokenizedMessage& Message) +{ + if (InPin && InPin->GetOwningNode()) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InPin->GetOwningNode(), InPin))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +#undef LOCTEXT_NAMESPACE +#endif // WITH_EDITOR diff --git a/Source/Flow/Private/FlowModule.cpp b/Source/Flow/Private/FlowModule.cpp index 0b8a9a35d..4c202ca89 100644 --- a/Source/Flow/Private/FlowModule.cpp +++ b/Source/Flow/Private/FlowModule.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowModule.h" #include "Modules/ModuleManager.h" diff --git a/Source/Flow/Private/FlowOwnerFunctionParams.cpp b/Source/Flow/Private/FlowOwnerFunctionParams.cpp new file mode 100644 index 000000000..5c4af749e --- /dev/null +++ b/Source/Flow/Private/FlowOwnerFunctionParams.cpp @@ -0,0 +1,60 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowOwnerFunctionParams.h" +#include "Nodes/FlowNode.h" +#include "Nodes/World/FlowNode_CallOwnerFunction.h" + + +// UFlowOwnerFunctionParams Implementation + +UFlowOwnerFunctionParams::UFlowOwnerFunctionParams() + : Super() +{ +#if WITH_EDITOR + InputNames.Add(UFlowNode::DefaultInputPin.PinName); + OutputNames.Add(UFlowNode::DefaultOutputPin.PinName); +#endif //WITH_EDITOR +} + +void UFlowOwnerFunctionParams::PreExecute(UFlowNode_CallOwnerFunction& InSourceNode, const FName& InputPinName) +{ + SourceNode = &InSourceNode; + ExecutedInputPinName = InputPinName; + + BP_PreExecute(); +} + +void UFlowOwnerFunctionParams::PostExecute() +{ + BP_PostExecute(); + + SourceNode = nullptr; + ExecutedInputPinName = NAME_None; +} + +bool UFlowOwnerFunctionParams::ShouldFinishForOutputName_Implementation(const FName& OutputName) const +{ + // Blueprint subclasses may want to declare certain outputs as "non-finishing" + // but by default, all outputs are 'finishing' + return true; +} + +TArray UFlowOwnerFunctionParams::BP_GetInputNames() const +{ + if (IsValid(SourceNode)) + { + return SourceNode->GetInputNames(); + } + + return TArray(); +} + +TArray UFlowOwnerFunctionParams::BP_GetOutputNames() const +{ + if (IsValid(SourceNode)) + { + return SourceNode->GetOutputNames(); + } + + return TArray(); +} diff --git a/Source/Flow/Private/FlowOwnerFunctionRef.cpp b/Source/Flow/Private/FlowOwnerFunctionRef.cpp new file mode 100644 index 000000000..d49bfbf2a --- /dev/null +++ b/Source/Flow/Private/FlowOwnerFunctionRef.cpp @@ -0,0 +1,87 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowOwnerFunctionRef.h" +#include "FlowOwnerFunctionParams.h" +#include "FlowModule.h" +#include "FlowOwnerInterface.h" + +#include "UObject/Class.h" +#include "Logging/LogMacros.h" + + +// FFlowOwnerFunctionRef Implementation + +UFunction* FFlowOwnerFunctionRef::TryResolveFunction(const UClass& InClass) +{ + if (IsConfigured()) + { + Function = InClass.FindFunctionByName(FunctionName); + } + else + { + Function = nullptr; + } + + return Function; +} + +FName FFlowOwnerFunctionRef::CallFunction(IFlowOwnerInterface& InFlowOwnerInterface, UFlowOwnerFunctionParams& InParams) const +{ + if (!IsResolved()) + { + const UObject* FlowOwnerObject = CastChecked(&InFlowOwnerInterface); + + UE_LOG( + LogFlow, + Error, + TEXT("Could not resolve function named %s with flow owner class %s"), + *FunctionName.ToString(), + *FlowOwnerObject->GetClass()->GetName()); + + return NAME_None; + } + + UObject* FlowOwnerObject = CastChecked(&InFlowOwnerInterface); + + struct FFlowOwnerFunctionRef_Parms + { + // Single FunctionParams object parameter + UFlowOwnerFunctionParams* Params; + + // Return value + FName OutputPinName; + }; + + FFlowOwnerFunctionRef_Parms Parms = { &InParams, NAME_None }; + + // Call the owner function itself + FlowOwnerObject->ProcessEvent(Function, &Parms); + + // Ensure the return value is valid + if (!Parms.OutputPinName.IsNone()) + { + const TArray OutputNames = InParams.GatherOutputNames(); + + if (!OutputNames.Contains(Parms.OutputPinName)) + { + FString OutputNamesStr = TEXT("None"); + for (const FName& OutputName : OutputNames) + { + OutputNamesStr += TEXT(", ") + OutputName.ToString(); + } + + UE_LOG( + LogFlow, + Error, + TEXT("Flow Owner Function %s returned an invalid OutputPinName '%s', which is not in the valid outputs: { %s }"), + *FunctionName.ToString(), + *Parms.OutputPinName.ToString(), + *OutputNamesStr); + + // Replace the invalid output pin name with None + Parms.OutputPinName = NAME_None; + } + } + + return Parms.OutputPinName; +} diff --git a/Source/Flow/Private/FlowSettings.cpp b/Source/Flow/Private/FlowSettings.cpp index f31c414ed..0e44fd35c 100644 --- a/Source/Flow/Private/FlowSettings.cpp +++ b/Source/Flow/Private/FlowSettings.cpp @@ -1,7 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowSettings.h" +#include "FlowComponent.h" UFlowSettings::UFlowSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , bCreateFlowSubsystemOnClients(true) + , bWarnAboutMissingIdentityTags(true) + , bLogOnSignalDisabled(true) + , bLogOnSignalPassthrough(true) + , bUseAdaptiveNodeTitles(false) + , DefaultExpectedOwnerClass(UFlowComponent::StaticClass()) +{ +} + +UClass* UFlowSettings::GetDefaultExpectedOwnerClass() const { + return CastChecked(TryResolveOrLoadSoftClass(DefaultExpectedOwnerClass), ECastCheckedType::NullAllowed); } + +UClass* UFlowSettings::TryResolveOrLoadSoftClass(const FSoftClassPath& SoftClassPath) +{ + if (UClass* Resolved = SoftClassPath.ResolveClass()) + { + return Resolved; + } + + return SoftClassPath.TryLoadClass(); +} \ No newline at end of file diff --git a/Source/Flow/Private/FlowSubsystem.cpp b/Source/Flow/Private/FlowSubsystem.cpp index 10e079aae..4f43e7250 100644 --- a/Source/Flow/Private/FlowSubsystem.cpp +++ b/Source/Flow/Private/FlowSubsystem.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowSubsystem.h" #include "FlowAsset.h" @@ -9,8 +11,16 @@ #include "Engine/GameInstance.h" #include "Engine/World.h" -#include "Misc/FileHelper.h" +#include "Logging/MessageLog.h" #include "Misc/Paths.h" +#include "UObject/UObjectHash.h" + +#if WITH_EDITOR +FNativeFlowAssetEvent UFlowSubsystem::OnInstancedTemplateAdded; +FNativeFlowAssetEvent UFlowSubsystem::OnInstancedTemplateRemoved; +#endif + +#define LOCTEXT_NAMESPACE "FlowSubsystem" UFlowSubsystem::UFlowSubsystem() : UGameInstanceSubsystem() @@ -19,13 +29,21 @@ UFlowSubsystem::UFlowSubsystem() bool UFlowSubsystem::ShouldCreateSubsystem(UObject* Outer) const { + // Only create an instance if there is no override implementation defined elsewhere + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + if (ChildClasses.Num() > 0) + { + return false; + } + // in this case, we simply create subsystem for every instance of the game if (UFlowSettings::Get()->bCreateFlowSubsystemOnClients) { return true; } - return Outer->GetWorld()->GetNetMode() < NM_Client && Outer->GetWorld()->IsServer(); + return Outer->GetWorld()->GetNetMode() < NM_Client; } void UFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection) @@ -52,23 +70,37 @@ void UFlowSubsystem::AbortActiveFlows() InstancedTemplates.Empty(); InstancedSubFlows.Empty(); + + RootInstances.Empty(); } void UFlowSubsystem::StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances /* = true */) { - UFlowAsset* NewFlow = CreateRootFlow(Owner, FlowAsset, bAllowMultipleInstances); - if (NewFlow) + if (FlowAsset) + { + if (UFlowAsset* NewFlow = CreateRootFlow(Owner, FlowAsset, bAllowMultipleInstances)) + { + NewFlow->StartFlow(); + } + } +#if WITH_EDITOR + else { - NewFlow->StartFlow(); + FMessageLog("PIE").Error(LOCTEXT("StartRootFlowNullAsset", "Attempted to start Root Flow with a null asset.")) + ->AddToken(FUObjectToken::Create(Owner)); } +#endif } UFlowAsset* UFlowSubsystem::CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances) { - if (RootInstances.Contains(Owner)) + for (const TPair>& RootInstance : RootInstances) { - UE_LOG(LogFlow, Warning, TEXT("Attempted to start Root Flow for the same Owner again. Owner: %s. Flow Asset: %s."), *Owner->GetName(), *FlowAsset->GetName()); - return nullptr; + if (Owner == RootInstance.Value.Get() && FlowAsset == RootInstance.Key->GetTemplateAsset()) + { + UE_LOG(LogFlow, Warning, TEXT("Attempted to start Root Flow for the same Owner again. Owner: %s. Flow Asset: %s."), *Owner->GetName(), *FlowAsset->GetName()); + return nullptr; + } } if (!bAllowMultipleInstances && InstancedTemplates.Contains(FlowAsset)) @@ -78,45 +110,86 @@ UFlowAsset* UFlowSubsystem::CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset } UFlowAsset* NewFlow = CreateFlowInstance(Owner, FlowAsset); - RootInstances.Add(Owner, NewFlow); + if (NewFlow) + { + RootInstances.Add(NewFlow, Owner); + } return NewFlow; } -void UFlowSubsystem::FinishRootFlow(UObject* Owner, const EFlowFinishPolicy FinishPolicy) +void UFlowSubsystem::FinishRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy) { - if (UFlowAsset* Instance = RootInstances.FindRef(Owner)) + UFlowAsset* InstanceToFinish = nullptr; + + for (TPair>& RootInstance : RootInstances) { - RootInstances.Remove(Owner); - Instance->FinishFlow(FinishPolicy); + if (Owner && Owner == RootInstance.Value.Get() && RootInstance.Key && RootInstance.Key->GetTemplateAsset() == TemplateAsset) + { + InstanceToFinish = RootInstance.Key; + break; + } + } + + if (InstanceToFinish) + { + RootInstances.Remove(InstanceToFinish); + InstanceToFinish->FinishFlow(FinishPolicy); + } +} + +void UFlowSubsystem::FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy) +{ + TArray InstancesToFinish; + + for (TPair>& RootInstance : RootInstances) + { + if (Owner && Owner == RootInstance.Value.Get() && RootInstance.Key) + { + InstancesToFinish.Emplace(RootInstance.Key); + } + } + + for (UFlowAsset* InstanceToFinish : InstancesToFinish) + { + RootInstances.Remove(InstanceToFinish); + InstanceToFinish->FinishFlow(FinishPolicy); } } -UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString NewInstanceName, const bool bPreloading /* = false */) +UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString SavedInstanceName, const bool bPreloading /* = false */) { UFlowAsset* NewInstance = nullptr; if (!InstancedSubFlows.Contains(SubGraphNode)) { const TWeakObjectPtr Owner = SubGraphNode->GetFlowAsset() ? SubGraphNode->GetFlowAsset()->GetOwner() : nullptr; - NewInstance = CreateFlowInstance(Owner, SubGraphNode->Asset, NewInstanceName); - InstancedSubFlows.Add(SubGraphNode, NewInstance); + NewInstance = CreateFlowInstance(Owner, SubGraphNode->Asset, SavedInstanceName); - if (bPreloading) + if (NewInstance) { - NewInstance->PreloadNodes(); + InstancedSubFlows.Add(SubGraphNode, NewInstance); + + if (bPreloading) + { + NewInstance->PreloadNodes(); + } } } - if (!bPreloading) + if (InstancedSubFlows.Contains(SubGraphNode) && !bPreloading) { // get instanced asset from map - in case it was already instanced by calling CreateSubFlow() with bPreloading == true UFlowAsset* AssetInstance = InstancedSubFlows[SubGraphNode]; - + AssetInstance->NodeOwningThisAssetInstance = SubGraphNode; SubGraphNode->GetFlowAsset()->ActiveSubGraphs.Add(SubGraphNode, AssetInstance); - - AssetInstance->StartFlow(); + + // don't activate Start Node if we're loading Sub Graph from SaveGame + if (SavedInstanceName.IsEmpty()) + { + AssetInstance->StartFlow(); + } } return NewInstance; @@ -138,53 +211,93 @@ void UFlowSubsystem::RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlow UFlowAsset* UFlowSubsystem::CreateFlowInstance(const TWeakObjectPtr Owner, TSoftObjectPtr FlowAsset, FString NewInstanceName) { - check(!FlowAsset.IsNull()); - - if (FlowAsset.IsPending()) + UFlowAsset* LoadedFlowAsset = FlowAsset.LoadSynchronous(); + if (LoadedFlowAsset == nullptr) { - const FSoftObjectPath& AssetRef = FlowAsset.ToSoftObjectPath(); - FlowAsset = Cast(Streamable.LoadSynchronous(AssetRef, false)); + return nullptr; } - InstancedTemplates.Add(FlowAsset.Get()); + AddInstancedTemplate(LoadedFlowAsset); #if WITH_EDITOR if (GetWorld()->WorldType != EWorldType::Game) { // Fix connections - even in packaged game if assets haven't been re-saved in the editor after changing node's definition - FlowAsset.Get()->HarvestNodeConnections(); + LoadedFlowAsset->HarvestNodeConnections(); } #endif // it won't be empty, if we're restoring Flow Asset instance from the SaveGame if (NewInstanceName.IsEmpty()) { - NewInstanceName = FPaths::GetBaseFilename(FlowAsset.Get()->GetPathName()) + TEXT("_") + FString::FromInt(FlowAsset.Get()->GetInstancesNum()); + NewInstanceName = MakeUniqueObjectName(this, UFlowAsset::StaticClass(), *FPaths::GetBaseFilename(LoadedFlowAsset->GetPathName())).ToString(); } - UFlowAsset* NewInstance = NewObject(this, FlowAsset->GetClass(), *NewInstanceName, RF_Transient, FlowAsset.Get(), false, nullptr); - NewInstance->InitializeInstance(Owner, FlowAsset.Get()); + UFlowAsset* NewInstance = NewObject(this, LoadedFlowAsset->GetClass(), *NewInstanceName, RF_Transient, LoadedFlowAsset, false, nullptr); + NewInstance->InitializeInstance(Owner, LoadedFlowAsset); - FlowAsset.Get()->AddInstance(NewInstance); + LoadedFlowAsset->AddInstance(NewInstance); return NewInstance; } +void UFlowSubsystem::AddInstancedTemplate(UFlowAsset* Template) +{ + if (!InstancedTemplates.Contains(Template)) + { + InstancedTemplates.Add(Template); + +#if WITH_EDITOR + Template->RuntimeLog = MakeShareable(new FFlowMessageLog()); + OnInstancedTemplateAdded.ExecuteIfBound(Template); +#endif + } +} + void UFlowSubsystem::RemoveInstancedTemplate(UFlowAsset* Template) { +#if WITH_EDITOR + OnInstancedTemplateRemoved.ExecuteIfBound(Template); + Template->RuntimeLog.Reset(); +#endif + InstancedTemplates.Remove(Template); } TMap UFlowSubsystem::GetRootInstances() const { TMap Result; - for (const TPair, UFlowAsset*>& Pair : RootInstances) + for (const TPair>& RootInstance : RootInstances) + { + Result.Emplace(RootInstance.Value.Get(), RootInstance.Key); + } + return Result; +} + +TSet UFlowSubsystem::GetRootInstancesByOwner(const UObject* Owner) const +{ + TSet Result; + for (const TPair>& RootInstance : RootInstances) { - Result.Emplace(Pair.Key.Get(), Pair.Value); + if (Owner && RootInstance.Value == Owner) + { + Result.Emplace(RootInstance.Key); + } } return Result; } +UFlowAsset* UFlowSubsystem::GetRootFlow(const UObject* Owner) const +{ + const TSet Result = GetRootInstancesByOwner(Owner); + if (Result.Num() > 0) + { + return Result.Array()[0]; + } + + return nullptr; +} + UWorld* UFlowSubsystem::GetWorld() const { return GetGameInstance()->GetWorld(); @@ -192,47 +305,82 @@ UWorld* UFlowSubsystem::GetWorld() const void UFlowSubsystem::OnGameSaved(UFlowSaveGame* SaveGame) { - // save graphs with nodes - for (const TPair, UFlowAsset*>& Pair : RootInstances) + // clear existing data, in case we received reused SaveGame instance + // we only remove data for the current world + global Flow Graph instances (i.e. not bound to any world if created by UGameInstanceSubsystem) + // we keep data bound to other worlds + if (GetWorld()) { - if (Pair.Key.IsValid() && Pair.Value) + const FString& WorldName = GetWorld()->GetName(); + + for (int32 i = SaveGame->FlowInstances.Num() - 1; i >= 0; i--) + { + if (SaveGame->FlowInstances[i].WorldName.IsEmpty() || SaveGame->FlowInstances[i].WorldName == WorldName) + { + SaveGame->FlowInstances.RemoveAt(i); + } + } + + for (int32 i = SaveGame->FlowComponents.Num() - 1; i >= 0; i--) { - if (UFlowComponent* FlowComponent = Cast(Pair.Key)) + if (SaveGame->FlowComponents[i].WorldName.IsEmpty() || SaveGame->FlowComponents[i].WorldName == WorldName) + { + SaveGame->FlowComponents.RemoveAt(i); + } + } + } + + // save Flow Graphs + for (const TPair>& RootInstance : RootInstances) + { + if (RootInstance.Key && RootInstance.Value.IsValid()) + { + if (UFlowComponent* FlowComponent = Cast(RootInstance.Value)) { FlowComponent->SaveRootFlow(SaveGame->FlowInstances); } else { - Pair.Value->SaveInstance(SaveGame->FlowInstances); + RootInstance.Key->SaveInstance(SaveGame->FlowInstances); } } } - // save components - TArray> ComponentsArray; - FlowComponentRegistry.GenerateValueArray(ComponentsArray); - const TSet> FlowComponents = TSet>(ComponentsArray); - for (TWeakObjectPtr FlowComponent : FlowComponents) + // save Flow Components { - SaveGame->FlowComponents.Emplace(FlowComponent->SaveInstance()); + // retrieve all registered components + TArray> ComponentsArray; + FlowComponentRegistry.GenerateValueArray(ComponentsArray); + + // ensure uniqueness of entries + const TSet> RegisteredComponents = TSet>(ComponentsArray); + + // write archives to SaveGame + for (const TWeakObjectPtr RegisteredComponent : RegisteredComponents) + { + SaveGame->FlowComponents.Emplace(RegisteredComponent->SaveInstance()); + } } } void UFlowSubsystem::OnGameLoaded(UFlowSaveGame* SaveGame) { LoadedSaveGame = SaveGame; + + // here's opportunity to apply loaded data to custom systems + // it's recommended to do this by overriding method in the subclass } void UFlowSubsystem::LoadRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const FString& SavedAssetInstanceName) { - if (SavedAssetInstanceName.IsEmpty()) + if (FlowAsset == nullptr || SavedAssetInstanceName.IsEmpty()) { return; } for (const FFlowAssetSaveData& AssetRecord : LoadedSaveGame->FlowInstances) { - if (AssetRecord.InstanceName == SavedAssetInstanceName) + if (AssetRecord.InstanceName == SavedAssetInstanceName + && (FlowAsset->IsBoundToWorld() == false || AssetRecord.WorldName == GetWorld()->GetName())) { UFlowAsset* LoadedInstance = CreateRootFlow(Owner, FlowAsset, false); if (LoadedInstance) @@ -246,9 +394,17 @@ void UFlowSubsystem::LoadRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const F void UFlowSubsystem::LoadSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedAssetInstanceName) { + if (SubGraphNode->Asset.IsNull()) + { + return; + } + + UFlowAsset* SubGraphAsset = SubGraphNode->Asset.LoadSynchronous(); + for (const FFlowAssetSaveData& AssetRecord : LoadedSaveGame->FlowInstances) { - if (AssetRecord.InstanceName == SavedAssetInstanceName) + if (AssetRecord.InstanceName == SavedAssetInstanceName + && ((SubGraphAsset && SubGraphAsset->IsBoundToWorld() == false) || AssetRecord.WorldName == GetWorld()->GetName())) { UFlowAsset* LoadedInstance = CreateSubFlow(SubGraphNode, SavedAssetInstanceName); if (LoadedInstance) @@ -321,7 +477,7 @@ void UFlowSubsystem::UnregisterComponent(UFlowComponent* Component) void UFlowSubsystem::OnIdentityTagRemoved(UFlowComponent* Component, const FGameplayTag& RemovedTag) { - FlowComponentRegistry.Emplace(RemovedTag, Component); + FlowComponentRegistry.Remove(RemovedTag, Component); // broadcast OnComponentUnregistered only if this component isn't present in the registry anymore if (Component->IdentityTags.Num() > 0) @@ -352,10 +508,10 @@ void UFlowSubsystem::OnIdentityTagsRemoved(UFlowComponent* Component, const FGam } } -TSet UFlowSubsystem::GetFlowComponentsByTag(const FGameplayTag Tag, const TSubclassOf ComponentClass) const +TSet UFlowSubsystem::GetFlowComponentsByTag(const FGameplayTag Tag, const TSubclassOf ComponentClass, const bool bExactMatch) const { TArray> FoundComponents; - FlowComponentRegistry.MultiFind(Tag, FoundComponents); + FindComponents(Tag, bExactMatch, FoundComponents); TSet Result; for (const TWeakObjectPtr& Component : FoundComponents) @@ -369,10 +525,10 @@ TSet UFlowSubsystem::GetFlowComponentsByTag(const FGameplayTag return Result; } -TSet UFlowSubsystem::GetFlowComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ComponentClass) const +TSet UFlowSubsystem::GetFlowComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ComponentClass, const bool bExactMatch) const { TSet> FoundComponents; - FindComponents(Tags, FoundComponents, MatchType); + FindComponents(Tags, MatchType, bExactMatch, FoundComponents); TSet Result; for (const TWeakObjectPtr& Component : FoundComponents) @@ -386,10 +542,10 @@ TSet UFlowSubsystem::GetFlowComponentsByTags(const FGameplayTag return Result; } -TSet UFlowSubsystem::GetFlowActorsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass) const +TSet UFlowSubsystem::GetFlowActorsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass, const bool bExactMatch) const { TArray> FoundComponents; - FlowComponentRegistry.MultiFind(Tag, FoundComponents); + FindComponents(Tag, bExactMatch, FoundComponents); TSet Result; for (const TWeakObjectPtr& Component : FoundComponents) @@ -403,10 +559,10 @@ TSet UFlowSubsystem::GetFlowActorsByTag(const FGameplayTag Tag, const T return Result; } -TSet UFlowSubsystem::GetFlowActorsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass) const +TSet UFlowSubsystem::GetFlowActorsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass, const bool bExactMatch) const { TSet> FoundComponents; - FindComponents(Tags, FoundComponents, MatchType); + FindComponents(Tags, MatchType, bExactMatch, FoundComponents); TSet Result; for (const TWeakObjectPtr& Component : FoundComponents) @@ -420,10 +576,10 @@ TSet UFlowSubsystem::GetFlowActorsByTags(const FGameplayTagContainer Ta return Result; } -TMap UFlowSubsystem::GetFlowActorsAndComponentsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass) const +TMap UFlowSubsystem::GetFlowActorsAndComponentsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass, const bool bExactMatch) const { TArray> FoundComponents; - FlowComponentRegistry.MultiFind(Tag, FoundComponents); + FindComponents(Tag, bExactMatch, FoundComponents); TMap Result; for (const TWeakObjectPtr& Component : FoundComponents) @@ -437,10 +593,10 @@ TMap UFlowSubsystem::GetFlowActorsAndComponentsByTag(c return Result; } -TMap UFlowSubsystem::GetFlowActorsAndComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass) const +TMap UFlowSubsystem::GetFlowActorsAndComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass, const bool bExactMatch) const { TSet> FoundComponents; - FindComponents(Tags, FoundComponents, MatchType); + FindComponents(Tags, MatchType, bExactMatch, FoundComponents); TMap Result; for (const TWeakObjectPtr& Component : FoundComponents) @@ -454,14 +610,32 @@ TMap UFlowSubsystem::GetFlowActorsAndComponentsByTags( return Result; } -void UFlowSubsystem::FindComponents(const FGameplayTagContainer& Tags, TSet>& OutComponents, const EGameplayContainerMatchType MatchType) const +void UFlowSubsystem::FindComponents(const FGameplayTag& Tag, const bool bExactMatch, TArray>& OutComponents) const +{ + if (bExactMatch) + { + FlowComponentRegistry.MultiFind(Tag, OutComponents); + } + else + { + for (TMultiMap>::TConstIterator It(FlowComponentRegistry); It; ++It) + { + if (It.Key().MatchesTag(Tag)) + { + OutComponents.Emplace(It.Value()); + } + } + } +} + +void UFlowSubsystem::FindComponents(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType, const bool bExactMatch, TSet>& OutComponents) const { if (MatchType == EGameplayContainerMatchType::Any) { for (const FGameplayTag& Tag : Tags) { TArray> ComponentsPerTag; - FlowComponentRegistry.MultiFind(Tag, ComponentsPerTag); + FindComponents(Tag, bExactMatch, ComponentsPerTag); OutComponents.Append(ComponentsPerTag); } } @@ -471,7 +645,7 @@ void UFlowSubsystem::FindComponents(const FGameplayTagContainer& Tags, TSet> ComponentsPerTag; - FlowComponentRegistry.MultiFind(Tag, ComponentsPerTag); + FindComponents(Tag, bExactMatch, ComponentsPerTag); ComponentsWithAnyTag.Append(ComponentsPerTag); } @@ -484,3 +658,5 @@ void UFlowSubsystem::FindComponents(const FGameplayTagContainer& Tags, TSetbAllowMultipleInstances = false; } -void AFlowWorldSettings::PostLoad() +void AFlowWorldSettings::PostInitializeComponents() { - Super::PostLoad(); + Super::PostInitializeComponents(); - if (FlowAsset_DEPRECATED) + if (!IsValidInstance()) { - FlowComponent->RootFlow = FlowAsset_DEPRECATED; + GetFlowComponent()->bAutoStartRootFlow = false; } } + +bool AFlowWorldSettings::IsValidInstance() const +{ + if (const UWorld* World = GetWorld()) + { + // workaround to prevent starting Flow from stray AWorldSettings actor that still exists in the world + // cause of this issue fixed in UE 5.0: https://github.com/EpicGames/UnrealEngine/commit/001f50b8b55507940f9c2cb1349592c692aae2c1?diff=unified + if (World->GetWorldSettings() == this) + { + return true; + } + } + + return false; +} diff --git a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp index 96a3b51ec..ae97484e4 100644 --- a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp +++ b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp @@ -1,8 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "LevelSequence/FlowLevelSequenceActor.h" #include "LevelSequence/FlowLevelSequencePlayer.h" +#include "Net/UnrealNetwork.h" AFlowLevelSequenceActor::AFlowLevelSequenceActor(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer - .SetDefaultSubobjectClass("AnimationPlayer")) + : Super(ObjectInitializer.SetDefaultSubobjectClass("AnimationPlayer")) + , ReplicatedLevelSequenceAsset(nullptr) +{ +} + +void AFlowLevelSequenceActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AFlowLevelSequenceActor, ReplicatedLevelSequenceAsset); +} + +void AFlowLevelSequenceActor::SetReplicatedLevelSequenceAsset(ULevelSequence* Asset) +{ + if (HasAuthority()) + { + LevelSequenceAsset = Asset; + ReplicatedLevelSequenceAsset = LevelSequenceAsset; + } +} + +void AFlowLevelSequenceActor::OnRep_ReplicatedLevelSequenceAsset() { + LevelSequenceAsset = ReplicatedLevelSequenceAsset; + ReplicatedLevelSequenceAsset = nullptr; + + InitializePlayer(); } diff --git a/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp b/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp index 484859fd9..29f51f385 100644 --- a/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp +++ b/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp @@ -1,16 +1,29 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "LevelSequence/FlowLevelSequencePlayer.h" #include "LevelSequence/FlowLevelSequenceActor.h" #include "Nodes/FlowNode.h" +#include "DefaultLevelSequenceInstanceData.h" + UFlowLevelSequencePlayer::UFlowLevelSequencePlayer(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , FlowEventReceiver(nullptr) + : Super(ObjectInitializer) + , FlowEventReceiver(nullptr) { } -UFlowLevelSequencePlayer* UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer(UObject* WorldContextObject, ULevelSequence* InLevelSequence, FMovieSceneSequencePlaybackSettings Settings, ALevelSequenceActor*& OutActor) +UFlowLevelSequencePlayer* UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer( + const UObject* WorldContextObject, + ULevelSequence* LevelSequence, + FMovieSceneSequencePlaybackSettings Settings, + FLevelSequenceCameraSettings CameraSettings, + AActor* TransformOriginActor, + const bool bReplicates, + const bool bAlwaysRelevant, + ALevelSequenceActor*& OutActor +) { - if (InLevelSequence == nullptr) + if (LevelSequence == nullptr) { return nullptr; } @@ -21,36 +34,64 @@ UFlowLevelSequencePlayer* UFlowLevelSequencePlayer::CreateFlowLevelSequencePlaye return nullptr; } - FActorSpawnParameters SpawnParams; - SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; - SpawnParams.ObjectFlags |= RF_Transient; - SpawnParams.bAllowDuringConstructionScript = true; + // Sequence Actor might be spawned exactly where playback happens + FTransform SpawnTransform = FTransform::Identity; + { + // apply Transform Origin + // https://docs.unrealengine.com/5.0/en-US/creating-level-sequences-with-dynamic-transforms-in-unreal-engine/ + if (IsValid(TransformOriginActor)) + { + // moving Level Sequence Actor might allow proper distance-based actor replication in networked games + SpawnTransform = TransformOriginActor->GetTransform(); + SpawnTransform = FTransform(SpawnTransform.GetRotation(), SpawnTransform.GetLocation(), FVector::OneVector); + } + } - // Defer construction for autoplay so that BeginPlay() is called - SpawnParams.bDeferConstruction = true; + // Create Sequence Actor + // We use deferred spawn, so we can set all actor properties prior to its initialization. + // This also helpful in case of multiplayer, since all actor settings are replicated with the spawned actor. No need to call replication just after spawn. + AFlowLevelSequenceActor* Actor = World->SpawnActorDeferred(AFlowLevelSequenceActor::StaticClass(), SpawnTransform, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + Actor->PlaybackSettings = Settings; + Actor->CameraSettings = CameraSettings; - ALevelSequenceActor* Actor = World->SpawnActor(SpawnParams); + // apply Transform Origin to spawned actor + if (IsValid(TransformOriginActor)) + { + if (UDefaultLevelSequenceInstanceData* InstanceData = Cast(Actor->DefaultInstanceData)) + { + Actor->bOverrideInstanceData = true; + InstanceData->TransformOriginActor = TransformOriginActor; + } + } - Actor->PlaybackSettings = Settings; - Actor->LevelSequence = InLevelSequence; + // support networking + if (bReplicates) + { + Actor->bReplicatePlayback = true; + Actor->bAlwaysRelevant = bAlwaysRelevant; + Actor->SetReplicatedLevelSequenceAsset(LevelSequence); + } + else + { + Actor->LevelSequenceAsset = LevelSequence; + } - Actor->InitializePlayer(); + // finish deferred spawn + Actor->FinishSpawning(SpawnTransform); OutActor = Actor; - const FTransform DefaultTransform; - Actor->FinishSpawning(DefaultTransform); - + // Sequence Player is created by Level Sequence Actor return Cast(Actor->SequencePlayer); } TArray UFlowLevelSequencePlayer::GetEventContexts() const { TArray EventContexts; - - if (FlowEventReceiver) - { - EventContexts.Add(FlowEventReceiver); - } + + if (FlowEventReceiver) + { + EventContexts.Add(FlowEventReceiver); + } return EventContexts; } diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp index 211a6b578..6aaaa8afc 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp @@ -1 +1,3 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "MovieScene/MovieSceneFlowRepeaterSection.h" diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp index 9acb1e42f..0016b8255 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp @@ -1 +1,3 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "MovieScene/MovieSceneFlowSectionBase.h" diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp index 5fd6dc681..6e953a52c 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "MovieScene/MovieSceneFlowTemplate.h" #include "MovieScene/MovieSceneFlowTrack.h" #include "Nodes/World/FlowNode_PlayLevelSequence.h" diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp index e67cb9ac5..cb47fe513 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "MovieScene/MovieSceneFlowTrack.h" #include "MovieScene/MovieSceneFlowRepeaterSection.h" #include "MovieScene/MovieSceneFlowTemplate.h" diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp index a81d575b5..bc86dd921 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "MovieScene/MovieSceneFlowTriggerSection.h" #include "Channels/MovieSceneChannelProxy.h" @@ -6,7 +8,7 @@ UMovieSceneFlowTriggerSection::UMovieSceneFlowTriggerSection(const FObjectInitia : Super(ObjInit) { bSupportsInfiniteRange = true; - SetRange(TRange::All()); + UMovieSceneSection::SetRange(TRange::All()); #if WITH_EDITOR ChannelProxy = MakeShared(StringChannel, FMovieSceneChannelMetaData(), TMovieSceneExternalValue::Make()); diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index 48353f6bd..6ab023a92 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -1,12 +1,22 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/FlowNode.h" #include "FlowAsset.h" #include "FlowModule.h" +#include "FlowOwnerInterface.h" +#include "FlowSettings.h" #include "FlowSubsystem.h" #include "FlowTypes.h" +#include "Components/ActorComponent.h" +#if WITH_EDITOR +#include "Editor.h" +#endif #include "Engine/Engine.h" +#include "Engine/ViewportStatsSubsystem.h" #include "Engine/World.h" +#include "GameFramework/Actor.h" #include "Misc/App.h" #include "Misc/Paths.h" #include "Serialization/MemoryReader.h" @@ -15,22 +25,28 @@ FFlowPin UFlowNode::DefaultInputPin(TEXT("In")); FFlowPin UFlowNode::DefaultOutputPin(TEXT("Out")); -FString UFlowNode::MissingIdentityTag = TEXT("Missing Identity Tag!"); -FString UFlowNode::MissingNotifyTag = TEXT("Missing Notify Tag!"); -FString UFlowNode::MissingClass = TEXT("Missing class!"); -FString UFlowNode::NoActorsFound = TEXT("No actors found!"); +FString UFlowNode::MissingIdentityTag = TEXT("Missing Identity Tag"); +FString UFlowNode::MissingNotifyTag = TEXT("Missing Notify Tag"); +FString UFlowNode::MissingClass = TEXT("Missing class"); +FString UFlowNode::NoActorsFound = TEXT("No actors found"); UFlowNode::UFlowNode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) #if WITH_EDITOR , GraphNode(nullptr) + , bCanDelete(true) + , bCanDuplicate(true) + , bNodeDeprecated(false) #endif + , AllowedSignalModes({EFlowSignalMode::Enabled, EFlowSignalMode::Disabled, EFlowSignalMode::PassThrough}) + , SignalMode(EFlowSignalMode::Enabled) , bPreloaded(false) , ActivationState(EFlowNodeState::NeverActivated) { #if WITH_EDITOR Category = TEXT("Uncategorized"); NodeStyle = EFlowNodeStyle::Default; + NodeColor = FLinearColor::Black; #endif InputPins = {DefaultInputPin}; @@ -57,28 +73,12 @@ void UFlowNode::PostLoad() FixNode(nullptr); } -void UFlowNode::FixNode(UEdGraphNode* NewGraph) +void UFlowNode::FixNode(UEdGraphNode* NewGraphNode) { // Fix any node pointers that may be out of date - if (NewGraph) - { - GraphNode = NewGraph; - } - - // v1.1 upgraded pins to be defined as structs - if (InputNames_DEPRECATED.Num() > InputPins.Num()) - { - for (int32 i = InputPins.Num(); i < InputNames_DEPRECATED.Num(); i++) - { - InputPins.Emplace(InputNames_DEPRECATED[i]); - } - } - if (OutputNames_DEPRECATED.Num() > OutputPins.Num()) + if (NewGraphNode) { - for (int32 i = OutputPins.Num(); i < OutputNames_DEPRECATED.Num(); i++) - { - OutputPins.Emplace(OutputNames_DEPRECATED[i]); - } + GraphNode = NewGraphNode; } } @@ -129,6 +129,17 @@ FText UFlowNode::GetNodeToolTip() const return GetClass()->GetToolTipText(); } +bool UFlowNode::GetDynamicTitleColor(FLinearColor& OutColor) const +{ + if (NodeStyle == EFlowNodeStyle::Custom) + { + OutColor = NodeColor; + return true; + } + + return false; +} + FString UFlowNode::GetNodeDescription() const { return K2_GetNodeDescription(); @@ -140,19 +151,130 @@ UFlowAsset* UFlowNode::GetFlowAsset() const return GetOuter() ? Cast(GetOuter()) : nullptr; } -void UFlowNode::AddInputPins(TArray PinNames) +AActor* UFlowNode::TryGetRootFlowActorOwner() const +{ + AActor* OwningActor = nullptr; + + UObject* RootFlowOwner = TryGetRootFlowObjectOwner(); + + if (IsValid(RootFlowOwner)) + { + // Check if the immediate parent is an AActor + OwningActor = Cast(RootFlowOwner); + + if (!IsValid(OwningActor)) + { + // Check if the if the immediate parent is an UActorComponent + // and return that Component's Owning actor + if (const UActorComponent* OwningComponent = Cast(RootFlowOwner)) + { + OwningActor = OwningComponent->GetOwner(); + } + } + } + + return OwningActor; +} + +UObject* UFlowNode::TryGetRootFlowObjectOwner() const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + + if (IsValid(FlowAsset)) + { + return FlowAsset->GetOwner(); + } + + return nullptr; +} + +IFlowOwnerInterface* UFlowNode::GetFlowOwnerInterface() const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + if (!IsValid(FlowAsset)) + { + return nullptr; + } + + const UClass* ExpectedOwnerClass = FlowAsset->GetExpectedOwnerClass(); + if (!IsValid(ExpectedOwnerClass)) + { + return nullptr; + } + + UObject* RootFlowOwner = FlowAsset->GetOwner(); + if (!IsValid(RootFlowOwner)) + { + return nullptr; + } + + if (IFlowOwnerInterface* FlowOwnerInterface = TryGetFlowOwnerInterfaceFromRootFlowOwner(*RootFlowOwner, *ExpectedOwnerClass)) + { + return FlowOwnerInterface; + } + + if (IFlowOwnerInterface* FlowOwnerInterface = TryGetFlowOwnerInterfaceActor(*RootFlowOwner, *ExpectedOwnerClass)) + { + return FlowOwnerInterface; + } + + return nullptr; +} + +IFlowOwnerInterface* UFlowNode::TryGetFlowOwnerInterfaceFromRootFlowOwner(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass) const +{ + const UClass* RootFlowOwnerClass = RootFlowOwner.GetClass(); + if (!IsValid(RootFlowOwnerClass)) + { + return nullptr; + } + + if (!RootFlowOwnerClass->IsChildOf(&ExpectedOwnerClass)) + { + return nullptr; + } + + // If the immediate owner is the expected class type, return its FlowOwnerInterface + return CastChecked(&RootFlowOwner); +} + +IFlowOwnerInterface* UFlowNode::TryGetFlowOwnerInterfaceActor(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass) const +{ + // Special case if the immediate owner is a component, also consider the component's owning actor + const UActorComponent* FlowComponent = Cast(&RootFlowOwner); + if (!IsValid(FlowComponent)) + { + return nullptr; + } + + AActor* ActorOwner = FlowComponent->GetOwner(); + if (!IsValid(ActorOwner)) + { + return nullptr; + } + + const UClass* ActorOwnerClass = ActorOwner->GetClass(); + if (!ActorOwnerClass->IsChildOf(&ExpectedOwnerClass)) + { + return nullptr; + } + + return CastChecked(ActorOwner); +} + +void UFlowNode::AddInputPins(TArray Pins) { - for (const FName& PinName : PinNames) + for (const FFlowPin& Pin : Pins) { - InputPins.Emplace(PinName); + InputPins.Emplace(Pin); } } -void UFlowNode::AddOutputPins(TArray PinNames) +void UFlowNode::AddOutputPins(TArray Pins) { - for (const FName& PinName : PinNames) + for (const FFlowPin& Pin : Pins) { - OutputPins.Emplace(PinName); + OutputPins.Emplace(Pin); } } @@ -176,6 +298,32 @@ void UFlowNode::SetNumberedOutputPins(const uint8 FirstNumber /*= 0*/, const uin } } +uint8 UFlowNode::CountNumberedInputs() const +{ + uint8 Result = 0; + for (const FFlowPin& Pin : InputPins) + { + if (Pin.PinName.ToString().IsNumeric()) + { + Result++; + } + } + return Result; +} + +uint8 UFlowNode::CountNumberedOutputs() const +{ + uint8 Result = 0; + for (const FFlowPin& Pin : OutputPins) + { + if (Pin.PinName.ToString().IsNumeric()) + { + Result++; + } + } + return Result; +} + TArray UFlowNode::GetInputNames() const { TArray Result; @@ -213,16 +361,60 @@ bool UFlowNode::CanUserAddOutput() const return K2_CanUserAddOutput(); } -void UFlowNode::RemoveUserInput() +void UFlowNode::RemoveUserInput(const FName& PinName) { Modify(); - InputPins.RemoveAt(InputPins.Num() - 1); + + int32 RemovedPinIndex = INDEX_NONE; + for (int32 i = 0; i < InputPins.Num(); i++) + { + if (InputPins[i].PinName == PinName) + { + InputPins.RemoveAt(i); + RemovedPinIndex = i; + break; + } + } + + // update remaining pins + if (RemovedPinIndex > INDEX_NONE) + { + for (int32 i = RemovedPinIndex; i < InputPins.Num(); ++i) + { + if (InputPins[i].PinName.ToString().IsNumeric()) + { + InputPins[i].PinName = *FString::FromInt(i); + } + } + } } -void UFlowNode::RemoveUserOutput() +void UFlowNode::RemoveUserOutput(const FName& PinName) { Modify(); - OutputPins.RemoveAt(OutputPins.Num() - 1); + + int32 RemovedPinIndex = INDEX_NONE; + for (int32 i = 0; i < OutputPins.Num(); i++) + { + if (OutputPins[i].PinName == PinName) + { + OutputPins.RemoveAt(i); + RemovedPinIndex = i; + break; + } + } + + // update remaining pins + if (RemovedPinIndex > INDEX_NONE) + { + for (int32 i = RemovedPinIndex; i < OutputPins.Num(); ++i) + { + if (OutputPins[i].PinName.ToString().IsNumeric()) + { + OutputPins[i].PinName = *FString::FromInt(i); + } + } + } } #endif @@ -236,32 +428,44 @@ TSet UFlowNode::GetConnectedNodes() const return Result; } -bool UFlowNode::IsOutputConnected(const FName& PinName) const +FName UFlowNode::GetPinConnectedToNode(const FGuid& OtherNodeGuid) { - return OutputPins.Contains(PinName) && Connections.Contains(PinName); -} - -void UFlowNode::RecursiveFindNodesByClass(UFlowNode* Node, const TSubclassOf Class, uint8 Depth, TArray& OutNodes) -{ - if (Node) + for (const TPair& Connection : Connections) { - // Record the node if it is the desired type - if (Node->GetClass() == Class) + if (Connection.Value.NodeGuid == OtherNodeGuid) { - OutNodes.AddUnique(Node); + return Connection.Key; } + } - if (OutNodes.Num() == Depth) - { - return; - } + return NAME_None; +} - // Recurse - for (UFlowNode* ConnectedNode : Node->GetConnectedNodes()) +bool UFlowNode::IsInputConnected(const FName& PinName) const +{ + if (GetFlowAsset()) + { + for (const TPair& Pair : GetFlowAsset()->Nodes) { - RecursiveFindNodesByClass(ConnectedNode, Class, Depth, OutNodes); + if (Pair.Value) + { + for (const TPair& Connection : Pair.Value->Connections) + { + if (Connection.Value.NodeGuid == NodeGuid && Connection.Value.PinName == PinName) + { + return true; + } + } + } } } + + return false; +} + +bool UFlowNode::IsOutputConnected(const FName& PinName) const +{ + return OutputPins.Contains(PinName) && Connections.Contains(PinName); } UFlowSubsystem* UFlowNode::GetFlowSubsystem() const @@ -306,20 +510,39 @@ void UFlowNode::FlushContent() K2_FlushContent(); } -void UFlowNode::TriggerInput(const FName& PinName) +void UFlowNode::OnActivate() +{ + K2_OnActivate(); +} + +void UFlowNode::TriggerInput(const FName& PinName, const EFlowPinActivationType ActivationType /*= Default*/) { + if (SignalMode == EFlowSignalMode::Disabled) + { + // entirely ignore any Input activation + } + if (InputPins.Contains(PinName)) { - ActivationState = EFlowNodeState::Active; + if (SignalMode == EFlowSignalMode::Enabled) + { + const EFlowNodeState PreviousActivationState = ActivationState; + if (PreviousActivationState != EFlowNodeState::Active) + { + OnActivate(); + } + + ActivationState = EFlowNodeState::Active; + } #if !UE_BUILD_SHIPPING // record for debugging TArray& Records = InputRecords.FindOrAdd(PinName); - Records.Add(FPinRecord(FApp::GetCurrentTime())); + Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); #endif // UE_BUILD_SHIPPING #if WITH_EDITOR - if (GetWorld()->WorldType == EWorldType::PIE && UFlowAsset::GetFlowGraphInterface().IsValid()) + if (GEditor && UFlowAsset::GetFlowGraphInterface().IsValid()) { UFlowAsset::GetFlowGraphInterface()->OnInputTriggered(GraphNode, InputPins.IndexOfByKey(PinName)); } @@ -333,7 +556,26 @@ void UFlowNode::TriggerInput(const FName& PinName) return; } - ExecuteInput(PinName); + switch (SignalMode) + { + case EFlowSignalMode::Enabled: + ExecuteInput(PinName); + break; + case EFlowSignalMode::Disabled: + if (UFlowSettings::Get()->bLogOnSignalDisabled) + { + LogNote(FString::Printf(TEXT("Node disabled while triggering input %s"), *PinName.ToString())); + } + break; + case EFlowSignalMode::PassThrough: + if (UFlowSettings::Get()->bLogOnSignalPassthrough) + { + LogNote(FString::Printf(TEXT("Signal pass-through on triggering input %s"), *PinName.ToString())); + } + OnPassThrough(); + break; + default: ; + } } void UFlowNode::ExecuteInput(const FName& PinName) @@ -349,23 +591,23 @@ void UFlowNode::TriggerFirstOutput(const bool bFinish) } } -void UFlowNode::TriggerOutput(const FName& PinName, const bool bFinish /*= false*/) +void UFlowNode::TriggerOutput(const FName& PinName, const bool bFinish /*= false*/, const EFlowPinActivationType ActivationType /*= Default*/) { // clean up node, if needed if (bFinish) { Finish(); } - + #if !UE_BUILD_SHIPPING if (OutputPins.Contains(PinName)) { // record for debugging, even if nothing is connected to this pin TArray& Records = OutputRecords.FindOrAdd(PinName); - Records.Add(FPinRecord(FApp::GetCurrentTime())); + Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); #if WITH_EDITOR - if (GetWorld()->WorldType == EWorldType::PIE && UFlowAsset::GetFlowGraphInterface().IsValid()) + if (GEditor && UFlowAsset::GetFlowGraphInterface().IsValid()) { UFlowAsset::GetFlowGraphInterface()->OnOutputTriggered(GraphNode, OutputPins.IndexOfByKey(PinName)); } @@ -385,6 +627,11 @@ void UFlowNode::TriggerOutput(const FName& PinName, const bool bFinish /*= false } } +void UFlowNode::TriggerOutputPin(const FFlowOutputPinHandle Pin, const bool bFinish, const EFlowPinActivationType ActivationType /*= Default*/) +{ + TriggerOutput(Pin.PinName, bFinish, ActivationType); +} + void UFlowNode::TriggerOutput(const FString& PinName, const bool bFinish) { TriggerOutput(*PinName, bFinish); @@ -425,6 +672,11 @@ void UFlowNode::Cleanup() K2_Cleanup(); } +void UFlowNode::DeinitializeInstance() +{ + K2_DeinitializeInstance(); +} + void UFlowNode::ForceFinishNode() { K2_ForceFinishNode(); @@ -440,12 +692,75 @@ void UFlowNode::ResetRecords() #endif } +void UFlowNode::SaveInstance(FFlowNodeSaveData& NodeRecord) +{ + NodeRecord.NodeGuid = NodeGuid; + OnSave(); + + FMemoryWriter MemoryWriter(NodeRecord.NodeData, true); + FFlowArchive Ar(MemoryWriter); + Serialize(Ar); +} + +void UFlowNode::LoadInstance(const FFlowNodeSaveData& NodeRecord) +{ + FMemoryReader MemoryReader(NodeRecord.NodeData, true); + FFlowArchive Ar(MemoryReader); + Serialize(Ar); + + if (UFlowAsset* FlowAsset = GetFlowAsset()) + { + FlowAsset->OnActivationStateLoaded(this); + } + + switch (SignalMode) + { + case EFlowSignalMode::Enabled: + OnLoad(); + break; + case EFlowSignalMode::Disabled: + // designer doesn't want to execute this node's logic at all, so we kill it + LogNote(TEXT("Signal disabled while loading Flow Node from SaveGame")); + Finish(); + break; + case EFlowSignalMode::PassThrough: + LogNote(TEXT("Signal pass-through on loading Flow Node from SaveGame")); + OnPassThrough(); + break; + default: ; + } +} + +void UFlowNode::OnSave_Implementation() +{ +} + +void UFlowNode::OnLoad_Implementation() +{ +} + +void UFlowNode::OnPassThrough_Implementation() +{ + // trigger all connected outputs + // pin connections aren't serialized to the SaveGame, so users can safely change connections post game release + for (const FFlowPin& OutputPin : OutputPins) + { + if (Connections.Contains(OutputPin.PinName)) + { + TriggerOutput(OutputPin.PinName, false, EFlowPinActivationType::PassThrough); + } + } + + // deactivate node, so it doesn't get saved to a new SaveGame + Finish(); +} + #if WITH_EDITOR UFlowNode* UFlowNode::GetInspectedInstance() const { if (const UFlowAsset* FlowInstance = GetFlowAsset()->GetInspectedInstance()) { - return FlowInstance->GetNodeInstance(GetGuid()); + return FlowInstance->GetNode(GetGuid()); } return nullptr; @@ -479,6 +794,11 @@ FString UFlowNode::GetStatusString() const return K2_GetStatusString(); } +bool UFlowNode::GetStatusBackgroundColor(FLinearColor& OutColor) const +{ + return K2_GetStatusBackgroundColor(OutColor); +} + FString UFlowNode::GetAssetPath() { return K2_GetAssetPath(); @@ -515,74 +835,90 @@ FString UFlowNode::GetClassDescription(const TSubclassOf Class) return Class ? Class->GetName() : MissingClass; } -FString UFlowNode::GetProgressAsString(float Value) +FString UFlowNode::GetProgressAsString(const float Value) { - // Avoids negative zero - if (Value == 0) - { - Value = 0; - } - - // First create the string - FString TempString = FString::Printf(TEXT("%f"), Value); - if (!TempString.IsNumeric()) - { - // String did not format as a valid decimal number so avoid messing with it - return TempString; - } + return FString::Printf(TEXT("%.*f"), 2, Value); +} - // Get position of the first digit after decimal separator - int32 TrimIndex = INDEX_NONE; - for (int32 CharIndex = 0; CharIndex < TempString.Len(); CharIndex++) +void UFlowNode::LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType) +{ +#if !UE_BUILD_SHIPPING + if (BuildMessage(Message)) { - const TCHAR Char = TempString[CharIndex]; - if (Char == TEXT('.')) + // OnScreen Message + if (OnScreenMessageType == EFlowOnScreenMessageType::Permanent) { - TrimIndex = CharIndex + 2; - break; + if (GetWorld()) + { + if (UViewportStatsSubsystem* StatsSubsystem = GetWorld()->GetSubsystem()) + { + StatsSubsystem->AddDisplayDelegate([this, Message](FText& OutText, FLinearColor& OutColor) + { + OutText = FText::FromString(Message); + OutColor = FLinearColor::Red; + return IsValid(this) && ActivationState != EFlowNodeState::NeverActivated; + }); + } + } } - if (TrimIndex == INDEX_NONE && Char != TEXT('0')) + else { - TrimIndex = CharIndex + 1; + GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, Message); } - } - - TempString.RemoveAt(TrimIndex, TempString.Len() - TrimIndex, /*bAllowShrinking*/false); - return TempString; -} -void UFlowNode::LogError(FString Message) const -{ - const FString TemplatePath = GetFlowAsset()->TemplateAsset->GetPathName(); - Message += TEXT(" in node ") + GetName() + TEXT(", asset ") + FPaths::GetPath(TemplatePath) + TEXT("/") + FPaths::GetBaseFilename(TemplatePath); + // Output Log + UE_LOG(LogFlow, Error, TEXT("%s"), *Message); - GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, Message); - UE_LOG(LogFlow, Error, TEXT("%s"), *Message); + // Message Log +#if WITH_EDITOR + GetFlowAsset()->GetTemplateAsset()->LogError(Message, this); +#endif + } +#endif } -void UFlowNode::SaveInstance(FFlowNodeSaveData& NodeRecord) +void UFlowNode::LogWarning(FString Message) { - NodeRecord.NodeGuid = NodeGuid; - OnSave(); +#if !UE_BUILD_SHIPPING + if (BuildMessage(Message)) + { + // Output Log + UE_LOG(LogFlow, Warning, TEXT("%s"), *Message); - FMemoryWriter MemoryWriter(NodeRecord.NodeData, true); - FFlowArchive Ar(MemoryWriter); - Serialize(Ar); + // Message Log +#if WITH_EDITOR + GetFlowAsset()->GetTemplateAsset()->LogWarning(Message, this); +#endif + } +#endif } -void UFlowNode::LoadInstance(const FFlowNodeSaveData& NodeRecord) +void UFlowNode::LogNote(FString Message) { - FMemoryReader MemoryReader(NodeRecord.NodeData, true); - FFlowArchive Ar(MemoryReader); - Serialize(Ar); +#if !UE_BUILD_SHIPPING + if (BuildMessage(Message)) + { + // Output Log + UE_LOG(LogFlow, Log, TEXT("%s"), *Message); - OnLoad(); + // Message Log +#if WITH_EDITOR + GetFlowAsset()->GetTemplateAsset()->LogNote(Message, this); +#endif + } +#endif } -void UFlowNode::OnSave_Implementation() +#if !UE_BUILD_SHIPPING +bool UFlowNode::BuildMessage(FString& Message) const { -} + if (GetFlowAsset()->TemplateAsset) // this is runtime log which is should be only called on runtime instances of asset + { + const FString TemplatePath = GetFlowAsset()->TemplateAsset->GetPathName(); + Message.Append(TEXT(" --- node ")).Append(GetName()).Append(TEXT(", asset ")).Append(FPaths::GetPath(TemplatePath) / FPaths::GetBaseFilename(TemplatePath)); + return true; + } -void UFlowNode::OnLoad_Implementation() -{ + return false; } +#endif diff --git a/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp b/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp index 8b06d0bfc..8d9b7969b 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/FlowNodeBlueprint.h" UFlowNodeBlueprint::UFlowNodeBlueprint(const FObjectInitializer& ObjectInitializer) diff --git a/Source/Flow/Private/Nodes/FlowPin.cpp b/Source/Flow/Private/Nodes/FlowPin.cpp index 8faca4ae1..368ffa312 100644 --- a/Source/Flow/Private/Nodes/FlowPin.cpp +++ b/Source/Flow/Private/Nodes/FlowPin.cpp @@ -1,20 +1,27 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/FlowPin.h" -#if !UE_BUILD_SHIPPING +////////////////////////////////////////////////////////////////////////// +// Pin Record +#if !UE_BUILD_SHIPPING FString FPinRecord::NoActivations = TEXT("No activations"); FString FPinRecord::PinActivations = TEXT("Pin activations"); +FString FPinRecord::ForcedActivation = TEXT(" (forced activation)"); +FString FPinRecord::PassThroughActivation = TEXT(" (pass-through activation)"); FPinRecord::FPinRecord() + : Time(0.0f) + , HumanReadableTime(FString()) + , ActivationType(EFlowPinActivationType::Default) { - Time = 0.0f; - HumanReadableTime = FString(); } -FPinRecord::FPinRecord(const double InTime) +FPinRecord::FPinRecord(const double InTime, const EFlowPinActivationType InActivationType) + : Time(InTime) + , ActivationType(InActivationType) { - Time = InTime; - const FDateTime SystemTime(FDateTime::Now()); HumanReadableTime = DoubleDigit(SystemTime.GetHour()) + TEXT(".") + DoubleDigit(SystemTime.GetMinute()) + TEXT(".") @@ -27,3 +34,85 @@ FORCEINLINE FString FPinRecord::DoubleDigit(const int32 Number) return Number > 9 ? FString::FromInt(Number) : TEXT("0") + FString::FromInt(Number); } #endif + +////////////////////////////////////////////////////////////////////////// +// Pin Trait + +void FFlowPinTrait::AllowTrait() +{ + if (!bTraitAllowed) + { + bTraitAllowed = true; + bEnabled = true; + } +} + +void FFlowPinTrait::DisallowTrait() +{ + if (bTraitAllowed) + { + bTraitAllowed = false; + bEnabled = false; + } +} + +bool FFlowPinTrait::IsAllowed() const +{ + return bTraitAllowed; +} + +void FFlowPinTrait::EnableTrait() +{ + if (bTraitAllowed && !bEnabled) + { + bEnabled = true; + } +} + +void FFlowPinTrait::DisableTrait() +{ + if (bTraitAllowed && bEnabled) + { + bEnabled = false; + } +} + +void FFlowPinTrait::ToggleTrait() +{ + if (bTraitAllowed) + { + bTraitAllowed = false; + bEnabled = false; + } + else + { + bTraitAllowed = true; + bEnabled = true; + } +} + +bool FFlowPinTrait::CanEnable() const +{ + return bTraitAllowed && !bEnabled; +} + +bool FFlowPinTrait::IsEnabled() const +{ + return bTraitAllowed && bEnabled; +} + +void FFlowPinTrait::MarkAsHit() +{ + bHit = true; +} + +void FFlowPinTrait::ResetHit() +{ + bHit = false; +} + +bool FFlowPinTrait::IsHit() const +{ + return bHit; +} + diff --git a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalAND.cpp b/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalAND.cpp index 4d57bc96a..753bed54a 100644 --- a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalAND.cpp +++ b/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalAND.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Operators/FlowNode_LogicalAND.h" UFlowNode_LogicalAND::UFlowNode_LogicalAND(const FObjectInitializer& ObjectInitializer) diff --git a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalOR.cpp b/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalOR.cpp index 5b1dcc0ad..402758b77 100644 --- a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalOR.cpp +++ b/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalOR.cpp @@ -1,7 +1,12 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Operators/FlowNode_LogicalOR.h" UFlowNode_LogicalOR::UFlowNode_LogicalOR(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bEnabled(true) + , ExecutionLimit(1) + , ExecutionCount(0) { #if WITH_EDITOR Category = TEXT("Operators"); @@ -9,9 +14,50 @@ UFlowNode_LogicalOR::UFlowNode_LogicalOR(const FObjectInitializer& ObjectInitial #endif SetNumberedInputPins(0, 1); + InputPins.Add(FFlowPin(TEXT("Enable"), TEXT("Enabling resets Execution Count"))); + InputPins.Add(FFlowPin(TEXT("Disable"), TEXT("Disabling resets Execution Count"))); } void UFlowNode_LogicalOR::ExecuteInput(const FName& PinName) { - TriggerFirstOutput(true); + if (PinName == TEXT("Enable")) + { + if (!bEnabled) + { + ResetCounter(); + bEnabled = true; + } + return; + } + + if (PinName == TEXT("Disable")) + { + if (bEnabled) + { + bEnabled = false; + Finish(); + } + return; + } + + if (bEnabled && PinName.ToString().IsNumeric()) + { + ExecutionCount++; + if (ExecutionLimit > 0 && ExecutionCount == ExecutionLimit) + { + bEnabled = false; + } + + TriggerFirstOutput(true); + } +} + +void UFlowNode_LogicalOR::Cleanup() +{ + ResetCounter(); +} + +void UFlowNode_LogicalOR::ResetCounter() +{ + ExecutionCount = 0; } diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp index 8ab4dd443..5a4eaf50c 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_Counter.h" UFlowNode_Counter::UFlowNode_Counter(const FObjectInitializer& ObjectInitializer) diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_CustomEventBase.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_CustomEventBase.cpp new file mode 100644 index 000000000..0b75bc43b --- /dev/null +++ b/Source/Flow/Private/Nodes/Route/FlowNode_CustomEventBase.cpp @@ -0,0 +1,52 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Route/FlowNode_CustomEventBase.h" +#include "FlowSettings.h" + +UFlowNode_CustomEventBase::UFlowNode_CustomEventBase(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +#if WITH_EDITOR + Category = TEXT("Route"); + NodeStyle = EFlowNodeStyle::InOut; +#endif + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +void UFlowNode_CustomEventBase::SetEventName(const FName& InEventName) +{ + if (EventName != InEventName) + { + EventName = InEventName; + +#if WITH_EDITOR + // Must reconstruct the visual representation if anything that is included in AdaptiveNodeTitles changes + OnReconstructionRequested.ExecuteIfBound(); +#endif // WITH_EDITOR + } +} + +#if WITH_EDITOR + +FString UFlowNode_CustomEventBase::GetNodeDescription() const +{ + if (UFlowSettings::Get()->bUseAdaptiveNodeTitles) + { + return Super::GetNodeDescription(); + } + + return EventName.ToString(); +} + +EDataValidationResult UFlowNode_CustomEventBase::ValidateNode() +{ + if (EventName.IsNone()) + { + ValidationLog.Error(TEXT("Event Name is empty!"), this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} +#endif diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_CustomInput.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_CustomInput.cpp index 06a61ae40..2e1f594c8 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_CustomInput.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_CustomInput.cpp @@ -1,13 +1,13 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_CustomInput.h" +#include "FlowSettings.h" + +#define LOCTEXT_NAMESPACE "FlowNode_CustomInput" UFlowNode_CustomInput::UFlowNode_CustomInput(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { -#if WITH_EDITOR - Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::InOut; -#endif - InputPins.Empty(); } @@ -17,8 +17,15 @@ void UFlowNode_CustomInput::ExecuteInput(const FName& PinName) } #if WITH_EDITOR -FString UFlowNode_CustomInput::GetNodeDescription() const +FText UFlowNode_CustomInput::GetNodeTitle() const { - return EventName.ToString(); + if (!EventName.IsNone() && UFlowSettings::Get()->bUseAdaptiveNodeTitles) + { + return FText::Format(LOCTEXT("CustomInputTitle", "{0} Input"), {FText::FromString(EventName.ToString())}); + } + + return Super::GetNodeTitle(); } #endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_CustomOutput.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_CustomOutput.cpp index 64d907f3e..a8a8a96cf 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_CustomOutput.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_CustomOutput.cpp @@ -1,30 +1,63 @@ -#include "Nodes/Route/FlowNode_CustomOutput.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#include "Nodes/Route/FlowNode_CustomOutput.h" #include "FlowAsset.h" -#include "Nodes/Route/FlowNode_SubGraph.h" +#include "FlowSettings.h" + +#define LOCTEXT_NAMESPACE "FlowNode_CustomOutput" UFlowNode_CustomOutput::UFlowNode_CustomOutput(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { -#if WITH_EDITOR - Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::InOut; -#endif - OutputPins.Empty(); } void UFlowNode_CustomOutput::ExecuteInput(const FName& PinName) { - if (!EventName.IsNone() && GetFlowAsset()->GetCustomOutputs().Contains(EventName) && GetFlowAsset()->GetNodeOwningThisAssetInstance()) + UFlowAsset* FlowAsset = GetFlowAsset(); + check(IsValid(FlowAsset)); + + if (EventName.IsNone()) { - GetFlowAsset()->TriggerCustomOutput(EventName); + LogWarning(FString::Printf(TEXT("Attempted to trigger a CustomOutput (Node %s, Asset %s), with no EventName"), + *GetName(), + *FlowAsset->GetPathName())); + } + else if (!FlowAsset->GetCustomOutputs().Contains(EventName)) + { + FString CustomOutputsString; + for (const FName& OutputName : FlowAsset->GetCustomOutputs()) + { + if (!CustomOutputsString.IsEmpty()) + { + CustomOutputsString += TEXT(", "); + } + + CustomOutputsString += OutputName.ToString(); + } + + LogWarning(FString::Printf(TEXT("Attempted to trigger a CustomOutput (Node %s, Asset %s), with EventName %s, which is not a listed CustomOutput { %s }"), + *GetName(), + *FlowAsset->GetPathName(), + *EventName.ToString(), + *CustomOutputsString)); + } + else + { + FlowAsset->TriggerCustomOutput(EventName); } } #if WITH_EDITOR -FString UFlowNode_CustomOutput::GetNodeDescription() const +FText UFlowNode_CustomOutput::GetNodeTitle() const { - return EventName.ToString(); + if (!EventName.IsNone() && UFlowSettings::Get()->bUseAdaptiveNodeTitles) + { + return FText::Format(LOCTEXT("CustomOutputTitle", "{0} Output"), {FText::FromString(EventName.ToString())}); + } + + return Super::GetNodeTitle(); } #endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp index 0b8858177..455989751 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_ExecutionMultiGate.h" UFlowNode_ExecutionMultiGate::UFlowNode_ExecutionMultiGate(const FObjectInitializer& ObjectInitializer) @@ -15,6 +17,7 @@ UFlowNode_ExecutionMultiGate::UFlowNode_ExecutionMultiGate(const FObjectInitiali InputPins.Add(FFlowPin(TEXT("Reset"), ResetPinTooltip)); SetNumberedOutputPins(0, 1); + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_ExecutionMultiGate::ExecuteInput(const FName& PinName) diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp index 4aacc2667..55402fe21 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp @@ -1,7 +1,10 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_ExecutionSequence.h" UFlowNode_ExecutionSequence::UFlowNode_ExecutionSequence(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bSavePinExecutionState(true) { #if WITH_EDITOR Category = TEXT("Route"); @@ -9,10 +12,17 @@ UFlowNode_ExecutionSequence::UFlowNode_ExecutionSequence(const FObjectInitialize #endif SetNumberedOutputPins(0, 1); + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_ExecutionSequence::ExecuteInput(const FName& PinName) { + if (bSavePinExecutionState) + { + ExecuteNewConnections(); + return; + } + for (const FFlowPin& Output : OutputPins) { TriggerOutput(Output.PinName, false); @@ -20,3 +30,35 @@ void UFlowNode_ExecutionSequence::ExecuteInput(const FName& PinName) Finish(); } + +#if WITH_EDITOR +FString UFlowNode_ExecutionSequence::GetNodeDescription() const +{ + if (bSavePinExecutionState) + { + return TEXT("Saves pin execution state"); + } + + return Super::GetNodeDescription(); +} +#endif + +void UFlowNode_ExecutionSequence::OnLoad_Implementation() +{ + ExecuteNewConnections(); +} + +void UFlowNode_ExecutionSequence::ExecuteNewConnections() +{ + for (const FFlowPin& Output : OutputPins) + { + const FConnectedPin& Connection = GetConnection(Output.PinName); + if (!ExecutedConnections.Contains(Connection.NodeGuid)) + { + ExecutedConnections.Emplace(Connection.NodeGuid); + TriggerOutput(Output.PinName, false); + } + } + + Finish(); +} diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Finish.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Finish.cpp index 9012dec8b..7001d8617 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Finish.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Finish.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_Finish.h" UFlowNode_Finish::UFlowNode_Finish(const FObjectInitializer& ObjectInitializer) @@ -9,6 +11,7 @@ UFlowNode_Finish::UFlowNode_Finish(const FObjectInitializer& ObjectInitializer) #endif OutputPins = {}; + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_Finish::ExecuteInput(const FName& PinName) diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp index 181172ea7..cf9d012d3 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_Reroute.h" UFlowNode_Reroute::UFlowNode_Reroute(const FObjectInitializer& ObjectInitializer) @@ -6,6 +8,8 @@ UFlowNode_Reroute::UFlowNode_Reroute(const FObjectInitializer& ObjectInitializer #if WITH_EDITOR Category = TEXT("Route"); #endif + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_Reroute::ExecuteInput(const FName& PinName) diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Start.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Start.cpp index 4334f0451..01fcae6fb 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Start.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Start.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_Start.h" UFlowNode_Start::UFlowNode_Start(const FObjectInitializer& ObjectInitializer) @@ -6,9 +8,11 @@ UFlowNode_Start::UFlowNode_Start(const FObjectInitializer& ObjectInitializer) #if WITH_EDITOR Category = TEXT("Route"); NodeStyle = EFlowNodeStyle::InOut; + bCanDelete = bCanDuplicate = false; #endif InputPins = {}; + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_Start::ExecuteInput(const FName& PinName) diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_SubGraph.cpp index 149e41615..f82a76aeb 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_SubGraph.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_SubGraph.cpp @@ -1,6 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_SubGraph.h" #include "FlowAsset.h" +#include "FlowMessageLog.h" #include "FlowSubsystem.h" FFlowPin UFlowNode_SubGraph::StartPin(TEXT("Start")); @@ -8,19 +11,27 @@ FFlowPin UFlowNode_SubGraph::FinishPin(TEXT("Finish")); UFlowNode_SubGraph::UFlowNode_SubGraph(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bCanInstanceIdenticalAsset(false) { #if WITH_EDITOR Category = TEXT("Route"); NodeStyle = EFlowNodeStyle::SubGraph; + + AllowedAssignedAssetClasses = {UFlowAsset::StaticClass()}; #endif InputPins = {StartPin}; OutputPins = {FinishPin}; } +bool UFlowNode_SubGraph::CanBeAssetInstanced() const +{ + return !Asset.IsNull() && (bCanInstanceIdenticalAsset || Asset->GetPathName() != GetFlowAsset()->GetTemplateAsset()->GetPathName()); +} + void UFlowNode_SubGraph::PreloadContent() { - if (!Asset.IsNull() && GetFlowSubsystem()) + if (CanBeAssetInstanced() && GetFlowSubsystem()) { GetFlowSubsystem()->CreateSubFlow(this, FString(), true); } @@ -28,7 +39,7 @@ void UFlowNode_SubGraph::PreloadContent() void UFlowNode_SubGraph::FlushContent() { - if (!Asset.IsNull() && GetFlowSubsystem()) + if (CanBeAssetInstanced() && GetFlowSubsystem()) { GetFlowSubsystem()->RemoveSubFlow(this, EFlowFinishPolicy::Abort); } @@ -36,30 +47,37 @@ void UFlowNode_SubGraph::FlushContent() void UFlowNode_SubGraph::ExecuteInput(const FName& PinName) { - if (Asset.IsNull()) - { - LogError(TEXT("Missing Flow Asset")); - Finish(); - } - else + if (CanBeAssetInstanced() == false) { - if (PinName == TEXT("Start")) + if (Asset.IsNull()) { - if (GetFlowSubsystem()) - { - GetFlowSubsystem()->CreateSubFlow(this); - } + LogError(TEXT("Missing Flow Asset")); } else { - GetFlowAsset()->TriggerCustomEvent(this, PinName); + LogError(FString::Printf(TEXT("Asset %s cannot be instance, probably is the same as the asset owning this SubGraph node."), *Asset->GetPathName())); } + + Finish(); + return; + } + + if (PinName == TEXT("Start")) + { + if (GetFlowSubsystem()) + { + GetFlowSubsystem()->CreateSubFlow(this); + } + } + else if (!PinName.IsNone()) + { + GetFlowAsset()->TriggerCustomInput_FromSubGraph(this, PinName); } } void UFlowNode_SubGraph::Cleanup() { - if (!Asset.IsNull() && GetFlowSubsystem()) + if (CanBeAssetInstanced() && GetFlowSubsystem()) { GetFlowSubsystem()->RemoveSubFlow(this, EFlowFinishPolicy::Keep); } @@ -87,12 +105,23 @@ FString UFlowNode_SubGraph::GetNodeDescription() const UObject* UFlowNode_SubGraph::GetAssetToEdit() { - return Asset.IsNull() ? nullptr : LoadAsset(Asset); + return Asset.IsNull() ? nullptr : Asset.LoadSynchronous(); } -TArray UFlowNode_SubGraph::GetContextInputs() +EDataValidationResult UFlowNode_SubGraph::ValidateNode() { - TArray EventNames; + if (Asset.IsNull()) + { + ValidationLog.Error(TEXT("Flow Asset not assigned or invalid!"), this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} + +TArray UFlowNode_SubGraph::GetContextInputs() +{ + TArray EventNames; if (!Asset.IsNull()) { @@ -109,9 +138,9 @@ TArray UFlowNode_SubGraph::GetContextInputs() return EventNames; } -TArray UFlowNode_SubGraph::GetContextOutputs() +TArray UFlowNode_SubGraph::GetContextOutputs() { - TArray EventNames; + TArray Pins; if (!Asset.IsNull()) { @@ -120,12 +149,12 @@ TArray UFlowNode_SubGraph::GetContextOutputs() { if (!PinName.IsNone()) { - EventNames.Emplace(PinName); + Pins.Emplace(PinName); } } } - return EventNames; + return Pins; } void UFlowNode_SubGraph::PostLoad() diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp index 06423bb1a..14367e826 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Route/FlowNode_Timer.h" #include "Engine/World.h" @@ -17,6 +19,7 @@ UFlowNode_Timer::UFlowNode_Timer(const FObjectInitializer& ObjectInitializer) #endif InputPins.Add(FFlowPin(TEXT("Skip"))); + InputPins.Add(FFlowPin(TEXT("Restart"))); OutputPins.Empty(); OutputPins.Add(FFlowPin(TEXT("Completed"))); @@ -26,42 +29,61 @@ UFlowNode_Timer::UFlowNode_Timer(const FObjectInitializer& ObjectInitializer) void UFlowNode_Timer::ExecuteInput(const FName& PinName) { - if (CompletionTime == 0.0f) - { - LogError("Invalid Timer settings"); - TriggerOutput(TEXT("Completed"), true); - return; - } - if (PinName == TEXT("In")) { if (CompletionTimerHandle.IsValid() || StepTimerHandle.IsValid()) { - LogError("Timer already active"); + LogError(TEXT("Timer already active")); return; } - if (GetWorld()) + SetTimer(); + } + else if (PinName == TEXT("Skip")) + { + TriggerOutput(TEXT("Skipped"), true); + } + else if (PinName == TEXT("Restart")) + { + Restart(); + } +} + +void UFlowNode_Timer::SetTimer() +{ + if (GetWorld()) + { + if (StepTime > 0.0f) { - if (StepTime > 0.0f) - { - GetWorld()->GetTimerManager().SetTimer(StepTimerHandle, this, &UFlowNode_Timer::OnStep, StepTime, true); - } + GetWorld()->GetTimerManager().SetTimer(StepTimerHandle, this, &UFlowNode_Timer::OnStep, StepTime, true); + } + if (CompletionTime > KINDA_SMALL_NUMBER) + { GetWorld()->GetTimerManager().SetTimer(CompletionTimerHandle, this, &UFlowNode_Timer::OnCompletion, CompletionTime, false); } else { - LogError("No valid world"); - TriggerOutput(TEXT("Completed"), true); + GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UFlowNode_Timer::OnCompletion); } } - else if (PinName == TEXT("Skip")) + else { - TriggerOutput(TEXT("Skipped"), true); + LogError(TEXT("No valid world")); + TriggerOutput(TEXT("Completed"), true); } } +void UFlowNode_Timer::Restart() +{ + Cleanup(); + + RemainingStepTime = 0.0f; + RemainingCompletionTime = 0.0f; + + SetTimer(); +} + void UFlowNode_Timer::OnStep() { SumOfSteps += StepTime; @@ -116,43 +138,46 @@ void UFlowNode_Timer::OnSave_Implementation() void UFlowNode_Timer::OnLoad_Implementation() { - if (RemainingStepTime > 0.0f) + if (RemainingStepTime > 0.0f || RemainingCompletionTime > 0.0f) { - GetWorld()->GetTimerManager().SetTimer(StepTimerHandle, this, &UFlowNode_Timer::OnStep, RemainingStepTime, true); - } + if (RemainingStepTime > 0.0f) + { + GetWorld()->GetTimerManager().SetTimer(StepTimerHandle, this, &UFlowNode_Timer::OnStep, StepTime, true, RemainingStepTime); + } - GetWorld()->GetTimerManager().SetTimer(CompletionTimerHandle, this, &UFlowNode_Timer::OnCompletion, RemainingCompletionTime, false); + GetWorld()->GetTimerManager().SetTimer(CompletionTimerHandle, this, &UFlowNode_Timer::OnCompletion, RemainingCompletionTime, false); - RemainingStepTime = 0.0f; - RemainingCompletionTime = 0.0f; + RemainingStepTime = 0.0f; + RemainingCompletionTime = 0.0f; + } } #if WITH_EDITOR FString UFlowNode_Timer::GetNodeDescription() const { - if (CompletionTime > 0.0f) + if (CompletionTime > KINDA_SMALL_NUMBER) { if (StepTime > 0.0f) { - return FString::SanitizeFloat(CompletionTime, 2).Append(TEXT(", step by ")).Append(FString::SanitizeFloat(StepTime, 2)); + return FString::Printf(TEXT("%.*f, step by %.*f"), 2, CompletionTime, 2, StepTime); } - return FString::SanitizeFloat(CompletionTime, 2); + return FString::Printf(TEXT("%.*f"), 2, CompletionTime); } - return TEXT("Invalid settings"); + return TEXT("Completes in next tick"); } FString UFlowNode_Timer::GetStatusString() const { if (StepTime > 0.0f) { - return TEXT("Progress: ") + GetProgressAsString(SumOfSteps); + return FString::Printf(TEXT("Progress: %.*f"), 2, SumOfSteps); } if (CompletionTimerHandle.IsValid() && GetWorld()) { - return TEXT("Progress: ") + GetProgressAsString(GetWorld()->GetTimerManager().GetTimerElapsed(CompletionTimerHandle)); + return FString::Printf(TEXT("Progress: %.*f"), 2, GetWorld()->GetTimerManager().GetTimerElapsed(CompletionTimerHandle)); } return FString(); diff --git a/Source/Flow/Private/Nodes/Utils/FlowNode_Checkpoint.cpp b/Source/Flow/Private/Nodes/Utils/FlowNode_Checkpoint.cpp index a8e44589c..1cdd9e280 100644 --- a/Source/Flow/Private/Nodes/Utils/FlowNode_Checkpoint.cpp +++ b/Source/Flow/Private/Nodes/Utils/FlowNode_Checkpoint.cpp @@ -1,4 +1,6 @@ -#include "Nodes/Utils/FlowNode_Checkpoint.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Utils/FlowNode_Checkpoint.h" #include "FlowSubsystem.h" #include "Kismet/GameplayStatics.h" diff --git a/Source/Flow/Private/Nodes/Utils/FlowNode_Log.cpp b/Source/Flow/Private/Nodes/Utils/FlowNode_Log.cpp index 01fb7fdbc..5da89a4e4 100644 --- a/Source/Flow/Private/Nodes/Utils/FlowNode_Log.cpp +++ b/Source/Flow/Private/Nodes/Utils/FlowNode_Log.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/Utils/FlowNode_Log.h" #include "FlowModule.h" @@ -6,6 +8,7 @@ UFlowNode_Log::UFlowNode_Log(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , Message(TEXT("Log!")) + , Verbosity(EFlowLogVerbosity::Warning) , bPrintToScreen(true) , Duration(5.0f) , TextColor(FColor::Yellow) @@ -17,7 +20,28 @@ UFlowNode_Log::UFlowNode_Log(const FObjectInitializer& ObjectInitializer) void UFlowNode_Log::ExecuteInput(const FName& PinName) { - UE_LOG(LogFlow, Warning, TEXT("%s"), *Message); + switch (Verbosity) + { + case EFlowLogVerbosity::Error: + UE_LOG(LogFlow, Error, TEXT("%s"), *Message); + break; + case EFlowLogVerbosity::Warning: + UE_LOG(LogFlow, Warning, TEXT("%s"), *Message); + break; + case EFlowLogVerbosity::Display: + UE_LOG(LogFlow, Display, TEXT("%s"), *Message); + break; + case EFlowLogVerbosity::Log: + UE_LOG(LogFlow, Log, TEXT("%s"), *Message); + break; + case EFlowLogVerbosity::Verbose: + UE_LOG(LogFlow, Verbose, TEXT("%s"), *Message); + break; + case EFlowLogVerbosity::VeryVerbose: + UE_LOG(LogFlow, VeryVerbose, TEXT("%s"), *Message); + break; + default: ; + } if (bPrintToScreen) { diff --git a/Source/Flow/Private/Nodes/World/FlowNode_CallOwnerFunction.cpp b/Source/Flow/Private/Nodes/World/FlowNode_CallOwnerFunction.cpp new file mode 100644 index 000000000..048d56391 --- /dev/null +++ b/Source/Flow/Private/Nodes/World/FlowNode_CallOwnerFunction.cpp @@ -0,0 +1,477 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/World/FlowNode_CallOwnerFunction.h" +#include "FlowAsset.h" +#include "FlowModule.h" +#include "FlowOwnerInterface.h" +#include "FlowOwnerFunctionParams.h" +#include "FlowSettings.h" + + +#define LOCTEXT_NAMESPACE "FlowNode" + +// UFlowNode_CallOwnerFunction Implementation + +UFlowNode_CallOwnerFunction::UFlowNode_CallOwnerFunction() + : Super() +{ +#if WITH_EDITOR + NodeStyle = EFlowNodeStyle::Default; + Category = TEXT("World"); +#endif // WITH_EDITOR +} + +void UFlowNode_CallOwnerFunction::ExecuteInput(const FName& PinName) +{ + Super::ExecuteInput(PinName); + + if (!IsValid(Params)) + { + UE_LOG(LogFlow, Error, TEXT("Expected a valid Params object")); + + return; + } + + IFlowOwnerInterface* FlowOwnerInterface = GetFlowOwnerInterface(); + if (!FlowOwnerInterface) + { + UE_LOG(LogFlow, Error, TEXT("Expected an owner that implements the IFlowOwnerInterface")); + + return; + } + + UObject* FlowOwnerObject = CastChecked(FlowOwnerInterface); + UClass* FlowOwnerClass = FlowOwnerObject->GetClass(); + check(IsValid(FlowOwnerClass)); + + if (!FunctionRef.TryResolveFunction(*FlowOwnerClass)) + { + UE_LOG( + LogFlow, + Error, + TEXT("Could not resolve function named %s with flow owner class %s"), + *FunctionRef.GetFunctionName().ToString(), + *FlowOwnerClass->GetName()); + + return; + } + + Params->PreExecute(*this, PinName); + + const FName ResultOutputName = FunctionRef.CallFunction(*FlowOwnerInterface, *Params); + + Params->PostExecute(); + + (void) TryExecuteOutputPin(ResultOutputName); +} + +bool UFlowNode_CallOwnerFunction::TryExecuteOutputPin(const FName& OutputName) +{ + if (OutputName.IsNone()) + { + return false; + } + + const bool bFinish = ShouldFinishForOutputName(OutputName); + TriggerOutput(OutputName, bFinish); + + return true; +} + +bool UFlowNode_CallOwnerFunction::ShouldFinishForOutputName(const FName& OutputName) const +{ + if (ensure(IsValid(Params))) + { + return Params->ShouldFinishForOutputName(OutputName); + } + + return true; +} + +#if WITH_EDITOR + +void UFlowNode_CallOwnerFunction::PostLoad() +{ + Super::PostLoad(); + + FObjectPropertyBase* ParamsProperty = FindFProperty(GetClass(), GET_MEMBER_NAME_CHECKED(UFlowNode_CallOwnerFunction, Params)); + check(ParamsProperty); + + UClass* RequiredParamsClass = GetRequiredParamsClass(); + if (IsValid(RequiredParamsClass)) + { + // Update the property to filter for just this class (and its subclasses) + ParamsProperty->SetPropertyClass(RequiredParamsClass); + } +} + +void UFlowNode_CallOwnerFunction::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + const FName MemberPropertyName = PropertyChangedEvent.MemberProperty->GetFName(); + + if (MemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode_CallOwnerFunction, Params)) + { + OnChangedParamsObject(); + } + + const FName PropertyName = PropertyChangedEvent.Property->GetFName(); + if (PropertyName == GET_MEMBER_NAME_CHECKED(FFlowOwnerFunctionRef, FunctionName)) + { + if (TryAllocateParamsInstance()) + { + OnChangedParamsObject(); + } + } +} + +void UFlowNode_CallOwnerFunction::OnChangedParamsObject() +{ + bool bChangedPins = false; + + if (IsValid(Params)) + { + bChangedPins = RebuildPinArray(Params->GetInputNames(), InputPins, DefaultInputPin) || bChangedPins; + bChangedPins = RebuildPinArray(Params->GetOutputNames(), OutputPins, DefaultOutputPin) || bChangedPins; + } + else + { + bChangedPins = RebuildPinArray(TArray(&DefaultInputPin.PinName, 1), InputPins, DefaultInputPin) || bChangedPins; + bChangedPins = RebuildPinArray(TArray(&DefaultOutputPin.PinName, 1), OutputPins, DefaultOutputPin) || bChangedPins; + } + + if (bChangedPins) + { + OnReconstructionRequested.ExecuteIfBound(); + } +} + +bool UFlowNode_CallOwnerFunction::RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin) +{ + bool bIsChanged = false; + + TArray NewPins; + + if (NewPinNames.Num() == 0) + { + bIsChanged = true; + + NewPins.Reserve(1); + + NewPins.Add(DefaultPin); + } + else + { + const bool bIsSameNum = (NewPinNames.Num() == InOutPins.Num()); + + bIsChanged = !bIsSameNum; + + NewPins.Reserve(NewPinNames.Num()); + + for (int32 NewPinIndex = 0; NewPinIndex < NewPinNames.Num(); ++NewPinIndex) + { + const FName& NewPinName = NewPinNames[NewPinIndex]; + NewPins.Add(FFlowPin(NewPinName)); + + if (bIsSameNum) + { + bIsChanged = bIsChanged || (NewPinName != InOutPins[NewPinIndex].PinName); + } + } + } + + if (bIsChanged) + { + InOutPins.Reset(); + + check(NewPins.Num() > 0); + + if (&InOutPins == &InputPins) + { + AddInputPins(NewPins); + } + else + { + checkf(&InOutPins == &OutputPins, TEXT("Only expected to be called with one or the other of the pin arrays")); + + AddOutputPins(NewPins); + } + } + + return bIsChanged; +} + +EDataValidationResult UFlowNode_CallOwnerFunction::ValidateNode() +{ + const bool bHasFunction = FunctionRef.IsConfigured(); + if (!bHasFunction) + { + ValidationLog.Error(TEXT("CallOwnerFunction requires a valid Function reference"), this); + + return EDataValidationResult::Invalid; + } + + const bool bHasParams = IsValid(Params); + if (!bHasParams) + { + ValidationLog.Error(TEXT("CallOwnerFunction requires a valid Params object"), this); + + return EDataValidationResult::Invalid; + } + + checkf(bHasParams && bHasFunction, TEXT("This should be assured by the preceding logic")); + + const UClass* ExpectedOwnerClass = TryGetExpectedOwnerClass(); + if (!IsValid(ExpectedOwnerClass)) + { + ValidationLog.Error(TEXT("Invalid or null Expected Owner Class for this Flow Asset"), this); + + return EDataValidationResult::Invalid; + } + + // Check if the function can be found on the expected owner + const UFunction* Function = FunctionRef.TryResolveFunction(*ExpectedOwnerClass); + if (!IsValid(Function)) + { + ValidationLog.Error(TEXT("Could not resolve function for flow owner"), this); + + return EDataValidationResult::Invalid; + } + + // Check the function signature + if (!DoesFunctionHaveValidFlowOwnerFunctionSignature(*Function)) + { + ValidationLog.Error(TEXT("Flow Owner Function has an invalid signature"), this); + + return EDataValidationResult::Invalid; + } + + const UClass* RequiredParamsClass = GetRequiredParamsClass(); + checkf(IsValid(RequiredParamsClass), TEXT("GetRequiredParamsClass() cannot return null if DoesFunctionHaveValidFlowOwnerFunctionSignature() is true")); + + const UClass* ExistingParamsClass = GetExistingParamsClass(); + checkf(IsValid(ExistingParamsClass), TEXT("This should be assured, if bHasParams == true")); + + // Check if the params (existing) are compatible with the function's expected (required) params + if (!ExistingParamsClass->IsChildOf(RequiredParamsClass)) + { + ValidationLog.Error(TEXT("Params object is not of the correct type for the flow owner function"), this); + + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} + +FString UFlowNode_CallOwnerFunction::GetStatusString() const +{ + if (ActivationState != EFlowNodeState::NeverActivated) + { + return UEnum::GetDisplayValueAsText(ActivationState).ToString(); + } + + return Super::GetStatusString(); +} + +UClass* UFlowNode_CallOwnerFunction::TryGetExpectedOwnerClass() const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + if (IsValid(FlowAsset)) + { + return FlowAsset->GetExpectedOwnerClass(); + } + + return nullptr; +} + +bool UFlowNode_CallOwnerFunction::DoesFunctionHaveValidFlowOwnerFunctionSignature(const UFunction& Function) +{ + if (GetParamsClassForFunction(Function) == nullptr) + { + return false; + } + + checkf(Function.NumParms == 2, TEXT("This should be checked in GetParamsClassForFunction()")); + + if (!DoesFunctionHaveNameReturnType(Function)) + { + return false; + } + + return true; +} + +bool UFlowNode_CallOwnerFunction::DoesFunctionHaveNameReturnType(const UFunction& Function) +{ + checkf(Function.NumParms == 2, TEXT("This should have already been checked in DoesFunctionHaveValidFlowOwnerFunctionSignature()")); + + TFieldIterator Iterator(&Function); + + while (Iterator) + { + const bool bIsOutParm = EnumHasAllFlags(Iterator->PropertyFlags, CPF_Parm | CPF_OutParm); + + return bIsOutParm; + } + + return false; +} + +UClass* UFlowNode_CallOwnerFunction::GetParamsClassForFunction(const UFunction& Function) +{ + if (Function.NumParms != 2) + { + // Flow Owner Functions expect exactly two parameters: + // - FFlowOwnerFunctionParams* + // - FName (return) + // See FFlowOwnerFunctionSignature + + return nullptr; + } + + TFieldIterator Iterator(&Function); + + while (Iterator && (Iterator->PropertyFlags & CPF_Parm)) + { + const FObjectPropertyBase* Prop = *Iterator; + check(Prop); + + UClass* PropertyClass = Prop->PropertyClass; + + if (!IsValid(PropertyClass)) + { + return nullptr; + } + + if (!PropertyClass->IsChildOf()) + { + return nullptr; + } + + return PropertyClass; + } + + return nullptr; +} + +UClass* UFlowNode_CallOwnerFunction::GetParamsClassForFunctionName(const UClass& ExpectedOwnerClass, const FName& FunctionName) +{ + const UFunction* Function = ExpectedOwnerClass.FindFunctionByName(FunctionName); + if (IsValid(Function)) + { + return GetParamsClassForFunction(*Function); + } + + return nullptr; +} + +bool UFlowNode_CallOwnerFunction::TryAllocateParamsInstance() +{ + FObjectPropertyBase* ParamsProperty = FindFProperty(GetClass(), GET_MEMBER_NAME_CHECKED(UFlowNode_CallOwnerFunction, Params)); + check(ParamsProperty); + + UClass* RequiredParamsClass = GetRequiredParamsClass(); + + if (ParamsProperty) + { + // Update the property to filter for just this class (and its subclasses) + ParamsProperty->SetPropertyClass(RequiredParamsClass); + } + + if (FunctionRef.GetFunctionName().IsNone()) + { + return false; + } + + const UClass* ExistingParamsClass = GetExistingParamsClass(); + + const bool bNeedsAllocateParams = + !IsValid(ExistingParamsClass) || + !ExistingParamsClass->IsChildOf(RequiredParamsClass); + + if (!bNeedsAllocateParams) + { + return false; + } + + // Throw out the old params object (if any) + Params = nullptr; + + // Create the new params object + Params = NewObject(this, RequiredParamsClass); + + return true; +} + +UClass* UFlowNode_CallOwnerFunction::GetRequiredParamsClass() const +{ + const UClass* ExpectedOwnerClass = TryGetExpectedOwnerClass(); + if (!IsValid(ExpectedOwnerClass)) + { + return UFlowOwnerFunctionParams::StaticClass(); + } + + const FName FunctionNameAsName = FunctionRef.GetFunctionName(); + + if (FunctionNameAsName.IsNone()) + { + return UFlowOwnerFunctionParams::StaticClass(); + } + + UClass* RequiredParamsClass = GetParamsClassForFunctionName(*ExpectedOwnerClass, FunctionNameAsName); + return RequiredParamsClass; +} + +UClass* UFlowNode_CallOwnerFunction::GetExistingParamsClass() const +{ + if (!IsValid(Params)) + { + return nullptr; + } + + UClass* ExistingParamsClass = Params->GetClass(); + return ExistingParamsClass; +} + +bool UFlowNode_CallOwnerFunction::IsAcceptableParamsPropertyClass(const UClass* ParamsClass) const +{ + if (!IsValid(ParamsClass)) + { + return false; + } + + if (!ParamsClass->IsChildOf()) + { + return false; + } + + const UClass* ExistingParamsClass = GetExistingParamsClass(); + + if (IsValid(ExistingParamsClass) && ParamsClass != ExistingParamsClass) + { + return false; + } + + return true; +} + +FText UFlowNode_CallOwnerFunction::GetNodeTitle() const +{ + const bool bUseAdaptiveNodeTitles = UFlowSettings::Get()->bUseAdaptiveNodeTitles; + + if (bUseAdaptiveNodeTitles && !FunctionRef.GetFunctionName().IsNone()) + { + const FText FunctionNameText = FText::FromName(FunctionRef.FunctionName); + + return FText::Format(LOCTEXT("CallOwnerFunction", "Call {0}"), { FunctionNameText }); + } + else + { + return Super::GetNodeTitle(); + } +} + +#endif // WITH_EDITOR + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/World/FlowNode_ComponentObserver.cpp b/Source/Flow/Private/Nodes/World/FlowNode_ComponentObserver.cpp index 2d4e76b61..73151f305 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_ComponentObserver.cpp +++ b/Source/Flow/Private/Nodes/World/FlowNode_ComponentObserver.cpp @@ -1,8 +1,11 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/World/FlowNode_ComponentObserver.h" #include "FlowSubsystem.h" UFlowNode_ComponentObserver::UFlowNode_ComponentObserver(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , IdentityMatchType(EFlowTagContainerMatchType::HasAnyExact) , SuccessLimit(1) , SuccessCount(0) { @@ -15,16 +18,6 @@ UFlowNode_ComponentObserver::UFlowNode_ComponentObserver(const FObjectInitialize OutputPins = {FFlowPin(TEXT("Success")), FFlowPin(TEXT("Completed")), FFlowPin(TEXT("Stopped"))}; } -void UFlowNode_ComponentObserver::PostLoad() -{ - Super::PostLoad(); - - if (IdentityTag_DEPRECATED.IsValid()) - { - IdentityTags = FGameplayTagContainer(IdentityTag_DEPRECATED); - } -} - void UFlowNode_ComponentObserver::ExecuteInput(const FName& PinName) { if (IdentityTags.IsValid()) @@ -54,27 +47,46 @@ void UFlowNode_ComponentObserver::OnLoad_Implementation() void UFlowNode_ComponentObserver::StartObserving() { - for (const TWeakObjectPtr& FoundComponent : GetFlowSubsystem()->GetComponents(IdentityTags, EGameplayContainerMatchType::Any)) + if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - ObserveActor(FoundComponent->GetOwner(), FoundComponent); - } - - GetFlowSubsystem()->OnComponentRegistered.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentRegistered); - GetFlowSubsystem()->OnComponentTagAdded.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagAdded); + // translate Flow name into engine types + const EGameplayContainerMatchType ContainerMatchType = (IdentityMatchType == EFlowTagContainerMatchType::HasAny || IdentityMatchType == EFlowTagContainerMatchType::HasAnyExact) ? EGameplayContainerMatchType::Any : EGameplayContainerMatchType::All; + const bool bExactMatch = (IdentityMatchType == EFlowTagContainerMatchType::HasAnyExact || IdentityMatchType == EFlowTagContainerMatchType::HasAllExact); - GetFlowSubsystem()->OnComponentUnregistered.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentUnregistered); - GetFlowSubsystem()->OnComponentTagRemoved.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagRemoved); + // collect already registered components + for (const TWeakObjectPtr& FoundComponent : FlowSubsystem->GetComponents(IdentityTags, ContainerMatchType, bExactMatch)) + { + ObserveActor(FoundComponent->GetOwner(), FoundComponent); + + // node might finish work immediately as the effect of ObserveActor() + // we should terminate iteration in this case + if (GetActivationState() != EFlowNodeState::Active) + { + return; + } + } + + FlowSubsystem->OnComponentRegistered.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentRegistered); + FlowSubsystem->OnComponentTagAdded.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagAdded); + FlowSubsystem->OnComponentTagRemoved.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagRemoved); + FlowSubsystem->OnComponentUnregistered.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentUnregistered); + } } void UFlowNode_ComponentObserver::StopObserving() { - GetFlowSubsystem()->OnComponentRegistered.RemoveAll(this); - GetFlowSubsystem()->OnComponentUnregistered.RemoveAll(this); + if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + { + FlowSubsystem->OnComponentRegistered.RemoveAll(this); + FlowSubsystem->OnComponentUnregistered.RemoveAll(this); + FlowSubsystem->OnComponentTagAdded.RemoveAll(this); + FlowSubsystem->OnComponentTagRemoved.RemoveAll(this); + } } void UFlowNode_ComponentObserver::OnComponentRegistered(UFlowComponent* Component) { - if (!RegisteredActors.Contains(Component->GetOwner()) && Component->IdentityTags.HasAnyExact(IdentityTags)) + if (!RegisteredActors.Contains(Component->GetOwner()) && FlowTypes::HasMatchingTags(Component->IdentityTags, IdentityTags, IdentityMatchType) == true) { ObserveActor(Component->GetOwner(), Component); } @@ -82,24 +94,24 @@ void UFlowNode_ComponentObserver::OnComponentRegistered(UFlowComponent* Componen void UFlowNode_ComponentObserver::OnComponentTagAdded(UFlowComponent* Component, const FGameplayTagContainer& AddedTags) { - if (!RegisteredActors.Contains(Component->GetOwner()) && AddedTags.HasAnyExact(IdentityTags)) + if (!RegisteredActors.Contains(Component->GetOwner()) && FlowTypes::HasMatchingTags(Component->IdentityTags, IdentityTags, IdentityMatchType) == true) { ObserveActor(Component->GetOwner(), Component); } } -void UFlowNode_ComponentObserver::OnComponentUnregistered(UFlowComponent* Component) +void UFlowNode_ComponentObserver::OnComponentTagRemoved(UFlowComponent* Component, const FGameplayTagContainer& RemovedTags) { - if (RegisteredActors.Contains(Component->GetOwner()) && Component->IdentityTags.HasAnyExact(IdentityTags)) + if (RegisteredActors.Contains(Component->GetOwner()) && FlowTypes::HasMatchingTags(Component->IdentityTags, IdentityTags, IdentityMatchType) == false) { RegisteredActors.Remove(Component->GetOwner()); ForgetActor(Component->GetOwner(), Component); } } -void UFlowNode_ComponentObserver::OnComponentTagRemoved(UFlowComponent* Component, const FGameplayTagContainer& RemovedTags) +void UFlowNode_ComponentObserver::OnComponentUnregistered(UFlowComponent* Component) { - if (RegisteredActors.Contains(Component->GetOwner()) && RemovedTags.HasAnyExact(IdentityTags)) + if (RegisteredActors.Contains(Component->GetOwner())) { RegisteredActors.Remove(Component->GetOwner()); ForgetActor(Component->GetOwner(), Component); @@ -109,7 +121,7 @@ void UFlowNode_ComponentObserver::OnComponentTagRemoved(UFlowComponent* Componen void UFlowNode_ComponentObserver::OnEventReceived() { TriggerFirstOutput(false); - + SuccessCount++; if (SuccessLimit > 0 && SuccessCount == SuccessLimit) { @@ -121,9 +133,12 @@ void UFlowNode_ComponentObserver::Cleanup() { StopObserving(); - for (const TPair, TWeakObjectPtr>& RegisteredActor : RegisteredActors) + if (RegisteredActors.Num() > 0) { - ForgetActor(RegisteredActor.Key, RegisteredActor.Value); + for (const TPair, TWeakObjectPtr>& RegisteredActor : RegisteredActors) + { + ForgetActor(RegisteredActor.Key, RegisteredActor.Value); + } } RegisteredActors.Empty(); @@ -136,6 +151,17 @@ FString UFlowNode_ComponentObserver::GetNodeDescription() const return GetIdentityTagsDescription(IdentityTags); } +EDataValidationResult UFlowNode_ComponentObserver::ValidateNode() +{ + if (IdentityTags.IsEmpty()) + { + ValidationLog.Error(*UFlowNode::MissingIdentityTag, this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} + FString UFlowNode_ComponentObserver::GetStatusString() const { if (ActivationState == EFlowNodeState::Active && RegisteredActors.Num() == 0) diff --git a/Source/Flow/Private/Nodes/World/FlowNode_NotifyActor.cpp b/Source/Flow/Private/Nodes/World/FlowNode_NotifyActor.cpp index 6f1f67309..dd3303c7b 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_NotifyActor.cpp +++ b/Source/Flow/Private/Nodes/World/FlowNode_NotifyActor.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/World/FlowNode_NotifyActor.h" #include "FlowComponent.h" #include "FlowSubsystem.h" @@ -6,35 +8,21 @@ UFlowNode_NotifyActor::UFlowNode_NotifyActor(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bExactMatch(true) + , NetMode(EFlowNetMode::Authority) { #if WITH_EDITOR Category = TEXT("Notifies"); #endif } -void UFlowNode_NotifyActor::PostLoad() -{ - Super::PostLoad(); - - if (IdentityTag_DEPRECATED.IsValid()) - { - IdentityTags = FGameplayTagContainer(IdentityTag_DEPRECATED); - } - - if (NotifyTag_DEPRECATED.IsValid()) - { - NotifyTags = FGameplayTagContainer(NotifyTag_DEPRECATED); - NotifyTag_DEPRECATED = FGameplayTag(); - } -} - void UFlowNode_NotifyActor::ExecuteInput(const FName& PinName) { - if (UFlowSubsystem* FlowSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) + if (const UFlowSubsystem* FlowSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) { - for (TWeakObjectPtr& Component : FlowSubsystem->GetComponents(IdentityTags, EGameplayContainerMatchType::Any)) + for (const TWeakObjectPtr& Component : FlowSubsystem->GetComponents(IdentityTags, EGameplayContainerMatchType::Any, bExactMatch)) { - Component->NotifyFromGraph(NotifyTags); + Component->NotifyFromGraph(NotifyTags, NetMode); } } @@ -46,4 +34,15 @@ FString UFlowNode_NotifyActor::GetNodeDescription() const { return GetIdentityTagsDescription(IdentityTags) + LINE_TERMINATOR + GetNotifyTagsDescription(NotifyTags); } + +EDataValidationResult UFlowNode_NotifyActor::ValidateNode() +{ + if (IdentityTags.IsEmpty()) + { + ValidationLog.Error(*UFlowNode::MissingIdentityTag, this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} #endif diff --git a/Source/Flow/Private/Nodes/World/FlowNode_OnActorRegistered.cpp b/Source/Flow/Private/Nodes/World/FlowNode_OnActorRegistered.cpp new file mode 100644 index 000000000..ba6a712ef --- /dev/null +++ b/Source/Flow/Private/Nodes/World/FlowNode_OnActorRegistered.cpp @@ -0,0 +1,17 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/World/FlowNode_OnActorRegistered.h" + +UFlowNode_OnActorRegistered::UFlowNode_OnActorRegistered(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UFlowNode_OnActorRegistered::ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) +{ + if (!RegisteredActors.Contains(Actor)) + { + RegisteredActors.Emplace(Actor, Component); + OnEventReceived(); + } +} diff --git a/Source/Flow/Private/Nodes/World/FlowNode_OnActorUnregistered.cpp b/Source/Flow/Private/Nodes/World/FlowNode_OnActorUnregistered.cpp new file mode 100644 index 000000000..488e63b2f --- /dev/null +++ b/Source/Flow/Private/Nodes/World/FlowNode_OnActorUnregistered.cpp @@ -0,0 +1,24 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/World/FlowNode_OnActorUnregistered.h" + +UFlowNode_OnActorUnregistered::UFlowNode_OnActorUnregistered(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UFlowNode_OnActorUnregistered::ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) +{ + if (!RegisteredActors.Contains(Actor)) + { + RegisteredActors.Emplace(Actor, Component); + } +} + +void UFlowNode_OnActorUnregistered::ForgetActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) +{ + if (ActivationState == EFlowNodeState::Active) + { + OnEventReceived(); + } +} diff --git a/Source/Flow/Private/Nodes/World/FlowNode_OnNotifyFromActor.cpp b/Source/Flow/Private/Nodes/World/FlowNode_OnNotifyFromActor.cpp index 131128097..477f1a1ed 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_OnNotifyFromActor.cpp +++ b/Source/Flow/Private/Nodes/World/FlowNode_OnNotifyFromActor.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/World/FlowNode_OnNotifyFromActor.h" #include "FlowComponent.h" @@ -11,21 +13,6 @@ UFlowNode_OnNotifyFromActor::UFlowNode_OnNotifyFromActor(const FObjectInitialize #endif } -void UFlowNode_OnNotifyFromActor::PostLoad() -{ - Super::PostLoad(); - - if (NotifyTag_DEPRECATED.IsValid()) - { - NotifyTags = FGameplayTagContainer(NotifyTag_DEPRECATED); - } -} - -void UFlowNode_OnNotifyFromActor::ExecuteInput(const FName& PinName) -{ - Super::ExecuteInput(PinName); -} - void UFlowNode_OnNotifyFromActor::ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) { if (!RegisteredActors.Contains(Actor)) diff --git a/Source/Flow/Private/Nodes/World/FlowNode_PlayLevelSequence.cpp b/Source/Flow/Private/Nodes/World/FlowNode_PlayLevelSequence.cpp index da1bb26b4..60aa544e1 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_PlayLevelSequence.cpp +++ b/Source/Flow/Private/Nodes/World/FlowNode_PlayLevelSequence.cpp @@ -1,7 +1,11 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/World/FlowNode_PlayLevelSequence.h" +#include "FlowAsset.h" #include "FlowModule.h" #include "FlowSubsystem.h" +#include "Launch/Resources/Version.h" #include "LevelSequence/FlowLevelSequencePlayer.h" #include "MovieScene/MovieSceneFlowTrack.h" #include "MovieScene/MovieSceneFlowTriggerSection.h" @@ -15,8 +19,14 @@ FFlowNodeLevelSequenceEvent UFlowNode_PlayLevelSequence::OnPlaybackCompleted; UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bPlayReverse(false) + , bUseGraphOwnerAsTransformOrigin(false) + , bReplicates(false) + , bAlwaysRelevant(false) + , bApplyOwnerTimeDilation(true) , LoadedSequence(nullptr) , SequencePlayer(nullptr) + , CachedPlayRate(0) , StartTime(0.0f) , ElapsedTime(0.0f) , TimeDilation(1.0f) @@ -28,6 +38,8 @@ UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence(const FObjectInitialize InputPins.Empty(); InputPins.Add(FFlowPin(TEXT("Start"))); + InputPins.Add(FFlowPin(TEXT("Pause"))); + InputPins.Add(FFlowPin(TEXT("Resume"))); InputPins.Add(FFlowPin(TEXT("Stop"))); OutputPins.Add(FFlowPin(TEXT("PreStart"))); @@ -37,14 +49,14 @@ UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence(const FObjectInitialize } #if WITH_EDITOR -TArray UFlowNode_PlayLevelSequence::GetContextOutputs() +TArray UFlowNode_PlayLevelSequence::GetContextOutputs() { if (Sequence.IsNull()) { - return TArray(); + return TArray(); } - TArray PinNames = {}; + TArray Pins = {}; Sequence = Sequence.LoadSynchronous(); if (Sequence && Sequence->GetMovieScene()) @@ -61,7 +73,7 @@ TArray UFlowNode_PlayLevelSequence::GetContextOutputs() { if (!EventName.IsEmpty()) { - PinNames.Emplace(EventName); + Pins.Emplace(EventName); } } } @@ -70,7 +82,7 @@ TArray UFlowNode_PlayLevelSequence::GetContextOutputs() } } - return PinNames; + return Pins; } void UFlowNode_PlayLevelSequence::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) @@ -108,14 +120,39 @@ void UFlowNode_PlayLevelSequence::FlushContent() } } -void UFlowNode_PlayLevelSequence::CreatePlayer(const FMovieSceneSequencePlaybackSettings& PlaybackSettings) +void UFlowNode_PlayLevelSequence::InitializeInstance() +{ + Super::InitializeInstance(); + + // Cache Play Rate set by user + CachedPlayRate = PlaybackSettings.PlayRate; +} + +void UFlowNode_PlayLevelSequence::CreatePlayer() { - LoadedSequence = LoadAsset(Sequence); + LoadedSequence = Sequence.LoadSynchronous(); if (LoadedSequence) { ALevelSequenceActor* SequenceActor; - SequencePlayer = UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer(this, LoadedSequence, PlaybackSettings, SequenceActor); - SequencePlayer->SetFlowEventReceiver(this); + + AActor* OwningActor = TryGetRootFlowActorOwner(); + + // Apply AActor::CustomTimeDilation from owner of the Root Flow + if (IsValid(OwningActor)) + { + PlaybackSettings.PlayRate = CachedPlayRate * OwningActor->CustomTimeDilation; + } + + // Apply Transform Origin + AActor* TransformOriginActor = bUseGraphOwnerAsTransformOrigin ? OwningActor : nullptr; + + // Finally create the player + SequencePlayer = UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer(this, LoadedSequence, PlaybackSettings, CameraSettings, TransformOriginActor, bReplicates, bAlwaysRelevant, SequenceActor); + + if (SequencePlayer) + { + SequencePlayer->SetFlowEventReceiver(this); + } const FFrameRate FrameRate = LoadedSequence->GetMovieScene()->GetTickResolution(); const FFrameNumber PlaybackStartFrame = LoadedSequence->GetMovieScene()->GetPlaybackRange().GetLowerBoundValue(); @@ -127,18 +164,29 @@ void UFlowNode_PlayLevelSequence::ExecuteInput(const FName& PinName) { if (PinName == TEXT("Start")) { - LoadedSequence = LoadAsset(Sequence); + LoadedSequence = Sequence.LoadSynchronous(); if (GetFlowSubsystem()->GetWorld() && LoadedSequence) { - CreatePlayer(FMovieSceneSequencePlaybackSettings()); + CreatePlayer(); - TriggerOutput(TEXT("PreStart")); + if (SequencePlayer) + { + TriggerOutput(TEXT("PreStart")); - SequencePlayer->OnFinished.AddDynamic(this, &UFlowNode_PlayLevelSequence::OnPlaybackFinished); - SequencePlayer->Play(); + SequencePlayer->OnFinished.AddDynamic(this, &UFlowNode_PlayLevelSequence::OnPlaybackFinished); - TriggerOutput(TEXT("Started")); + if (bPlayReverse) + { + SequencePlayer->PlayReverse(); + } + else + { + SequencePlayer->Play(); + } + + TriggerOutput(TEXT("Started")); + } } TriggerFirstOutput(false); @@ -147,6 +195,14 @@ void UFlowNode_PlayLevelSequence::ExecuteInput(const FName& PinName) { StopPlayback(); } + else if (PinName == TEXT("Pause")) + { + SequencePlayer->Pause(); + } + else if (PinName == TEXT("Resume") && SequencePlayer->IsPaused()) + { + SequencePlayer->Play(); + } } void UFlowNode_PlayLevelSequence::OnSave_Implementation() @@ -161,16 +217,29 @@ void UFlowNode_PlayLevelSequence::OnLoad_Implementation() { if (ElapsedTime != 0.0f) { - LoadedSequence = LoadAsset(Sequence); - + LoadedSequence = Sequence.LoadSynchronous(); if (GetFlowSubsystem()->GetWorld() && LoadedSequence) { - CreatePlayer(FMovieSceneSequencePlaybackSettings()); - SequencePlayer->OnFinished.AddDynamic(this, &UFlowNode_PlayLevelSequence::OnPlaybackFinished); + CreatePlayer(); + + if (SequencePlayer) + { + SequencePlayer->OnFinished.AddDynamic(this, &UFlowNode_PlayLevelSequence::OnPlaybackFinished); + + SequencePlayer->SetPlaybackPosition(FMovieSceneSequencePlaybackParams(ElapsedTime, EUpdatePositionMethod::Jump)); - SequencePlayer->SetPlayRate(TimeDilation); - SequencePlayer->SetPlaybackPosition(FMovieSceneSequencePlaybackParams(ElapsedTime, EUpdatePositionMethod::Jump)); - SequencePlayer->Play(); + // Take into account Play Rate set in the Playback Settings + SequencePlayer->SetPlayRate(TimeDilation * CachedPlayRate); + + if (bPlayReverse) + { + SequencePlayer->PlayReverse(); + } + else + { + SequencePlayer->Play(); + } + } } } } @@ -185,7 +254,9 @@ void UFlowNode_PlayLevelSequence::OnTimeDilationUpdate(const float NewTimeDilati if (SequencePlayer) { TimeDilation = NewTimeDilation; - SequencePlayer->SetPlayRate(NewTimeDilation); + + // Take into account Play Rate set in the Playback Settings + SequencePlayer->SetPlayRate(NewTimeDilation * CachedPlayRate); } } @@ -210,7 +281,10 @@ void UFlowNode_PlayLevelSequence::Cleanup() { SequencePlayer->SetFlowEventReceiver(nullptr); SequencePlayer->OnFinished.RemoveAll(this); - SequencePlayer->Stop(); + if (!PlaybackSettings.bPauseAtEnd) + { + SequencePlayer->Stop(); + } SequencePlayer = nullptr; } @@ -228,7 +302,7 @@ FString UFlowNode_PlayLevelSequence::GetPlaybackProgress() const { if (SequencePlayer && SequencePlayer->IsPlaying()) { - return GetProgressAsString(SequencePlayer->GetCurrentTime().AsSeconds() - StartTime).Append(TEXT(" / ")).Append(GetProgressAsString(SequencePlayer->GetDuration().AsSeconds())); + return FString::Printf(TEXT("%.*f / %.*f"), 2, SequencePlayer->GetCurrentTime().AsSeconds() - StartTime, 2, SequencePlayer->GetDuration().AsSeconds()); } return FString(); @@ -240,6 +314,17 @@ FString UFlowNode_PlayLevelSequence::GetNodeDescription() const return Sequence.IsNull() ? TEXT("[No sequence]") : Sequence.GetAssetName(); } +EDataValidationResult UFlowNode_PlayLevelSequence::ValidateNode() +{ + if (Sequence.IsNull()) + { + ValidationLog.Error(TEXT("Level Sequence asset not assigned or invalid!"), this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} + FString UFlowNode_PlayLevelSequence::GetStatusString() const { return GetPlaybackProgress(); @@ -247,7 +332,7 @@ FString UFlowNode_PlayLevelSequence::GetStatusString() const UObject* UFlowNode_PlayLevelSequence::GetAssetToEdit() { - return Sequence.IsNull() ? nullptr : LoadAsset(Sequence); + return Sequence.IsNull() ? nullptr : Sequence.LoadSynchronous(); } #endif diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 4fe578d94..bae194724 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -1,13 +1,16 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "CoreMinimal.h" +#include "FlowMessageLog.h" #include "FlowSave.h" #include "FlowTypes.h" +#include "Nodes/FlowNode.h" + +#include "UObject/ObjectKey.h" #include "FlowAsset.generated.h" -class UFlowNode; class UFlowNode_CustomInput; -class UFlowNode_Start; class UFlowNode_SubGraph; class UFlowSubsystem; @@ -28,7 +31,8 @@ class FLOW_API IFlowGraphInterface virtual void OnOutputTriggered(UEdGraphNode* GraphNode, const int32 Index) const {} }; -DECLARE_DELEGATE(FFlowAssetEvent); +DECLARE_DELEGATE(FFlowGraphEvent); + #endif /** @@ -38,13 +42,22 @@ UCLASS(BlueprintType, hideCategories = Object) class FLOW_API UFlowAsset : public UObject { GENERATED_UCLASS_BODY() - friend class UFlowNode; friend class UFlowNode_CustomOutput; friend class UFlowNode_SubGraph; friend class UFlowSubsystem; friend class FFlowAssetDetails; + friend class FFlowNode_SubGraphDetails; + friend class UFlowGraphSchema; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow Asset") + FGuid AssetGuid; + + // Set it to False, if this asset is instantiated as Root Flow for owner that doesn't live in the world + // This allow to SaveGame support works properly, if owner of Root Flow would be Game Instance or its subsystem + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Flow Asset") + bool bWorldBound; ////////////////////////////////////////////////////////////////////////// // Graph @@ -56,12 +69,19 @@ class FLOW_API UFlowAsset : public UObject static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector); virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; virtual void PostDuplicate(bool bDuplicateForPIE) override; - virtual EDataValidationResult IsDataValid(TArray& ValidationErrors) override; // -- + + virtual EDataValidationResult ValidateAsset(FFlowMessageLog& MessageLog); + + // Returns whether the node class is allowed in this flow asset + bool IsNodeClassAllowed(const UClass* FlowNodeClass) const; + + static FString ValidationError_NodeClassNotAllowed; #endif // IFlowGraphInterface #if WITH_EDITORONLY_DATA + private: UPROPERTY() UEdGraph* FlowGraph; @@ -81,6 +101,15 @@ class FLOW_API UFlowAsset : public UObject ////////////////////////////////////////////////////////////////////////// // Nodes +protected: + TArray> AllowedNodeClasses; + TArray> DeniedNodeClasses; + + TArray> AllowedInSubgraphNodeClasses; + TArray> DeniedInSubgraphNodeClasses; + + bool bStartNodePlacedAsGhostNode; + private: UPROPERTY() TMap Nodes; @@ -89,34 +118,97 @@ class FLOW_API UFlowAsset : public UObject * Custom Inputs define custom entry points in graph, it's similar to blueprint Custom Events * Sub Graph node using this Flow Asset will generate context Input Pin for every valid Event name on this list */ - UPROPERTY(EditAnywhere, Category = "Flow") + UPROPERTY(EditAnywhere, Category = "Sub Graph") TArray CustomInputs; /** * Custom Outputs define custom graph outputs, this allow to send signals to the parent graph while executing this graph * Sub Graph node using this Flow Asset will generate context Output Pin for every valid Event name on this list */ - UPROPERTY(EditAnywhere, Category = "Flow") + UPROPERTY(EditAnywhere, Category = "Sub Graph") TArray CustomOutputs; public: #if WITH_EDITOR - FFlowAssetEvent OnSubGraphReconstructionRequested; + FFlowGraphEvent OnSubGraphReconstructionRequested; UFlowNode* CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode); void RegisterNode(const FGuid& NewGuid, UFlowNode* NewNode); void UnregisterNode(const FGuid& NodeGuid); -#endif // Processes all nodes and creates map of all pin connections void HarvestNodeConnections(); +#endif + + const TMap& GetNodes() const { return Nodes; } + UFlowNode* GetNode(const FGuid& Guid) const { return Nodes.FindRef(Guid); } + + template + T* GetNode(const FGuid& Guid) const + { + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNode must be derived from UFlowNode"); - UFlowNode* GetNode(const FGuid& Guid) const; - TMap GetNodes() const { return Nodes; } + if (UFlowNode* Node = Nodes.FindRef(Guid)) + { + return Cast(Node); + } - TArray GetCustomInputs() const { return CustomInputs; } - TArray GetCustomOutputs() const { return CustomOutputs; } + return nullptr; + } + + UFUNCTION(BlueprintPure, Category = "FlowAsset") + virtual UFlowNode* GetDefaultEntryNode() const; + +#if WITH_EDITOR +protected: + void AddCustomInput(const FName& EventName); + void RemoveCustomInput(const FName& EventName); + + void AddCustomOutput(const FName& EventName); + void RemoveCustomOutput(const FName& EventName); +#endif + +public: + const TArray& GetCustomInputs() const { return CustomInputs; } + const TArray& GetCustomOutputs() const { return CustomOutputs; } + + UFlowNode_CustomInput* TryFindCustomInputNodeByEventName(const FName& EventName) const; + + UFUNCTION(BlueprintPure, Category = "FlowAsset", meta = (DeterminesOutputType = "FlowNodeClass")) + TArray GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass); + + template + void GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, TArray& OutNodes) + { + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNodesInExecutionOrder must be derived from UFlowNode"); + + if (FirstIteratedNode) + { + TSet> IteratedNodes; + GetNodesInExecutionOrder_Recursive(FirstIteratedNode, IteratedNodes, OutNodes); + } + } + +protected: + template + void GetNodesInExecutionOrder_Recursive(UFlowNode* Node, TSet>& IteratedNodes, TArray& OutNodes) + { + IteratedNodes.Add(Node); + + if (T* NodeOfRequiredType = Cast(Node)) + { + OutNodes.Emplace(NodeOfRequiredType); + } + + for (UFlowNode* ConnectedNode : Node->GetConnectedNodes()) + { + if (ConnectedNode && !IteratedNodes.Contains(ConnectedNode)) + { + GetNodesInExecutionOrder_Recursive(ConnectedNode, IteratedNodes, OutNodes); + } + } + } ////////////////////////////////////////////////////////////////////////// // Instances of the template asset @@ -128,10 +220,14 @@ class FLOW_API UFlowAsset : public UObject #if WITH_EDITORONLY_DATA TWeakObjectPtr InspectedInstance; + + // Message log for storing runtime errors/notes/warnings that will only last until the next game run + // Log lives in the asset template, so it can be inspected after ending the PIE + TSharedPtr RuntimeLog; #endif public: - void AddInstance(UFlowAsset* NewInstance); + void AddInstance(UFlowAsset* Instance); int32 RemoveInstance(UFlowAsset* Instance); void ClearInstances(); @@ -143,12 +239,19 @@ class FLOW_API UFlowAsset : public UObject void SetInspectedInstance(const FName& NewInspectedInstanceName); UFlowAsset* GetInspectedInstance() const { return InspectedInstance.IsValid() ? InspectedInstance.Get() : nullptr; } - DECLARE_EVENT(UFlowAsset, FRegenerateToolbarsEvent); - FRegenerateToolbarsEvent& OnRegenerateToolbars() { return RegenerateToolbarsEvent; } - FRegenerateToolbarsEvent RegenerateToolbarsEvent; + DECLARE_EVENT(UFlowAsset, FRefreshDebuggerEvent); + + FRefreshDebuggerEvent& OnDebuggerRefresh() { return RefreshDebuggerEvent; } + FRefreshDebuggerEvent RefreshDebuggerEvent; + + DECLARE_EVENT_TwoParams(UFlowAsset, FRuntimeMessageEvent, UFlowAsset*, const TSharedRef&); + + FRuntimeMessageEvent& OnRuntimeMessageAdded() { return RuntimeMessageEvent; } + FRuntimeMessageEvent RuntimeMessageEvent; private: - void BroadcastRegenerateToolbars() const { RegenerateToolbarsEvent.Broadcast(); } + void BroadcastDebuggerRefresh() const; + void BroadcastRuntimeMessageAdded(const TSharedRef& Message); #endif ////////////////////////////////////////////////////////////////////////// @@ -168,13 +271,9 @@ class FLOW_API UFlowAsset : public UObject // Flow Asset instances created by SubGraph nodes placed in the current graph TMap, TWeakObjectPtr> ActiveSubGraphs; - // Execution of the graph always starts from this node, there can be only one StartNode in the graph - UPROPERTY() - UFlowNode_Start* StartNode; - // Optional entry points to the graph, similar to blueprint Custom Events UPROPERTY() - TMap CustomInputNodes; + TSet CustomInputNodes; UPROPERTY() TSet PreloadedNodes; @@ -190,34 +289,43 @@ class FLOW_API UFlowAsset : public UObject EFlowFinishPolicy FinishPolicy; public: - void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset* InTemplateAsset); + virtual void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset* InTemplateAsset); + virtual void DeinitializeInstance(); UFlowAsset* GetTemplateAsset() const { return TemplateAsset; } - + // Object that spawned Root Flow instance, i.e. World Settings or Player Controller // This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes UFUNCTION(BlueprintPure, Category = "Flow") UObject* GetOwner() const { return Owner.Get(); } template - TWeakObjectPtr GetOwner() const + TWeakObjectPtr GetOwner() const { return Owner.IsValid() ? Cast(Owner) : nullptr; } - virtual void PreloadNodes(); + // Returns the Owner as an Actor, or if Owner is a Component, return its Owner as an Actor + UFUNCTION(BlueprintPure, Category = "Flow") + AActor* TryFindActorOwner() const; + + // Opportunity to preload content of project-specific nodes + virtual void PreloadNodes() {} virtual void PreStartFlow(); virtual void StartFlow(); - - virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy); + + virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance = true); + + bool HasStartedFlow() const; + void TriggerCustomInput(const FName& EventName); // Get Flow Asset instance created by the given SubGraph node TWeakObjectPtr GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const; -private: - void TriggerCustomEvent(UFlowNode_SubGraph* Node, const FName& EventName); - void TriggerCustomOutput(const FName& EventName) const; +protected: + void TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* Node, const FName& EventName) const; + void TriggerCustomOutput(const FName& EventName); void TriggerInput(const FGuid& NodeGuid, const FName& PinName); @@ -229,8 +337,7 @@ class FLOW_API UFlowAsset : public UObject FName GetDisplayName() const; UFlowNode_SubGraph* GetNodeOwningThisAssetInstance() const; - UFlowAsset* GetMasterInstance() const; - UFlowNode* GetNodeInstance(const FGuid Guid) const; + UFlowAsset* GetParentInstance() const; // Are there any active nodes? UFUNCTION(BlueprintPure, Category = "Flow") @@ -238,25 +345,56 @@ class FLOW_API UFlowAsset : public UObject // Returns nodes that have any work left, not marked as Finished yet UFUNCTION(BlueprintPure, Category = "Flow") - TArray GetActiveNodes() const { return ActiveNodes; } + const TArray& GetActiveNodes() const { return ActiveNodes; } // Returns nodes active in the past, done their work UFUNCTION(BlueprintPure, Category = "Flow") - TArray GetRecordedNodes() const { return RecordedNodes; } + const TArray& GetRecordedNodes() const { return RecordedNodes; } ////////////////////////////////////////////////////////////////////////// -// SaveGame - - UFUNCTION(BlueprintCallable, Category = "Flow") +// Expected Owner Class support (for use with CallOwnerFunction nodes) + +public: + UClass* GetExpectedOwnerClass() const { return ExpectedOwnerClass; } + +protected: + // Expects to be owned (at runtime) by an object with this class (or one of its subclasses) + // NOTE - If the class is an AActor, and the flow asset is owned by a component, + // it will consider the component's owner for the AActor + UPROPERTY(EditAnywhere, Category = "Flow", meta = (MustImplement = "FlowOwnerInterface")) + TSubclassOf ExpectedOwnerClass; + +////////////////////////////////////////////////////////////////////////// +// SaveGame support + +public: + UFUNCTION(BlueprintCallable, Category = "SaveGame") FFlowAssetSaveData SaveInstance(TArray& SavedFlowInstances); - UFUNCTION(BlueprintCallable, Category = "Flow") + UFUNCTION(BlueprintCallable, Category = "SaveGame") void LoadInstance(const FFlowAssetSaveData& AssetRecord); +private: + void OnActivationStateLoaded(UFlowNode* Node); + protected: UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") void OnSave(); - + UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") void OnLoad(); + +public: + UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") + bool IsBoundToWorld(); + +////////////////////////////////////////////////////////////////////////// +// Utils + +#if WITH_EDITOR +public: + void LogError(const FString& MessageToLog, UFlowNode* Node); + void LogWarning(const FString& MessageToLog, UFlowNode* Node); + void LogNote(const FString& MessageToLog, UFlowNode* Node); +#endif }; diff --git a/Source/Flow/Public/FlowComponent.h b/Source/Flow/Public/FlowComponent.h index a282ae3e0..b800c2b07 100644 --- a/Source/Flow/Public/FlowComponent.h +++ b/Source/Flow/Public/FlowComponent.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Components/ActorComponent.h" @@ -5,6 +7,7 @@ #include "FlowSave.h" #include "FlowTypes.h" +#include "FlowOwnerInterface.h" #include "FlowComponent.generated.h" class UFlowAsset; @@ -39,7 +42,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FFlowComponentDynamicNotify, class * Base component of Flow System - makes possible to communicate between Actor, Flow Subsystem and Flow Graphs */ UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent)) -class FLOW_API UFlowComponent : public UActorComponent +class FLOW_API UFlowComponent : public UActorComponent, public IFlowOwnerInterface { GENERATED_UCLASS_BODY() @@ -92,6 +95,12 @@ class FLOW_API UFlowComponent : public UActorComponent UPROPERTY(BlueprintAssignable, Category = "Flow") FFlowComponentTagsReplicated OnIdentityTagsRemoved; +public: + void VerifyIdentityTags() const; + + UFUNCTION(BlueprintCallable, Category = "Flow") + void LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType = EFlowOnScreenMessageType::Permanent) const; + ////////////////////////////////////////////////////////////////////////// // Component sending Notify Tags to Flow Graph, or any other listener @@ -101,7 +110,7 @@ class FLOW_API UFlowComponent : public UActorComponent FGameplayTagContainer RecentlySentNotifyTags; public: - FGameplayTagContainer GetRecentlySentNotifyTags() const { return RecentlySentNotifyTags; } + const FGameplayTagContainer& GetRecentlySentNotifyTags() const { return RecentlySentNotifyTags; } // Send single notification from the actor to Flow graphs // If set on server, it always going to be replicated to clients @@ -187,14 +196,32 @@ class FLOW_API UFlowComponent : public UActorComponent // This will destroy instantiated Flow Asset - created from asset assigned on this component. UFUNCTION(BlueprintCallable, Category = "RootFlow") - void FinishRootFlow(const EFlowFinishPolicy FinishPolicy); + void FinishRootFlow(UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy); + + UFUNCTION(BlueprintPure, Category = "FlowSubsystem") + TSet GetRootInstances(const UObject* Owner) const; + + UFUNCTION(BlueprintPure, Category = "RootFlow", meta = (DeprecatedFunction, DeprecationMessage="Use GetRootInstances() instead.")) + UFlowAsset* GetRootFlowInstance() const; - UFUNCTION(BlueprintPure, Category = "RootFlow") - UFlowAsset* GetRootFlowInstance(); +////////////////////////////////////////////////////////////////////////// +// UFlowComponent overrideable events + +public: + // Called when a Root flow asset triggers a CustomOutput + UFUNCTION(BlueprintImplementableEvent, DisplayName = "OnTriggerRootFlowOutputEvent") + void BP_OnTriggerRootFlowOutputEvent(UFlowAsset* RootFlowInstance, const FName& EventName); + + virtual void OnTriggerRootFlowOutputEvent(UFlowAsset* RootFlowInstance, const FName& EventName) {} + + // UFlowAsset-only access + void OnTriggerRootFlowOutputEventDispatcher(UFlowAsset* RootFlowInstance, const FName& EventName); + // --- ////////////////////////////////////////////////////////////////////////// // SaveGame +public: UFUNCTION(BlueprintCallable, Category = "SaveGame") virtual void SaveRootFlow(TArray& SavedFlowInstances); diff --git a/Source/Flow/Public/FlowMessageLog.h b/Source/Flow/Public/FlowMessageLog.h new file mode 100644 index 000000000..58ad0495b --- /dev/null +++ b/Source/Flow/Public/FlowMessageLog.h @@ -0,0 +1,102 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Logging/TokenizedMessage.h" +#include "Misc/UObjectToken.h" + +class UFlowAsset; +class UFlowNode; + +#if WITH_EDITOR + +/** + * Message Log token that links to an element in Flow Graph + */ +class FLOW_API FFlowGraphToken : public IMessageToken +{ +private: + const TWeakObjectPtr GraphNode; + const FEdGraphPinReference GraphPin; + + explicit FFlowGraphToken(const UFlowAsset* InFlowAsset); + explicit FFlowGraphToken(const UFlowNode* InFlowNode); + explicit FFlowGraphToken(UEdGraphNode* InGraphNode, const UEdGraphPin* InPin); + +public: + /** Factory method, tokens can only be constructed as shared refs */ + static TSharedPtr Create(const UFlowAsset* InFlowAsset, FTokenizedMessage& Message); + static TSharedPtr Create(const UFlowNode* InFlowNode, FTokenizedMessage& Message); + static TSharedPtr Create(UEdGraphNode* InGraphNode, FTokenizedMessage& Message); + static TSharedPtr Create(const UEdGraphPin* InPin, FTokenizedMessage& Message); + + const UEdGraphNode* GetGraphNode() const { return GraphNode.Get(); } + const UEdGraphPin* GetPin() const { return GraphPin.Get(); } + + // IMessageToken + virtual EMessageToken::Type GetType() const override + { + return EMessageToken::EdGraph; + } +}; + +/** + * List of Message Log lines + */ +class FLOW_API FFlowMessageLog +{ +public: + static const FName LogName; + TArray> Messages; + +public: + FFlowMessageLog() + { + } + + template + TSharedRef Error(const TCHAR* Format, T* Object) + { + TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Error); + AddMessage(NAME_None, Format, Message, Object); + return Message; + } + + template + TSharedRef Warning(const TCHAR* Format, T* Object) + { + TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Warning); + AddMessage(NAME_None, Format, Message, Object); + return Message; + } + + template + TSharedRef Note(const TCHAR* Format, T* Object) + { + TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Info); + AddMessage(NAME_None, Format, Message, Object); + return Message; + } + +protected: + template + void AddMessage(FName MessageID, const TCHAR* Format, TSharedRef& Message, T* Object) + { + Message->SetIdentifier(MessageID); + + if (Object) + { + if (const TSharedPtr Token = FFlowGraphToken::Create(Object, Message.Get())) + { + Message->SetMessageLink(FUObjectToken::Create(Object)); + } + } + + Message.Get().AddToken(FTextToken::Create(FText::FromString(Format))); + Messages.Add(Message); + } +}; + +#endif // WITH_EDITOR diff --git a/Source/Flow/Public/FlowModule.h b/Source/Flow/Public/FlowModule.h index ef7b470ed..9244e2be4 100644 --- a/Source/Flow/Public/FlowModule.h +++ b/Source/Flow/Public/FlowModule.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Logging/LogMacros.h" diff --git a/Source/Flow/Public/FlowOwnerFunctionParams.h b/Source/Flow/Public/FlowOwnerFunctionParams.h new file mode 100644 index 000000000..f627fe875 --- /dev/null +++ b/Source/Flow/Public/FlowOwnerFunctionParams.h @@ -0,0 +1,83 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "CoreMinimal.h" + +#include "FlowOwnerFunctionParams.generated.h" + + +// Forward Declarations +class UFlowNode_CallOwnerFunction; + + +UCLASS(BlueprintType, Blueprintable, EditInlineNew) +class FLOW_API UFlowOwnerFunctionParams : public UObject +{ + GENERATED_BODY() + +public: + + UFlowOwnerFunctionParams(); + + void PreExecute(UFlowNode_CallOwnerFunction& InSourceNode, const FName& InputPinName); + void PostExecute(); + + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "FlowOwnerFunction") + bool ShouldFinishForOutputName(const FName& OutputName) const; + +#if WITH_EDITORONLY_DATA + const TArray& GetInputNames() const { return InputNames; } + const TArray& GetOutputNames() const { return OutputNames; } +#endif // WITH_EDITORONLY_DATA + + // Get the input/output pin names for the SourceNode. + // Slightly slower than the editor-only counterparts owing to the deep copy of the array. + // Valid only if called between PreExecute() and PostExecute(), inclusive + TArray GatherInputNames() const { return BP_GetInputNames(); } + TArray GatherOutputNames() const { return BP_GetOutputNames(); } + +protected: + + // Called prior to the owner executing the function described by this object. + // Can be overridden to prepare the stateful data before execution. + UFUNCTION(BlueprintImplementableEvent, Category = "FlowOwnerFunction", DisplayName = "PreExecute") + void BP_PreExecute(); + + // Cleans up the stateful data in this Params struct. + // Can be overridden to cleanup the stateful data after execution. + UFUNCTION(BlueprintImplementableEvent, Category = "FlowOwnerFunction", DisplayName = "PostExecute") + void BP_PostExecute(); + + // Get the input pin names for the SourceNode + // Valid only if called between PreExecute() and PostExecute(), inclusive + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FlowOwnerFunction", DisplayName = "GetInputNames") + TArray BP_GetInputNames() const; + + // Get the output pin names for the SourceNode + // Valid only if called between PreExecute() and PostExecute(), inclusive + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FlowOwnerFunction", DisplayName = "GetOutputNames") + TArray BP_GetOutputNames() const; + +protected: + + // CallOwnerObjectFunction node that is executing this set of function params. + // Valid only if called between PreExecute() and PostExecute(), inclusive + UPROPERTY(Transient, BlueprintReadOnly, Category = "FlowOwnerFunction") + UFlowNode_CallOwnerFunction* SourceNode = nullptr; + + // This is the Name from the Input Pin that caused this node to Execute. + // Valid only if called between PreExecute() and PostExecute(), inclusive + UPROPERTY(Transient, BlueprintReadOnly, Category = "FlowOwnerFunction") + FName ExecutedInputPinName; + +#if WITH_EDITORONLY_DATA + // Input pin names for this function + UPROPERTY(EditDefaultsOnly, Category = "FlowOwnerFunction") + TArray InputNames; + + // Output pin names for this function + UPROPERTY(EditDefaultsOnly, Category = "FlowOwnerFunction") + TArray OutputNames; +#endif // WITH_EDITORONLY_DATA +}; diff --git a/Source/Flow/Public/FlowOwnerFunctionRef.h b/Source/Flow/Public/FlowOwnerFunctionRef.h new file mode 100644 index 000000000..629949c8d --- /dev/null +++ b/Source/Flow/Public/FlowOwnerFunctionRef.h @@ -0,0 +1,59 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "CoreMinimal.h" +#include "Templates/SubclassOf.h" + +#include "FlowOwnerFunctionRef.generated.h" + + +// Forward Declarations +class UFlowOwnerFunctionParams; +class IFlowOwnerInterface; + + +// Similar to FAnimNodeFunctionRef, providing a FName-based function binding +// that is resolved at runtime +USTRUCT(BlueprintType) +struct FFlowOwnerFunctionRef +{ + GENERATED_BODY() + + // For GET_MEMBER_NAME_CHECKED access + friend class UFlowNode_CallOwnerFunction; + friend class FFlowOwnerFunctionRefCustomization; + +public: + + // Resolves the function and returns the UFunction + UFunction* TryResolveFunction(const UClass& InClass); + + // Returns a the resolved function + // (assumes TryResolveFunction was called previously) + UFunction* GetResolvedFunction() const { return Function; } + + // Call the function and return the Output Pin Name result + FName CallFunction(IFlowOwnerInterface& InFlowOwnerInterface, UFlowOwnerFunctionParams& InParams) const; + + // Accessors + FName GetFunctionName() const { return FunctionName; } + bool IsConfigured() const { return !FunctionName.IsNone(); } + bool IsResolved() const { return ::IsValid(Function); } + +protected: + + // The name of the function to call + UPROPERTY(VisibleAnywhere, Category = "FlowOwnerFunction") + FName FunctionName = NAME_None; + + // The function to call + // (resolved by looking for a function named FunctionName on the ExpectedOwnerClass) + UPROPERTY(Transient) + TObjectPtr Function = nullptr; + +#if WITH_EDITORONLY_DATA + UPROPERTY(VisibleAnywhere, Category = "FlowOwnerFunction", meta = (DisplayName = "Function Parameters Class")) + TSubclassOf ParamsClass; +#endif // WITH_EDITORONLY_DATA +}; diff --git a/Source/Flow/Public/FlowOwnerInterface.h b/Source/Flow/Public/FlowOwnerInterface.h new file mode 100644 index 000000000..a6ad53675 --- /dev/null +++ b/Source/Flow/Public/FlowOwnerInterface.h @@ -0,0 +1,22 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "CoreMinimal.h" + +#include "UObject/Interface.h" + +#include "FlowOwnerInterface.generated.h" + + +// (optional) interface to enable a Flow owner object to execute CallOwnerFunction nodes +UINTERFACE(MinimalAPI, Blueprintable, BlueprintType) +class UFlowOwnerInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowOwnerInterface +{ + GENERATED_BODY() +}; diff --git a/Source/Flow/Public/FlowSave.h b/Source/Flow/Public/FlowSave.h index 7c98ab50b..f79711f9c 100644 --- a/Source/Flow/Public/FlowSave.h +++ b/Source/Flow/Public/FlowSave.h @@ -1,8 +1,8 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "CoreMinimal.h" #include "GameFramework/SaveGame.h" -#include "Serialization/BufferArchive.h" #include "Serialization/ObjectAndNameAsStringProxyArchive.h" #include "FlowSave.generated.h" @@ -28,6 +28,9 @@ struct FLOW_API FFlowAssetSaveData { GENERATED_USTRUCT_BODY() + UPROPERTY(SaveGame, VisibleAnywhere, Category = "Flow") + FString WorldName; + UPROPERTY(SaveGame, VisibleAnywhere, Category = "Flow") FString InstanceName; diff --git a/Source/Flow/Public/FlowSettings.h b/Source/Flow/Public/FlowSettings.h index 608bae1ce..ddb491d13 100644 --- a/Source/Flow/Public/FlowSettings.h +++ b/Source/Flow/Public/FlowSettings.h @@ -1,7 +1,10 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Engine/DeveloperSettings.h" #include "Templates/SubclassOf.h" +#include "UObject/SoftObjectPath.h" #include "FlowSettings.generated.h" class UFlowNode; @@ -10,7 +13,7 @@ class UFlowNode; * */ UCLASS(Config = Game, defaultconfig, meta = (DisplayName = "Flow")) -class UFlowSettings final : public UDeveloperSettings +class FLOW_API UFlowSettings : public UDeveloperSettings { GENERATED_UCLASS_BODY() @@ -21,7 +24,33 @@ class UFlowSettings final : public UDeveloperSettings UPROPERTY(Config, EditAnywhere, Category = "Networking") bool bCreateFlowSubsystemOnClients; - // How many nodes of given class should be preloaded with the Flow Asset instance? - UPROPERTY(Config, EditAnywhere, Category = "Preload") - TMap, int32> DefaultPreloadDepth; + UPROPERTY(Config, EditAnywhere, Category = "SaveSystem") + bool bWarnAboutMissingIdentityTags; + + // If enabled, runtime logs will be added when a flow node signal mode is set to Disabled + UPROPERTY(Config, EditAnywhere, Category = "Flow") + bool bLogOnSignalDisabled; + + // If enabled, runtime logs will be added when a flow node signal mode is set to Pass-through + UPROPERTY(Config, EditAnywhere, Category = "Flow") + bool bLogOnSignalPassthrough; + + // Adjust the Titles for FlowNodes to be more expressive than default + // by incorporating data that would otherwise go in the Description + UPROPERTY(EditAnywhere, config, Category = "Nodes") + bool bUseAdaptiveNodeTitles; + + // Default class to use as a FlowAsset's "ExpectedOwnerClass" + UPROPERTY(EditAnywhere, Config, Category = "Nodes", meta = (MustImplement = "FlowOwnerInterface")) + FSoftClassPath DefaultExpectedOwnerClass; + +public: + UClass* GetDefaultExpectedOwnerClass() const; + + static UClass* TryResolveOrLoadSoftClass(const FSoftClassPath& SoftClassPath); + +#if WITH_EDITORONLY_DATA + virtual FName GetCategoryName() const override { return FName("Flow Graph"); } + virtual FText GetSectionText() const override { return INVTEXT("Settings"); } +#endif }; diff --git a/Source/Flow/Public/FlowSubsystem.h b/Source/Flow/Public/FlowSubsystem.h index 7f6ba58d2..4633eef40 100644 --- a/Source/Flow/Public/FlowSubsystem.h +++ b/Source/Flow/Public/FlowSubsystem.h @@ -1,6 +1,7 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "Engine/StreamableManager.h" #include "GameFramework/Actor.h" #include "GameplayTagContainer.h" #include "Subsystems/GameInstanceSubsystem.h" @@ -15,6 +16,8 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSimpleFlowEvent); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSimpleFlowComponentEvent, UFlowComponent*, Component); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FTaggedFlowComponentEvent, UFlowComponent*, Component, const FGameplayTagContainer&, Tags); +DECLARE_DELEGATE_OneParam(FNativeFlowAssetEvent, class UFlowAsset*); + /** * Flow Subsystem * - manages lifetime of Flow Graphs @@ -26,26 +29,36 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() +public: UFlowSubsystem(); friend class UFlowAsset; friend class UFlowComponent; friend class UFlowNode_SubGraph; - // All asset templates with active instances +private: + /* All asset templates with active instances */ UPROPERTY() TArray InstancedTemplates; - // Assets instanced by object from another system, i.e. World Settings or Player Controller + /* Assets instanced by object from another system, i.e. World Settings or Player Controller */ UPROPERTY() - TMap, UFlowAsset*> RootInstances; + TMap> RootInstances; - // Assets instanced by Sub Graph nodes + /* Assets instanced by Sub Graph nodes */ UPROPERTY() TMap InstancedSubFlows; - FStreamableManager Streamable; +#if WITH_EDITOR +public: + /* Called after creating the first instance of given Flow Asset */ + static FNativeFlowAssetEvent OnInstancedTemplateAdded; + /* Called just before removing the last instance of given Flow Asset */ + static FNativeFlowAssetEvent OnInstancedTemplateRemoved; +#endif + +protected: UPROPERTY() UFlowSaveGame* LoadedSaveGame; @@ -55,44 +68,56 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void AbortActiveFlows(); - // Start the root Flow, graph that will eventually instantiate next Flow Graphs through the SubGraph node - UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") - void StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true); + /* Start the root Flow, graph that will eventually instantiate next Flow Graphs through the SubGraph node */ + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem", meta = (DefaultToSelf = "Owner")) + virtual void StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true); -protected: - UFlowAsset* CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true); + virtual UFlowAsset* CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true); -public: - // Finish the root Flow, typically when closing world that created this flow - UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") - void FinishRootFlow(UObject* Owner, const EFlowFinishPolicy FinishPolicy); + /* Finish Policy value is read by Flow Node + * Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted + * Example: Spawn node might despawn all actors if Flow Graph is aborted, not completed */ + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem", meta = (DefaultToSelf = "Owner")) + virtual void FinishRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy); -private: - UFlowAsset* CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString NewInstanceName = FString(), const bool bPreloading = false); + /* Finish Policy value is read by Flow Node + * Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted + * Example: Spawn node might despawn all actors if Flow Graph is aborted, not completed */ + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem", meta = (DefaultToSelf = "Owner")) + virtual void FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy); + +protected: + UFlowAsset* CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString SavedInstanceName = FString(), const bool bPreloading = false); void RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlowFinishPolicy FinishPolicy); UFlowAsset* CreateFlowInstance(const TWeakObjectPtr Owner, TSoftObjectPtr FlowAsset, FString NewInstanceName = FString()); - void RemoveInstancedTemplate(UFlowAsset* Template); + + virtual void AddInstancedTemplate(UFlowAsset* Template); + virtual void RemoveInstancedTemplate(UFlowAsset* Template); public: - // Returns asset instanced by object from another system like World Settings + /* Returns all assets instanced by object from another system like World Settings */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem") - UFlowAsset* GetRootFlow(UObject* Owner) const { return RootInstances.FindRef(Owner); } + TMap GetRootInstances() const; - // Returns assets instanced by object from another system like World Settings + /* Returns asset instanced by specific object */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem") - TMap GetRootInstances() const; + TSet GetRootInstancesByOwner(const UObject* Owner) const; - // Returns assets instanced by Sub Graph nodes + UFUNCTION(BlueprintPure, Category = "FlowSubsystem", meta = (DeprecatedFunction, DeprecationMessage="Use GetRootInstancesByOwner() instead.")) + UFlowAsset* GetRootFlow(const UObject* Owner) const; + + /* Returns assets instanced by Sub Graph nodes */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem") - TMap GetInstancedSubFlows() const { return InstancedSubFlows; } - + const TMap& GetInstancedSubFlows() const { return InstancedSubFlows; } + virtual UWorld* GetWorld() const override; - + ////////////////////////////////////////////////////////////////////////// -// SaveGame +// SaveGame support UPROPERTY(BlueprintAssignable, Category = "FlowSubsystem") FSimpleFlowEvent OnSaveGame; @@ -103,149 +128,298 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void OnGameLoaded(UFlowSaveGame* SaveGame); + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void LoadRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const FString& SavedAssetInstanceName); + + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void LoadSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedAssetInstanceName); - + UFUNCTION(BlueprintPure, Category = "FlowSubsystem") UFlowSaveGame* GetLoadedSaveGame() const { return LoadedSaveGame; } ////////////////////////////////////////////////////////////////////////// // Component Registry -private: - // All the Flow Components currently existing in the world +protected: + /* All the Flow Components currently existing in the world */ TMultiMap> FlowComponentRegistry; protected: virtual void RegisterComponent(UFlowComponent* Component); virtual void OnIdentityTagAdded(UFlowComponent* Component, const FGameplayTag& AddedTag); virtual void OnIdentityTagsAdded(UFlowComponent* Component, const FGameplayTagContainer& AddedTags); - + virtual void UnregisterComponent(UFlowComponent* Component); virtual void OnIdentityTagRemoved(UFlowComponent* Component, const FGameplayTag& RemovedTag); virtual void OnIdentityTagsRemoved(UFlowComponent* Component, const FGameplayTagContainer& RemovedTags); public: - // Called when actor with Flow Component appears in the world + /* Called when actor with Flow Component appears in the world */ UPROPERTY(BlueprintAssignable, Category = "FlowSubsystem") FSimpleFlowComponentEvent OnComponentRegistered; - // Called after adding Identity Tags to already registered Flow Component - // This can happen only after Begin Play occured in the component + /* Called after adding Identity Tags to already registered Flow Component + * This can happen only after Begin Play occured in the component */ UPROPERTY(BlueprintAssignable, Category = "FlowSubsystem") FTaggedFlowComponentEvent OnComponentTagAdded; - // Called when actor with Flow Component disappears from the world + /* Called when actor with Flow Component disappears from the world */ UPROPERTY(BlueprintAssignable, Category = "FlowSubsystem") FSimpleFlowComponentEvent OnComponentUnregistered; - // Called after removing Identity Tags from the Flow Component, if component still has some Identity Tags - // This can happen only after Begin Play occured in the component + /* Called after removing Identity Tags from the Flow Component, if component still has some Identity Tags + * This can happen only after Begin Play occured in the component */ UPROPERTY(BlueprintAssignable, Category = "FlowSubsystem") FTaggedFlowComponentEvent OnComponentTagRemoved; - // Returns all registered Flow Components identified by given tag + /** + * Returns all registered Flow Components identified by given tag + * + * @param Tag Tag to check if it matches Identity Tags of registered Flow Components + * @param ComponentClass Only components matching this class we'll be returned + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem", meta = (DeterminesOutputType = "ComponentClass")) - TSet GetFlowComponentsByTag(const FGameplayTag Tag, const TSubclassOf ComponentClass) const; - - // Returns all registered Flow Components identified by Any or All provided tags + TSet GetFlowComponentsByTag(const FGameplayTag Tag, const TSubclassOf ComponentClass, const bool bExactMatch = true) const; + + /** + * Returns all registered Flow Components identified by Any or All provided tags + * + * @param Tags Container to check if it matches Identity Tags of registered Flow Components + * @param MatchType If Any, returned component needs to have only one of given tags. If All, component needs to have all given Identity Tags + * @param ComponentClass Only components matching this class we'll be returned + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem", meta = (DeterminesOutputType = "ComponentClass")) - TSet GetFlowComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ComponentClass) const; - - // Returns all registered actors with Flow Component identified by given tag + TSet GetFlowComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ComponentClass, const bool bExactMatch = true) const; + + /** + * Returns all registered actors with Flow Component identified by given tag + * + * @param Tag Tag to check if it matches Identity Tags of registered Flow Components + * @param ActorClass Only actors matching this class we'll be returned + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem", meta = (DeterminesOutputType = "ActorClass")) - TSet GetFlowActorsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass) const; - - // Returns all registered actors with Flow Component identified by Any or All provided tags + TSet GetFlowActorsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass, const bool bExactMatch = true) const; + + /** + * Returns all registered actors with Flow Component identified by Any or All provided tags + * + * @param Tags Container to check if it matches Identity Tags of registered Flow Components + * @param MatchType If Any, returned component needs to have only one of given tags. If All, component needs to have all given Identity Tags + * @param ActorClass Only actors matching this class we'll be returned + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem", meta = (DeterminesOutputType = "ActorClass")) - TSet GetFlowActorsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass) const; - - // Returns all registered actors as pairs: Actor as key, its Flow Component as value + TSet GetFlowActorsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass, const bool bExactMatch = true) const; + + /** + * Returns all registered actors as pairs: Actor as key, its Flow Component as value + * + * @param Tag Tag to check if it matches Identity Tags of registered Flow Components + * @param ActorClass Only actors matching this class we'll be returned + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem", meta = (DeterminesOutputType = "ActorClass")) - TMap GetFlowActorsAndComponentsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass) const; - - // Returns all registered actors as pairs: Actor as key, its Flow Component as value + TMap GetFlowActorsAndComponentsByTag(const FGameplayTag Tag, const TSubclassOf ActorClass, const bool bExactMatch = true) const; + + /** + * Returns all registered actors as pairs: Actor as key, its Flow Component as value + * + * @param Tags Container to check if it matches Identity Tags of registered Flow Components + * @param MatchType If Any, returned component needs to have only one of given tags. If All, component needs to have all given Identity Tags + * @param ActorClass Only actors matching this class we'll be returned + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem", meta = (DeterminesOutputType = "ActorClass")) - TMap GetFlowActorsAndComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass) const; - - // Returns all registered Flow Components identified by given tag + TMap GetFlowActorsAndComponentsByTags(const FGameplayTagContainer Tags, const EGameplayContainerMatchType MatchType, const TSubclassOf ActorClass, const bool bExactMatch = true) const; + + /** + * Returns all registered Flow Components identified by given tag + * + * @tparam T Only components matching this class we'll be returned + * @param Tag Tag to check if it matches Identity Tags of registered Flow Components + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ template - TSet> GetComponents(const FGameplayTag& Tag) const + TSet> GetComponents(const FGameplayTag& Tag, const bool bExactMatch = true) const { static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetComponents must be derived from UActorComponent"); TArray> FoundComponents; - FlowComponentRegistry.MultiFind(Tag, FoundComponents); + FindComponents(Tag, bExactMatch, FoundComponents); TSet> Result; for (const TWeakObjectPtr& Component : FoundComponents) { - if (Component.IsValid() && Component->GetClass()->IsChildOf(T::StaticClass())) + if (Component.IsValid()) { - Result.Emplace(Cast(Component)); + if (T* ComponentOfClass = Cast(Component)) + { + Result.Emplace(ComponentOfClass); + } } } return Result; } - // Returns all registered Flow Components identified by Any or All provided tags + /** + * Returns all registered Flow Components identified by Any or All provided tags + * + * @tparam T Only components matching this class we'll be returned + * @param Tags Container to check if it matches Identity Tags of registered Flow Components + * @param MatchType If Any, returned component needs to have only one of given tags. If All, component needs to have all given Identity Tags + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ template - TSet> GetComponents(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType) const + TSet> GetComponents(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType, const bool bExactMatch = true) const { static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetComponents must be derived from UActorComponent"); TSet> FoundComponents; - FindComponents(Tags, FoundComponents, MatchType); + FindComponents(Tags, MatchType, bExactMatch, FoundComponents); TSet> Result; for (const TWeakObjectPtr& Component : FoundComponents) { - if (Component.IsValid() && Component->GetClass()->IsChildOf(T::StaticClass())) + if (Component.IsValid()) { - Result.Emplace(Cast(Component)); + if (T* ComponentOfClass = Cast(Component)) + { + Result.Emplace(ComponentOfClass); + } } } return Result; } - // Returns all registered actors with Flow Component identified by given tag + /** + * Returns all registered Flow Components identified by given tag + * + * @tparam T Only components matching this class we'll be returned + * @param Tag Tag to check if it matches Identity Tags of registered Flow Components + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ template - TMap, TWeakObjectPtr> GetActors(const FGameplayTag& Tag) const + TSet> GetActors(const FGameplayTag& Tag, const bool bExactMatch = true) const { - static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetComponents must be derived from AActor"); + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetActors must be derived from AActor"); TArray> FoundComponents; - FlowComponentRegistry.MultiFind(Tag, FoundComponents); + FindComponents(Tag, bExactMatch, FoundComponents); - TMap, TWeakObjectPtr> Result; + TSet> Result; for (const TWeakObjectPtr& Component : FoundComponents) { - if (Component.IsValid() && Component->GetOwner()->GetClass()->IsChildOf(T::StaticClass())) + if (Component.IsValid()) { - Result.Emplace(Cast(Component->GetOwner()), Component); + if (T* ActorOfClass = Cast(Component->GetOwner())) + { + Result.Emplace(ActorOfClass); + } } } return Result; } - // Returns all registered actors with Flow Component identified by at least one of given tags + /** + * Returns all registered Flow Components identified by Any or All provided tags + * + * @tparam T Only actors matching this class we'll be returned + * @param Tags Container to check if it matches Identity Tags of registered Flow Components + * @param MatchType If Any, returned component needs to have only one of given tags. If All, component needs to have all given Identity Tags + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ template - TMap, TWeakObjectPtr> GetActors(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType) const + TSet> GetActors(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType, const bool bExactMatch = true) const + { + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetActors must be derived from AActor"); + + TSet> FoundComponents; + FindComponents(Tags, MatchType, bExactMatch, FoundComponents); + + TSet> Result; + for (const TWeakObjectPtr& Component : FoundComponents) + { + if (Component.IsValid()) + { + if (T* ActorOfClass = Cast(Component->GetOwner())) + { + Result.Emplace(ActorOfClass); + } + } + } + + return Result; + } + + /** + * Returns all registered actors with Flow Component identified by given tag + * + * @tparam ActorT Only actors matching this class we'll be returned + * @tparam ComponentT Only components matching this class we'll be returned + * @param Tag Tag to check if it matches Identity Tags of registered Flow Components + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ + template + TMap, TWeakObjectPtr> GetActorsAndComponents(const FGameplayTag& Tag, const bool bExactMatch = true) const + { + static_assert(TPointerIsConvertibleFromTo::Value, "'ActorT' template parameter to GetActorsAndComponents must be derived from AActor"); + static_assert(TPointerIsConvertibleFromTo::Value, "'ComponentT' template parameter to GetActorsAndComponents must be derived from UActorComponent"); + + TArray> FoundComponents; + FindComponents(Tag, bExactMatch, FoundComponents); + + TMap, TWeakObjectPtr> Result; + for (const TWeakObjectPtr& Component : FoundComponents) + { + if (Component.IsValid()) + { + ComponentT* ComponentOfClass = Cast(Component); + ActorT* ActorOfClass = Cast(Component->GetOwner()); + if (ComponentOfClass && ActorOfClass) + { + Result.Emplace(ActorOfClass, ComponentOfClass); + } + } + } + + return Result; + } + + /** + * Returns all registered actors with Flow Component identified by Any or All provided tags + * + * @tparam ActorT Only actors matching this class we'll be returned + * @tparam ComponentT Only components matching this class we'll be returned + * @param Tags Container to check if it matches Identity Tags of registered Flow Components + * @param MatchType If Any, returned component needs to have only one of given tags. If All, component needs to have all given Identity Tags + * @param bExactMatch If true, the tag has to be exactly present, if false then TagContainer will include it's parent tags while matching. Be careful, using latter option may be very expensive, as the search cost is proportional to the number of registered Gameplay Tags! + */ + template + TMap, TWeakObjectPtr> GetActorsAndComponents(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType, const bool bExactMatch = true) const { - static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetComponents must be derived from AActor"); + static_assert(TPointerIsConvertibleFromTo::Value, "'ActorT' template parameter to GetActorsAndComponents must be derived from AActor"); + static_assert(TPointerIsConvertibleFromTo::Value, "'ComponentT' template parameter to GetActorsAndComponents must be derived from UActorComponent"); TSet> FoundComponents; - FindComponents(Tags, FoundComponents, MatchType); + FindComponents(Tags, MatchType, bExactMatch, FoundComponents); - TMap, TWeakObjectPtr> Result; + TMap, TWeakObjectPtr> Result; for (const TWeakObjectPtr& Component : FoundComponents) { - if (Component.IsValid() && Component->GetOwner()->GetClass()->IsChildOf(T::StaticClass())) + if (Component.IsValid()) { - Result.Emplace(Cast(Component->GetOwner()), Component); + ComponentT* ComponentOfClass = Cast(Component); + ActorT* ActorOfClass = Cast(Component->GetOwner()); + if (ComponentOfClass && ActorOfClass) + { + Result.Emplace(ActorOfClass, ComponentOfClass); + } } } @@ -253,5 +427,6 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem } private: - void FindComponents(const FGameplayTagContainer& Tags, TSet>& OutComponents, const EGameplayContainerMatchType MatchType) const; -}; \ No newline at end of file + void FindComponents(const FGameplayTag& Tag, const bool bExactMatch, TArray>& OutComponents) const; + void FindComponents(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType, const bool bExactMatch, TSet>& OutComponents) const; +}; diff --git a/Source/Flow/Public/FlowTypes.h b/Source/Flow/Public/FlowTypes.h index b1a6b390a..2a0f3793b 100644 --- a/Source/Flow/Public/FlowTypes.h +++ b/Source/Flow/Public/FlowTypes.h @@ -1,6 +1,8 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "FlowSave.h" +#include "GameplayTagContainer.h" #include "FlowTypes.generated.h" #if WITH_EDITORONLY_DATA @@ -12,7 +14,8 @@ enum class EFlowNodeStyle : uint8 InOut UMETA(Hidden), Latent, Logic, - SubGraph UMETA(Hidden) + SubGraph UMETA(Hidden), + Custom }; #endif @@ -25,6 +28,9 @@ enum class EFlowNodeState : uint8 Aborted }; +// Finish Policy value is read by Flow Node +// Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted +// Example: Spawn node might despawn all actors if Flow Graph is aborted, not completed UENUM(BlueprintType) enum class EFlowFinishPolicy : uint8 { @@ -32,12 +38,56 @@ enum class EFlowFinishPolicy : uint8 Abort }; +UENUM(BlueprintType) +enum class EFlowSignalMode : uint8 +{ + Enabled UMETA(ToolTip = "Default state, node is fully executed."), + Disabled UMETA(ToolTip = "No logic executed, any Input Pin activation is ignored. Node instantly enters a deactivated state."), + PassThrough UMETA(ToolTip = "Internal node logic not executed. All connected outputs are triggered, node finishes its work.") +}; + UENUM(BlueprintType) enum class EFlowNetMode : uint8 { - Any UMETA(ToolTip = "Any networking mode"), + Any UMETA(ToolTip = "Any networking mode."), Authority UMETA(ToolTip = "Executed on the server or in the single-player (standalone)."), ClientOnly UMETA(ToolTip = "Executed locally, on the single client."), - ServerOnly UMETA(ToolTip = "Executed on the server"), + ServerOnly UMETA(ToolTip = "Executed on the server."), SinglePlayerOnly UMETA(ToolTip = "Executed only in the single player, not available in multiplayer.") }; + +UENUM(BlueprintType) +enum class EFlowTagContainerMatchType : uint8 +{ + HasAny UMETA(ToolTip = "Check if container A contains ANY of the tags in the specified container B."), + HasAnyExact UMETA(ToolTip = "Check if container A contains ANY of the tags in the specified container B, only allowing exact matches."), + HasAll UMETA(ToolTip = "Check if container A contains ALL of the tags in the specified container B."), + HasAllExact UMETA(ToolTip = "Check if container A contains ALL of the tags in the specified container B, only allowing exact matches") +}; + +namespace FlowTypes +{ + FORCEINLINE_DEBUGGABLE bool HasMatchingTags(const FGameplayTagContainer& Container, const FGameplayTagContainer& OtherContainer, const EFlowTagContainerMatchType MatchType) + { + switch (MatchType) + { + case EFlowTagContainerMatchType::HasAny: + return Container.HasAny(OtherContainer); + case EFlowTagContainerMatchType::HasAnyExact: + return Container.HasAnyExact(OtherContainer); + case EFlowTagContainerMatchType::HasAll: + return Container.HasAll(OtherContainer); + case EFlowTagContainerMatchType::HasAllExact: + return Container.HasAllExact(OtherContainer); + default: + return false; + } + } +} + +UENUM(BlueprintType) +enum class EFlowOnScreenMessageType : uint8 +{ + Temporary, + Permanent +}; diff --git a/Source/Flow/Public/FlowWorldSettings.h b/Source/Flow/Public/FlowWorldSettings.h index 97402469f..ba1b1dacc 100644 --- a/Source/Flow/Public/FlowWorldSettings.h +++ b/Source/Flow/Public/FlowWorldSettings.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "GameFramework/WorldSettings.h" @@ -20,9 +22,8 @@ class FLOW_API AFlowWorldSettings : public AWorldSettings public: UFlowComponent* GetFlowComponent() const { return FlowComponent; } - virtual void PostLoad() override; + virtual void PostInitializeComponents() override; private: - UPROPERTY() - class UFlowAsset* FlowAsset_DEPRECATED; + bool IsValidInstance() const; }; diff --git a/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h b/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h index 18b0ca75d..8242cae05 100644 --- a/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h +++ b/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "LevelSequenceActor.h" @@ -10,4 +12,17 @@ UCLASS(hideCategories=(Rendering, Physics, LOD, Activation, Input)) class FLOW_API AFlowLevelSequenceActor : public ALevelSequenceActor { GENERATED_UCLASS_BODY() + +protected: + UPROPERTY(ReplicatedUsing = OnRep_ReplicatedLevelSequenceAsset) + TObjectPtr ReplicatedLevelSequenceAsset; + + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + +public: + void SetReplicatedLevelSequenceAsset(ULevelSequence* Asset); + +protected: + UFUNCTION() + void OnRep_ReplicatedLevelSequenceAsset(); }; diff --git a/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h b/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h index b6eb28eb6..3e98e11c4 100644 --- a/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h +++ b/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "LevelSequencePlayer.h" @@ -11,20 +13,28 @@ class UFlowNode; UCLASS() class FLOW_API UFlowLevelSequencePlayer : public ULevelSequencePlayer { - GENERATED_UCLASS_BODY() + GENERATED_UCLASS_BODY() private: - // most likely this is a UFlowNode_PlayLevelSequence or its child - UPROPERTY() - UFlowNode* FlowEventReceiver; + // most likely this is a UFlowNode_PlayLevelSequence or its child + UPROPERTY() + UFlowNode* FlowEventReceiver; public: - // variant of ULevelSequencePlayer::CreateLevelSequencePlayer - static UFlowLevelSequencePlayer* CreateFlowLevelSequencePlayer(UObject* WorldContextObject, ULevelSequence* LevelSequence, FMovieSceneSequencePlaybackSettings Settings, ALevelSequenceActor*& OutActor); + // variant of ULevelSequencePlayer::CreateLevelSequencePlayer + static UFlowLevelSequencePlayer* CreateFlowLevelSequencePlayer( + const UObject* WorldContextObject, + ULevelSequence* LevelSequence, + FMovieSceneSequencePlaybackSettings Settings, + FLevelSequenceCameraSettings CameraSettings, + AActor* TransformOriginActor, + const bool bReplicates, + const bool bAlwaysRelevant, + ALevelSequenceActor*& OutActor); void SetFlowEventReceiver(UFlowNode* FlowNode) { FlowEventReceiver = FlowNode; } // IMovieScenePlayer virtual TArray GetEventContexts() const override; - // -- + // -- }; diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h b/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h index 118c3138d..3642103f6 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "MovieSceneFlowSectionBase.h" diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h b/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h index d41f972d7..6ea290bc2 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "MovieSceneSection.h" diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h b/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h index 4d8f08ff5..47ff27af4 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Evaluation/MovieSceneEvalTemplate.h" diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h b/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h index af7f07f4d..3b7f921ef 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Tracks/MovieSceneEventTrack.h" diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h b/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h index 880beb995..1650cd373 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Channels/MovieSceneStringChannel.h" diff --git a/Source/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index 66103f648..ef47720b5 100644 --- a/Source/Flow/Public/Nodes/FlowNode.h +++ b/Source/Flow/Public/Nodes/FlowNode.h @@ -1,17 +1,20 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "CoreMinimal.h" #include "EdGraph/EdGraphNode.h" -#include "Engine/StreamableManager.h" #include "GameplayTagContainer.h" +#include "Templates/SubclassOf.h" #include "VisualLogger/VisualLoggerDebugSnapshotInterface.h" +#include "FlowMessageLog.h" #include "FlowTypes.h" #include "Nodes/FlowPin.h" #include "FlowNode.generated.h" class UFlowAsset; class UFlowSubsystem; +class IFlowOwnerInterface; #if WITH_EDITOR DECLARE_DELEGATE(FFlowNodeEvent); @@ -24,10 +27,12 @@ UCLASS(Abstract, Blueprintable, HideCategories = Object) class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInterface { GENERATED_UCLASS_BODY() - friend class SFlowGraphNode; friend class UFlowAsset; friend class UFlowGraphNode; + friend class UFlowGraphSchema; + friend class SFlowInputPinHandle; + friend class SFlowOutputPinHandle; ////////////////////////////////////////////////////////////////////////// // Node @@ -37,13 +42,34 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte UEdGraphNode* GraphNode; #if WITH_EDITORONLY_DATA + protected: + UPROPERTY() + TArray> AllowedAssetClasses; + + UPROPERTY() + TArray> DeniedAssetClasses; + UPROPERTY() FString Category; UPROPERTY(EditDefaultsOnly, Category = "FlowNode") EFlowNodeStyle NodeStyle; + // Set Node Style to custom to use your own color for this node + UPROPERTY(EditDefaultsOnly, Category = "FlowNode", meta = (EditCondition = "NodeStyle == EFlowNodeStyle::Custom")) + FLinearColor NodeColor; + + uint8 bCanDelete : 1; + uint8 bCanDuplicate : 1; + + UPROPERTY(EditDefaultsOnly, Category = "FlowNode") + bool bNodeDeprecated; + + // If this node is deprecated, it might be replaced by another node + UPROPERTY(EditDefaultsOnly, Category = "FlowNode") + TSubclassOf ReplacedBy; + public: FFlowNodeEvent OnReconstructionRequested; #endif @@ -56,7 +82,12 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte // -- // Opportunity to update node's data before UFlowGraphNode would call ReconstructNode() - virtual void FixNode(UEdGraphNode* NewGraph); + virtual void FixNode(UEdGraphNode* NewGraphNode); + + virtual EDataValidationResult ValidateNode() { return EDataValidationResult::NotValidated; } + + // used when import graph from another asset + virtual void PostImport() {} #endif UEdGraphNode* GetGraphNode() const { return GraphNode; } @@ -67,9 +98,9 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte virtual FString GetNodeCategory() const; virtual FText GetNodeTitle() const; virtual FText GetNodeToolTip() const; - + // This method allows to have different for every node instance, i.e. Red if node represents enemy, Green if node represents a friend - virtual bool GetNodeTitleColor(FLinearColor& OutColor) const { return false; } + virtual bool GetDynamicTitleColor(FLinearColor& OutColor) const; EFlowNodeStyle GetNodeStyle() const { return NodeStyle; } @@ -79,7 +110,7 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte protected: // Short summary of node's content - displayed over node as NodeInfoPopup - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetNodeDescription")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Node Description")) FString K2_GetNodeDescription() const; // Inherits Guid after graph node @@ -93,6 +124,38 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte UFUNCTION(BlueprintPure, Category = "FlowNode") UFlowAsset* GetFlowAsset() const; + // Gets the Owning Actor for this Node's RootFlow + // (if the immediate parent is an UActorComponent, it will get that Component's actor) + AActor* TryGetRootFlowActorOwner() const; + + // Returns the IFlowOwnerInterface for the owner object (if implemented) + // NOTE - will consider a UActorComponent owner's owning actor if appropriate + IFlowOwnerInterface* GetFlowOwnerInterface() const; + +protected: + + // Helper functions for GetFlowOwnerInterface() + IFlowOwnerInterface* TryGetFlowOwnerInterfaceFromRootFlowOwner(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass) const; + IFlowOwnerInterface* TryGetFlowOwnerInterfaceActor(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass) const; + + // Gets the Owning Object for this Node's RootFlow + UObject* TryGetRootFlowObjectOwner() const; + + virtual bool CanFinishGraph() const { return false; } + +protected: + UPROPERTY(EditDefaultsOnly, Category = "FlowNode") + TArray AllowedSignalModes; + + // If enabled, signal will pass through node without calling ExecuteInput() + // Designed to handle patching + UPROPERTY() + EFlowSignalMode SignalMode; + +#if WITH_EDITOR + FFlowMessageLog ValidationLog; +#endif + ////////////////////////////////////////////////////////////////////////// // All created pins (default, class-specific and added by user) @@ -109,63 +172,71 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte UPROPERTY(EditDefaultsOnly, Category = "FlowNode") TArray OutputPins; - void AddInputPins(TArray PinNames); - void AddOutputPins(TArray PinNames); + void AddInputPins(TArray Pins); + void AddOutputPins(TArray Pins); // always use default range for nodes with user-created outputs i.e. Execution Sequence void SetNumberedInputPins(const uint8 FirstNumber = 0, const uint8 LastNumber = 1); void SetNumberedOutputPins(const uint8 FirstNumber = 0, const uint8 LastNumber = 1); - TArray GetInputPins() const { return InputPins; } - TArray GetOutputPins() const { return OutputPins; } + uint8 CountNumberedInputs() const; + uint8 CountNumberedOutputs() const; + const TArray& GetInputPins() const { return InputPins; } + const TArray& GetOutputPins() const { return OutputPins; } + +public: UFUNCTION(BlueprintPure, Category = "FlowNode") TArray GetInputNames() const; UFUNCTION(BlueprintPure, Category = "FlowNode") TArray GetOutputNames() const; -public: #if WITH_EDITOR virtual bool SupportsContextPins() const { return false; } // Be careful, enabling it might cause loading gigabytes of data as nodes would load all related data (i.e. Level Sequences) virtual bool CanRefreshContextPinsOnLoad() const { return false; } - virtual TArray GetContextInputs() { return TArray(); } - virtual TArray GetContextOutputs() { return TArray(); } + virtual TArray GetContextInputs() { return TArray(); } + virtual TArray GetContextOutputs() { return TArray(); } virtual bool CanUserAddInput() const; virtual bool CanUserAddOutput() const; - void RemoveUserInput(); - void RemoveUserOutput(); + void RemoveUserInput(const FName& PinName); + void RemoveUserOutput(const FName& PinName); #endif protected: - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "CanUserAddInput")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Can User Add Input")) bool K2_CanUserAddInput() const; - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "CanUserAddOutput")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Can User Add Output")) bool K2_CanUserAddOutput() const; ////////////////////////////////////////////////////////////////////////// // Connections to other nodes -private: +protected: // Map outputs to the connected node and input pin UPROPERTY() TMap Connections; public: - void SetConnections(TMap& InConnections) { Connections = InConnections; } + void SetConnections(const TMap& InConnections) { Connections = InConnections; } FConnectedPin GetConnection(const FName OutputName) const { return Connections.FindRef(OutputName); } + + UFUNCTION(BlueprintPure, Category= "FlowNode") TSet GetConnectedNodes() const; + + FName GetPinConnectedToNode(const FGuid& OtherNodeGuid); UFUNCTION(BlueprintPure, Category= "FlowNode") - bool IsOutputConnected(const FName& PinName) const; + bool IsInputConnected(const FName& PinName) const; - static void RecursiveFindNodesByClass(UFlowNode* Node, const TSubclassOf Class, uint8 Depth, TArray& OutNodes); + UFUNCTION(BlueprintPure, Category= "FlowNode") + bool IsOutputConnected(const FName& PinName) const; ////////////////////////////////////////////////////////////////////////// // Debugger @@ -182,12 +253,14 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte bool bPreloaded; protected: - FStreamableManager StreamableManager; - UPROPERTY(SaveGame) EFlowNodeState ActivationState; - + +public: + EFlowNodeState GetActivationState() const { return ActivationState; } + #if !UE_BUILD_SHIPPING + private: TMap> InputRecords; TMap> OutputRecords; @@ -206,7 +279,7 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte // Event called just after creating the node instance, while initializing the Flow Asset instance // This happens before executing graph, only called during gameplay - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "InitInstance")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Init Instance")) void K2_InitializeInstance(); public: @@ -217,34 +290,41 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte virtual void PreloadContent(); virtual void FlushContent(); - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "PreloadContent")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Preload Content")) void K2_PreloadContent(); - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "FlushContent")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Flush Content")) void K2_FlushContent(); + virtual void OnActivate(); + + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "On Activate")) + void K2_OnActivate(); + // Trigger execution of input pin - void TriggerInput(const FName& PinName); + void TriggerInput(const FName& PinName, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default); // Method reacting on triggering Input pin virtual void ExecuteInput(const FName& PinName); // Event reacting on triggering Input pin - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "ExecuteInput")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Execute Input")) void K2_ExecuteInput(const FName& PinName); // Simply trigger the first Output Pin, convenient to use if node has only one output UFUNCTION(BlueprintCallable, Category = "FlowNode") void TriggerFirstOutput(const bool bFinish); - // Trigger Output Pin - UFUNCTION(BlueprintCallable, Category = "FlowNode") - void TriggerOutput(const FName& PinName, const bool bFinish = false); + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (HidePin = "bForcedActivation")) + void TriggerOutput(const FName& PinName, const bool bFinish = false, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default); void TriggerOutput(const FString& PinName, const bool bFinish = false); void TriggerOutput(const FText& PinName, const bool bFinish = false); void TriggerOutput(const TCHAR* PinName, const bool bFinish = false); + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (HidePin = "ActivationType")) + void TriggerOutputPin(const FFlowOutputPinHandle Pin, const bool bFinish = false, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default); + // Finish execution of node, it will call Cleanup UFUNCTION(BlueprintCallable, Category = "FlowNode") void Finish(); @@ -258,28 +338,58 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Cleanup")) void K2_Cleanup(); + // Method called from UFlowAsset::DeinitializeInstance() + virtual void DeinitializeInstance(); + + // Event called from UFlowAsset::DeinitializeInstance() + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "DeinitializeInstance")) + void K2_DeinitializeInstance(); + public: // Define what happens when node is terminated from the outside virtual void ForceFinishNode(); protected: // Define what happens when node is terminated from the outside - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "ForceFinishNode")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Force Finish Node")) void K2_ForceFinishNode(); private: void ResetRecords(); +////////////////////////////////////////////////////////////////////////// +// SaveGame support + +public: + UFUNCTION(BlueprintCallable, Category = "FlowNode") + void SaveInstance(FFlowNodeSaveData& NodeRecord); + + UFUNCTION(BlueprintCallable, Category = "FlowNode") + void LoadInstance(const FFlowNodeSaveData& NodeRecord); + +protected: + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + void OnSave(); + + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + void OnLoad(); + + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + void OnPassThrough(); + +////////////////////////////////////////////////////////////////////////// +// Utils + #if WITH_EDITOR public: UFlowNode* GetInspectedInstance() const; - EFlowNodeState GetActivationState() const { return ActivationState; } TMap GetWireRecords() const; TArray GetPinRecords(const FName& PinName, const EEdGraphPinDirection PinDirection) const; // Information displayed while node is working - displayed over node as NodeInfoPopup virtual FString GetStatusString() const; + virtual bool GetStatusBackgroundColor(FLinearColor& OutColor) const; virtual FString GetAssetPath(); virtual UObject* GetAssetToEdit(); @@ -288,32 +398,21 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte protected: // Information displayed while node is working - displayed over node as NodeInfoPopup - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetStatusString")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Status String")) FString K2_GetStatusString() const; - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetAssetPath")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Status Background Color")) + bool K2_GetStatusBackgroundColor(FLinearColor& OutColor) const; + + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Asset Path")) FString K2_GetAssetPath(); - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetAssetToEdit")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Asset To Edit")) UObject* K2_GetAssetToEdit(); - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetActorToFocus")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Actor To Focus")) AActor* K2_GetActorToFocus(); - template - T* LoadAsset(TSoftObjectPtr AssetPtr) - { - ensure(!AssetPtr.IsNull()); - - if (AssetPtr.IsPending()) - { - const FSoftObjectPath& AssetRef = AssetPtr.ToSoftObjectPath(); - AssetPtr = Cast(StreamableManager.LoadSynchronous(AssetRef, false)); - } - - return Cast(AssetPtr.Get()); - } - public: UFUNCTION(BlueprintPure, Category = "FlowNode") static FString GetIdentityTagDescription(const FGameplayTag& Tag); @@ -330,26 +429,18 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte UFUNCTION(BlueprintPure, Category = "FlowNode") static FString GetProgressAsString(float Value); - UFUNCTION(BlueprintCallable, Category = "FlowNode") - void LogError(FString Message) const; - - UFUNCTION(BlueprintCallable, Category = "FlowNode") - void SaveInstance(FFlowNodeSaveData& NodeRecord); +public: + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (DevelopmentOnly)) + void LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType = EFlowOnScreenMessageType::Permanent); - UFUNCTION(BlueprintCallable, Category = "FlowNode") - void LoadInstance(const FFlowNodeSaveData& NodeRecord); + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (DevelopmentOnly)) + void LogWarning(FString Message); -protected: - UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") - void OnSave(); - - UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") - void OnLoad(); + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (DevelopmentOnly)) + void LogNote(FString Message); +#if !UE_BUILD_SHIPPING private: - UPROPERTY() - TArray InputNames_DEPRECATED; - - UPROPERTY() - TArray OutputNames_DEPRECATED; + bool BuildMessage(FString& Message) const; +#endif }; diff --git a/Source/Flow/Public/Nodes/FlowNodeBlueprint.h b/Source/Flow/Public/Nodes/FlowNodeBlueprint.h index 3a02a2675..32f82c4ba 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBlueprint.h +++ b/Source/Flow/Public/Nodes/FlowNodeBlueprint.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "CoreMinimal.h" @@ -9,7 +11,7 @@ */ UCLASS(BlueprintType) -class FLOW_API UFlowNodeBlueprint final : public UBlueprint +class FLOW_API UFlowNodeBlueprint : public UBlueprint { GENERATED_UCLASS_BODY() diff --git a/Source/Flow/Public/Nodes/FlowPin.h b/Source/Flow/Public/Nodes/FlowPin.h index 116677f8f..950e9373f 100644 --- a/Source/Flow/Public/Nodes/FlowPin.h +++ b/Source/Flow/Public/Nodes/FlowPin.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "FlowPin.generated.h" @@ -7,12 +9,19 @@ struct FLOW_API FFlowPin { GENERATED_BODY() + // A logical name, used during execution of pin UPROPERTY(EditDefaultsOnly, Category = "FlowPin") FName PinName; + // An optional Display Name, you can use it to override PinName without the need to update graph connections + UPROPERTY(EditDefaultsOnly, Category = "FlowPin") + FText PinFriendlyName; + UPROPERTY(EditDefaultsOnly, Category = "FlowPin") FString PinToolTip; + static inline FName AnyPinName = TEXT("AnyPinName"); + FFlowPin() : PinName(NAME_None) { @@ -29,7 +38,7 @@ struct FLOW_API FFlowPin } FFlowPin(const FText& InPinName) - : PinName(*InPinName.ToString()) + : PinName(*InPinName.ToString()) { } @@ -48,30 +57,25 @@ struct FLOW_API FFlowPin { } - FFlowPin(const FName& InPinName, const FString& InPinTooltip) + FFlowPin(const FStringView InPinName, const FText& InPinFriendlyName) : PinName(InPinName) - , PinToolTip(InPinTooltip) + , PinFriendlyName(InPinFriendlyName) { } - FFlowPin(const FString& InPinName, const FString& InPinTooltip) - : PinName(*InPinName) + FFlowPin(const FStringView InPinName, const FString& InPinTooltip) + : PinName(InPinName) , PinToolTip(InPinTooltip) { } - FFlowPin(const FText& InPinName, const FString& InPinTooltip) - : PinName(*InPinName.ToString()) - , PinToolTip(InPinTooltip) - { - } - - FFlowPin(const TCHAR* InPinName, const FString& InPinTooltip) - : PinName(FName(InPinName)) + FFlowPin(const FStringView InPinName, const FText& InPinFriendlyName, const FString& InPinTooltip) + : PinName(InPinName) + , PinFriendlyName(InPinFriendlyName) , PinToolTip(InPinTooltip) { } - + FORCEINLINE bool IsValid() const { return !PinName.IsNone(); @@ -96,6 +100,46 @@ struct FLOW_API FFlowPin { return PinName != Other; } + + friend uint32 GetTypeHash(const FFlowPin& FlowPin) + { + return GetTypeHash(FlowPin.PinName); + } +}; + +USTRUCT() +struct FLOW_API FFlowPinHandle +{ + GENERATED_BODY() + + // Update SFlowPinHandleBase code if this property name would be ever changed + UPROPERTY() + FName PinName; + + FFlowPinHandle() + : PinName(NAME_None) + { + } +}; + +USTRUCT(BlueprintType) +struct FLOW_API FFlowInputPinHandle : public FFlowPinHandle +{ + GENERATED_BODY() + + FFlowInputPinHandle() + { + } +}; + +USTRUCT(BlueprintType) +struct FLOW_API FFlowOutputPinHandle : public FFlowPinHandle +{ + GENERATED_BODY() + + FFlowOutputPinHandle() + { + } }; // Processing Flow Nodes creates map of connected pins @@ -104,39 +148,109 @@ struct FLOW_API FConnectedPin { GENERATED_USTRUCT_BODY() - UPROPERTY() + UPROPERTY() FGuid NodeGuid; UPROPERTY() FName PinName; FConnectedPin() - : NodeGuid(FGuid()) - , PinName(NAME_None) + : NodeGuid(FGuid()) + , PinName(NAME_None) { } FConnectedPin(const FGuid InNodeId, const FName& InPinName) - : NodeGuid(InNodeId) - , PinName(InPinName) + : NodeGuid(InNodeId) + , PinName(InPinName) + { + } + + FORCEINLINE bool operator==(const FConnectedPin& Other) const { + return NodeGuid == Other.NodeGuid && PinName == Other.PinName; } + + FORCEINLINE bool operator!=(const FConnectedPin& Other) const + { + return NodeGuid != Other.NodeGuid || PinName != Other.PinName; + } + + friend uint32 GetTypeHash(const FConnectedPin& ConnectedPin) + { + return GetTypeHash(ConnectedPin.NodeGuid) + GetTypeHash(ConnectedPin.PinName); + } +}; + +UENUM(BlueprintType) +enum class EFlowPinActivationType : uint8 +{ + Default, + Forced, + PassThrough }; // Every time pin is activated, we record it and display this data while user hovers mouse over pin #if !UE_BUILD_SHIPPING struct FLOW_API FPinRecord -{ +{ double Time; FString HumanReadableTime; + EFlowPinActivationType ActivationType; static FString NoActivations; static FString PinActivations; + static FString ForcedActivation; + static FString PassThroughActivation; FPinRecord(); - FPinRecord(const double InTime); + FPinRecord(const double InTime, const EFlowPinActivationType InActivationType); - private: +private: FORCEINLINE static FString DoubleDigit(const int32 Number); }; #endif + +// It can represent any trait added on the specific node instance, i.e. breakpoint +USTRUCT() +struct FLOW_API FFlowPinTrait +{ + GENERATED_USTRUCT_BODY() + +protected: + UPROPERTY() + uint8 bTraitAllowed : 1; + + uint8 bEnabled : 1; + uint8 bHit : 1; + +public: + FFlowPinTrait() + : bTraitAllowed(false) + , bEnabled(false) + , bHit(false) + { + }; + + explicit FFlowPinTrait(const bool bInitialState) + : bTraitAllowed(bInitialState) + , bEnabled(bInitialState) + , bHit(false) + { + }; + + void AllowTrait(); + void DisallowTrait(); + bool IsAllowed() const; + + void EnableTrait(); + void DisableTrait(); + void ToggleTrait(); + + bool CanEnable() const; + bool IsEnabled() const; + + void MarkAsHit(); + void ResetHit(); + bool IsHit() const; +}; diff --git a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalAND.h b/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalAND.h index 1b27942fd..6296d07e1 100644 --- a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalAND.h +++ b/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalAND.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" @@ -7,7 +9,7 @@ * Logical AND * Output will be triggered only once */ -UCLASS(NotBlueprintable, meta = (DisplayName = "AND")) +UCLASS(NotBlueprintable, meta = (DisplayName = "AND", Keywords = "&")) class FLOW_API UFlowNode_LogicalAND final : public UFlowNode { GENERATED_UCLASS_BODY() diff --git a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalOR.h b/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalOR.h index 6abdd48af..70ea21e82 100644 --- a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalOR.h +++ b/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalOR.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" @@ -7,15 +9,32 @@ * Logical OR * Output will be triggered only once */ -UCLASS(NotBlueprintable, meta = (DisplayName = "OR")) +UCLASS(NotBlueprintable, meta = (DisplayName = "OR", Keywords = "|")) class FLOW_API UFlowNode_LogicalOR final : public UFlowNode { GENERATED_UCLASS_BODY() +protected: + UPROPERTY(EditAnywhere, Category = "Lifetime", SaveGame) + bool bEnabled; + + // This node will become Blocked (not executed any more), if Execution Limit > 0 and Execution Count reaches this limit + // Set this to zero, if you'd like fire output indefinitely + UPROPERTY(EditAnywhere, Category = "Lifetime", meta = (ClampMin = 0)) + int32 ExecutionLimit; + + // This node will become Blocked (not executed any more), if Execution Limit > 0 and Execution Count reaches this limit + UPROPERTY(VisibleAnywhere, Category = "Lifetime", SaveGame) + int32 ExecutionCount; + #if WITH_EDITOR +public: virtual bool CanUserAddInput() const override { return true; } #endif protected: virtual void ExecuteInput(const FName& PinName) override; + virtual void Cleanup() override; + + void ResetCounter(); }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h b/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h index 0bce7ea28..8346814d5 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_CustomEventBase.h b/Source/Flow/Public/Nodes/Route/FlowNode_CustomEventBase.h new file mode 100644 index 000000000..7e9944d25 --- /dev/null +++ b/Source/Flow/Public/Nodes/Route/FlowNode_CustomEventBase.h @@ -0,0 +1,29 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Nodes/FlowNode.h" +#include "FlowNode_CustomEventBase.generated.h" + +/** + * Base class for nodes used to receive/send events between graphs + */ +UCLASS(Abstract, NotBlueprintable) +class FLOW_API UFlowNode_CustomEventBase : public UFlowNode +{ + GENERATED_UCLASS_BODY() + +protected: + UPROPERTY() + FName EventName; + +public: + void SetEventName(const FName& InEventName); + const FName& GetEventName() const { return EventName; } + +#if WITH_EDITOR +public: + virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; +#endif +}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_CustomInput.h b/Source/Flow/Public/Nodes/Route/FlowNode_CustomInput.h index 9a0e244c9..12f6012c0 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_CustomInput.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_CustomInput.h @@ -1,24 +1,25 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "Nodes/FlowNode.h" +#include "FlowNode_CustomEventBase.h" #include "FlowNode_CustomInput.generated.h" /** * Triggers output upon activation of Input (matching this EventName) on the SubGraph node containing this graph */ UCLASS(NotBlueprintable, meta = (DisplayName = "Custom Input")) -class FLOW_API UFlowNode_CustomInput : public UFlowNode +class FLOW_API UFlowNode_CustomInput : public UFlowNode_CustomEventBase { GENERATED_UCLASS_BODY() - UPROPERTY() - FName EventName; + friend class UFlowAsset; protected: virtual void ExecuteInput(const FName& PinName) override; #if WITH_EDITOR public: - virtual FString GetNodeDescription() const override; + virtual FText GetNodeTitle() const override; #endif }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_CustomOutput.h b/Source/Flow/Public/Nodes/Route/FlowNode_CustomOutput.h index d1ec007c6..b0e93d422 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_CustomOutput.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_CustomOutput.h @@ -1,6 +1,8 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "Nodes/FlowNode.h" +#include "FlowNode_CustomEventBase.h" #include "FlowNode_CustomOutput.generated.h" /** @@ -8,17 +10,14 @@ * Triggered output name matches EventName selected on this node */ UCLASS(NotBlueprintable, meta = (DisplayName = "Custom Output")) -class FLOW_API UFlowNode_CustomOutput final : public UFlowNode +class FLOW_API UFlowNode_CustomOutput final : public UFlowNode_CustomEventBase { GENERATED_UCLASS_BODY() - UPROPERTY() - FName EventName; - protected: virtual void ExecuteInput(const FName& PinName) override; #if WITH_EDITOR - virtual FString GetNodeDescription() const override; + virtual FText GetNodeTitle() const override; #endif }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h index 29c03f6f8..262529cbc 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" @@ -6,7 +8,7 @@ /** * Executes a series of pins in order */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Multi Gate")) +UCLASS(NotBlueprintable, meta = (DisplayName = "Multi Gate", Keywords = "series, loop, random")) class FLOW_API UFlowNode_ExecutionMultiGate final : public UFlowNode { GENERATED_UCLASS_BODY() diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h index 69b99cfe3..ea7f4f5a8 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" @@ -13,8 +15,32 @@ class FLOW_API UFlowNode_ExecutionSequence final : public UFlowNode #if WITH_EDITOR virtual bool CanUserAddOutput() const override { return true; } + + virtual FString GetNodeDescription() const override; #endif + virtual void OnLoad_Implementation() override; + protected: virtual void ExecuteInput(const FName& PinName) override; + + /** + * If enabled and the flowgraph is saved during gameplay, this node + * tracks and saves which pins it has executed. + * + * If you add new connections or replace old connections with with + * different nodes, this node will detect the changes. If during gameplay + * you load an old save game which had different connections, this node + * will automatically execute the updated connections you created. + * + * This is useful if you want the ability to add new parts to your + * graph after release. + */ + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bSavePinExecutionState; + + UPROPERTY(SaveGame) + TSet ExecutedConnections; + + void ExecuteNewConnections(); }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Finish.h b/Source/Flow/Public/Nodes/Route/FlowNode_Finish.h index 832bf9e39..a9f9299b6 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Finish.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Finish.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" @@ -13,5 +15,6 @@ class FLOW_API UFlowNode_Finish : public UFlowNode GENERATED_UCLASS_BODY() protected: + virtual bool CanFinishGraph() const override { return true; } virtual void ExecuteInput(const FName& PinName) override; }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h b/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h index f4e17eea9..3556dbe91 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Start.h b/Source/Flow/Public/Nodes/Route/FlowNode_Start.h index 50af14bde..803cf42df 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Start.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Start.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" @@ -11,6 +13,8 @@ class FLOW_API UFlowNode_Start : public UFlowNode { GENERATED_UCLASS_BODY() + friend class UFlowAsset; + protected: virtual void ExecuteInput(const FName& PinName) override; }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_SubGraph.h b/Source/Flow/Public/Nodes/Route/FlowNode_SubGraph.h index 4702fa367..49dbcf6d1 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_SubGraph.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_SubGraph.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" @@ -12,6 +14,7 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode GENERATED_UCLASS_BODY() friend class UFlowAsset; + friend class FFlowNode_SubGraphDetails; friend class UFlowSubsystem; static FFlowPin StartPin; @@ -21,12 +24,19 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode UPROPERTY(EditAnywhere, Category = "Graph") TSoftObjectPtr Asset; - TWeakObjectPtr AssetInstance; + /* + * Allow to create instance of the same Flow Asset as the asset containing this node + * Enabling it may cause an infinite loop, if graph would keep creating copies of itself + */ + UPROPERTY(EditAnywhere, Category = "Graph") + bool bCanInstanceIdenticalAsset; UPROPERTY(SaveGame) FString SavedAssetInstanceName; protected: + virtual bool CanBeAssetInstanced() const; + virtual void PreloadContent() override; virtual void FlushContent() override; @@ -39,15 +49,28 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode protected: virtual void OnLoad_Implementation() override; -public: + +#if WITH_EDITORONLY_DATA +protected: + // All the classes allowed to be used as assets on this subgraph node + UPROPERTY() + TArray> AllowedAssignedAssetClasses; + + // All the classes disallowed to be used as assets on this subgraph node + UPROPERTY() + TArray> DeniedAssignedAssetClasses; +#endif + #if WITH_EDITOR +public: virtual FString GetNodeDescription() const override; virtual UObject* GetAssetToEdit() override; - + virtual EDataValidationResult ValidateNode() override; + virtual bool SupportsContextPins() const override { return true; } - virtual TArray GetContextInputs() override; - virtual TArray GetContextOutputs() override; + virtual TArray GetContextInputs() override; + virtual TArray GetContextOutputs() override; // UObject virtual void PostLoad() override; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h b/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h index 35562d387..c920c8662 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Engine/EngineTypes.h" @@ -7,12 +9,13 @@ /** * Triggers outputs after time elapsed */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Timer")) -class FLOW_API UFlowNode_Timer final : public UFlowNode +UCLASS(NotBlueprintable, meta = (DisplayName = "Timer", Keywords = "delay, step, tick")) +class FLOW_API UFlowNode_Timer : public UFlowNode { GENERATED_UCLASS_BODY() protected: + // If the value is closer to 0, Timer will complete in next tick UPROPERTY(EditAnywhere, Category = "Timer", meta = (ClampMin = 0.0f)) float CompletionTime; @@ -36,6 +39,9 @@ class FLOW_API UFlowNode_Timer final : public UFlowNode protected: virtual void ExecuteInput(const FName& PinName) override; + virtual void SetTimer(); + virtual void Restart(); + private: UFUNCTION() void OnStep(); diff --git a/Source/Flow/Public/Nodes/Utils/FlowNode_Checkpoint.h b/Source/Flow/Public/Nodes/Utils/FlowNode_Checkpoint.h index b5ca71382..a4ce5605e 100644 --- a/Source/Flow/Public/Nodes/Utils/FlowNode_Checkpoint.h +++ b/Source/Flow/Public/Nodes/Utils/FlowNode_Checkpoint.h @@ -1,13 +1,15 @@ -#pragma once +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once #include "Nodes/FlowNode.h" #include "FlowNode_Checkpoint.generated.h" /** * Save the state of the game to the save file -* It's recommended to replace this with game-specific variant and this node to UFlowGraphSettings::HiddenNodes + * It's recommended to replace this with game-specific variant and this node to UFlowGraphSettings::HiddenNodes */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Checkpoint")) +UCLASS(NotBlueprintable, meta = (DisplayName = "Checkpoint", Keywords = "autosave, save")) class FLOW_API UFlowNode_Checkpoint final : public UFlowNode { GENERATED_UCLASS_BODY() diff --git a/Source/Flow/Public/Nodes/Utils/FlowNode_Log.h b/Source/Flow/Public/Nodes/Utils/FlowNode_Log.h index c20daa9f6..89c1fef8f 100644 --- a/Source/Flow/Public/Nodes/Utils/FlowNode_Log.h +++ b/Source/Flow/Public/Nodes/Utils/FlowNode_Log.h @@ -1,13 +1,27 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/FlowNode.h" #include "FlowNode_Log.generated.h" +// Variant of ELogVerbosity +UENUM(BlueprintType) +enum class EFlowLogVerbosity : uint8 +{ + Error UMETA(ToolTip = "Prints a message to console (and log file)"), + Warning UMETA(ToolTip = "Prints a message to console (and log file)"), + Display UMETA(ToolTip = "Prints a message to console (and log file)"), + Log UMETA(ToolTip = "Prints a message to a log file (does not print to console)"), + Verbose UMETA(ToolTip = "Prints a verbose message to a log file (if Verbose logging is enabled for the given category, usually used for detailed logging)"), + VeryVerbose UMETA(ToolTip = "Prints a verbose message to a log file (if VeryVerbose logging is enabled, usually used for detailed logging that would otherwise spam output)"), +}; + /** * Adds message to log * Optionally shows message on screen */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Log")) +UCLASS(NotBlueprintable, meta = (DisplayName = "Log", Keywords = "print")) class FLOW_API UFlowNode_Log : public UFlowNode { GENERATED_UCLASS_BODY() @@ -16,6 +30,9 @@ class FLOW_API UFlowNode_Log : public UFlowNode UPROPERTY(EditAnywhere, Category = "Flow") FString Message; + UPROPERTY(EditAnywhere, Category = "Flow") + EFlowLogVerbosity Verbosity; + UPROPERTY(EditAnywhere, Category = "Flow") bool bPrintToScreen; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_CallOwnerFunction.h b/Source/Flow/Public/Nodes/World/FlowNode_CallOwnerFunction.h new file mode 100644 index 000000000..55d8266ce --- /dev/null +++ b/Source/Flow/Public/Nodes/World/FlowNode_CallOwnerFunction.h @@ -0,0 +1,95 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "CoreMinimal.h" + +#include "GameplayTagContainer.h" + +#include "FlowOwnerFunctionRef.h" +#include "Nodes/FlowNode.h" + +#include "FlowNode_CallOwnerFunction.generated.h" + + +// Forward Declarations +class UFlowOwnerFunctionParams; +class IFlowOwnerInterface; + + +// Example signature for valid Flow Owner Functions +typedef TFunction FFlowOwnerFunctionSignature; + + +/** + * FlowNode to call an owner function + * - Owner must implement IFlowOwnerInterface + * - Callable functions must take a single input parameter deriving from UFlowOwnerFunctionParams + * and return FName for the Output event to trigger (or "None" to trigger none of the outputs) + */ +UCLASS(NotBlueprintable, meta = (DisplayName = "Call Owner Function")) +class FLOW_API UFlowNode_CallOwnerFunction : public UFlowNode +{ + GENERATED_BODY() + +public: + +#if WITH_EDITOR + //Begin UObject + virtual void PostLoad() override; + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + //End UObject + + //Begin UFlowNode public + virtual FText GetNodeTitle() const override; + virtual EDataValidationResult ValidateNode() override; + + virtual FString GetStatusString() const override; + //End UFlowNode public +#endif // WITH_EDITOR + + UFlowNode_CallOwnerFunction(); + + UClass* GetRequiredParamsClass() const; + UClass* GetExistingParamsClass() const; + + bool IsAcceptableParamsPropertyClass(const UClass* ParamsClass) const; + + UClass* TryGetExpectedOwnerClass() const; + + static bool DoesFunctionHaveValidFlowOwnerFunctionSignature(const UFunction& Function); + + static UClass* GetParamsClassForFunctionName(const UClass& ExpectedOwnerClass, const FName& FunctionName); + static UClass* GetParamsClassForFunction(const UFunction& Function); + +protected: + +#if WITH_EDITOR + // returns true if the InOutPins array was rebuilt + bool RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin); + + void OnChangedParamsObject(); +#endif // WITH_EDITOR + + //Begin UFlowNode protected + virtual void ExecuteInput(const FName& PinName) override; + //End UFlowNode protected + + bool ShouldFinishForOutputName(const FName& OutputName) const; + bool TryExecuteOutputPin(const FName& OutputName); + + bool TryAllocateParamsInstance(); + + // Helper function for DoesFunctionHaveValidFlowOwnerFunctionSignature() + static bool DoesFunctionHaveNameReturnType(const UFunction& Function); + +protected: + + // Function reference on the expected owner to call + UPROPERTY(EditAnywhere, Category = "Call Owner", meta = (DisplayName = "Function")) + FFlowOwnerFunctionRef FunctionRef; + + // Parameter object to pass to the function when called + UPROPERTY(EditAnywhere, Category = "Call Owner", Instanced) + UFlowOwnerFunctionParams* Params; +}; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_ComponentObserver.h b/Source/Flow/Public/Nodes/World/FlowNode_ComponentObserver.h index d19b647d4..ecc5bbf35 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_ComponentObserver.h +++ b/Source/Flow/Public/Nodes/World/FlowNode_ComponentObserver.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "GameplayTagContainer.h" @@ -15,27 +17,30 @@ UCLASS(Abstract, NotBlueprintable) class FLOW_API UFlowNode_ComponentObserver : public UFlowNode { GENERATED_UCLASS_BODY() - - friend class FFlowNode_ComponentObserverDetails; + friend class FFlowNode_ComponentObserverDetails; + protected: UPROPERTY(EditAnywhere, Category = "ObservedComponent") FGameplayTagContainer IdentityTags; + // Container A: Identity Tags in Flow Component + // Container B: Identity Tags listed above + UPROPERTY(EditAnywhere, Category = "ObservedComponent") + EFlowTagContainerMatchType IdentityMatchType; + // This node will become Completed, if Success Limit > 0 and Success Count reaches this limit // Set this to zero, if you'd like receive events indefinitely (node would finish work only if explicitly Stopped) UPROPERTY(EditAnywhere, Category = "Lifetime", meta = (ClampMin = 0)) int32 SuccessLimit; // This node will become Completed, if Success Limit > 0 and Success Count reaches this limit - UPROPERTY(VisibleAnywhere, Category = "Lifetime") + UPROPERTY(VisibleAnywhere, Category = "Lifetime", SaveGame) int32 SuccessCount; - + TMap, TWeakObjectPtr> RegisteredActors; protected: - virtual void PostLoad() override; - virtual void ExecuteInput(const FName& PinName) override; virtual void OnLoad_Implementation() override; @@ -46,29 +51,27 @@ class FLOW_API UFlowNode_ComponentObserver : public UFlowNode virtual void OnComponentRegistered(UFlowComponent* Component); UFUNCTION() - virtual void OnComponentTagAdded(UFlowComponent* Component, const FGameplayTagContainer& AddedTags); + virtual void OnComponentTagAdded(UFlowComponent* Component, const FGameplayTagContainer& AddedTags); UFUNCTION() - virtual void OnComponentUnregistered(UFlowComponent* Component); + virtual void OnComponentTagRemoved(UFlowComponent* Component, const FGameplayTagContainer& RemovedTags); UFUNCTION() - virtual void OnComponentTagRemoved(UFlowComponent* Component, const FGameplayTagContainer& RemovedTags); + virtual void OnComponentUnregistered(UFlowComponent* Component); virtual void ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) {} virtual void ForgetActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) {} UFUNCTION() virtual void OnEventReceived(); - + virtual void Cleanup() override; #if WITH_EDITOR public: virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; + virtual FString GetStatusString() const override; #endif - -private: - UPROPERTY() - FGameplayTag IdentityTag_DEPRECATED; }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_NotifyActor.h b/Source/Flow/Public/Nodes/World/FlowNode_NotifyActor.h index ae0201d80..fa7b712b1 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_NotifyActor.h +++ b/Source/Flow/Public/Nodes/World/FlowNode_NotifyActor.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "GameplayTagContainer.h" @@ -8,32 +10,34 @@ /** * Finds all Flow Components with matching Identity Tag and calls ReceiveNotify event on these components */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Notify Actor")) +UCLASS(NotBlueprintable, meta = (DisplayName = "Notify Actor", Keywords = "event")) class FLOW_API UFlowNode_NotifyActor : public UFlowNode { GENERATED_UCLASS_BODY() protected: - UPROPERTY(EditAnywhere, Category = "ObservedComponent") + UPROPERTY(EditAnywhere, Category = "Notify") FGameplayTagContainer IdentityTags; + + /** + * If true, identity tags must be an exact match. + * Be careful, setting this to false may be very expensive, as the + * search cost is proportional to the number of registered Gameplay Tags! + */ + UPROPERTY(EditAnywhere, Category = "Notify") + bool bExactMatch; - UPROPERTY(EditAnywhere, Category = "ObservedComponent") + UPROPERTY(EditAnywhere, Category = "Notify") FGameplayTagContainer NotifyTags; - virtual void PostLoad() override; - + UPROPERTY(EditAnywhere, Category = "Notify") + EFlowNetMode NetMode; + virtual void ExecuteInput(const FName& PinName) override; #if WITH_EDITOR public: virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; #endif - -private: - UPROPERTY() - FGameplayTag IdentityTag_DEPRECATED; - - UPROPERTY() - FGameplayTag NotifyTag_DEPRECATED; - }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_OnActorRegistered.h b/Source/Flow/Public/Nodes/World/FlowNode_OnActorRegistered.h new file mode 100644 index 000000000..d4045c667 --- /dev/null +++ b/Source/Flow/Public/Nodes/World/FlowNode_OnActorRegistered.h @@ -0,0 +1,18 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Nodes/World/FlowNode_ComponentObserver.h" +#include "FlowNode_OnActorRegistered.generated.h" + +/** + * Triggers output when Flow Component with matching Identity Tag appears in the world + */ +UCLASS(NotBlueprintable, meta = (DisplayName = "On Actor Registered", Keywords = "bind")) +class FLOW_API UFlowNode_OnActorRegistered : public UFlowNode_ComponentObserver +{ + GENERATED_UCLASS_BODY() + +protected: + virtual void ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; +}; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_OnActorUnregistered.h b/Source/Flow/Public/Nodes/World/FlowNode_OnActorUnregistered.h new file mode 100644 index 000000000..655e98082 --- /dev/null +++ b/Source/Flow/Public/Nodes/World/FlowNode_OnActorUnregistered.h @@ -0,0 +1,19 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Nodes/World/FlowNode_ComponentObserver.h" +#include "FlowNode_OnActorUnregistered.generated.h" + +/** + * Triggers output when Flow Component with matching Identity Tag disappears from the world + */ +UCLASS(NotBlueprintable, meta = (DisplayName = "On Actor Unregistered", Keywords = "unbind")) +class FLOW_API UFlowNode_OnActorUnregistered : public UFlowNode_ComponentObserver +{ + GENERATED_UCLASS_BODY() + +protected: + virtual void ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; + virtual void ForgetActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; +}; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_OnNotifyFromActor.h b/Source/Flow/Public/Nodes/World/FlowNode_OnNotifyFromActor.h index 251c2c0b3..b64c4068b 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_OnNotifyFromActor.h +++ b/Source/Flow/Public/Nodes/World/FlowNode_OnNotifyFromActor.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Nodes/World/FlowNode_ComponentObserver.h" @@ -12,18 +14,14 @@ class FLOW_API UFlowNode_OnNotifyFromActor : public UFlowNode_ComponentObserver GENERATED_UCLASS_BODY() protected: - UPROPERTY(EditAnywhere, Category = "ObservedComponent") + UPROPERTY(EditAnywhere, Category = "Notify") FGameplayTagContainer NotifyTags; // If true, node will check given Notify Tag is present in the Recently Sent Notify Tags // This might be helpful in multiplayer, if client-side Flow Node started work after server sent the notify - UPROPERTY(EditAnywhere, Category = "ObservedComponent") + UPROPERTY(EditAnywhere, Category = "Notify") bool bRetroactive; - virtual void PostLoad() override; - - virtual void ExecuteInput(const FName& PinName) override; - virtual void ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; virtual void ForgetActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; @@ -33,8 +31,4 @@ class FLOW_API UFlowNode_OnNotifyFromActor : public UFlowNode_ComponentObserver public: virtual FString GetNodeDescription() const override; #endif - -private: - UPROPERTY() - FGameplayTag NotifyTag_DEPRECATED; }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_PlayLevelSequence.h b/Source/Flow/Public/Nodes/World/FlowNode_PlayLevelSequence.h index 4ef7e914b..d7ef98831 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_PlayLevelSequence.h +++ b/Source/Flow/Public/Nodes/World/FlowNode_PlayLevelSequence.h @@ -1,12 +1,16 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "EngineDefines.h" +#include "Engine/StreamableManager.h" +#include "LevelSequencePlayer.h" #include "MovieSceneSequencePlayer.h" + #include "Nodes/FlowNode.h" #include "FlowNode_PlayLevelSequence.generated.h" class UFlowLevelSequencePlayer; -class ULevelSequence; DECLARE_MULTICAST_DELEGATE(FFlowNodeLevelSequenceEvent); @@ -21,15 +25,43 @@ UCLASS(NotBlueprintable, meta = (DisplayName = "Play Level Sequence")) class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode { GENERATED_UCLASS_BODY() - friend struct FFlowTrackExecutionToken; +public: static FFlowNodeLevelSequenceEvent OnPlaybackStarted; static FFlowNodeLevelSequenceEvent OnPlaybackCompleted; UPROPERTY(EditAnywhere, Category = "Sequence") TSoftObjectPtr Sequence; + UPROPERTY(EditAnywhere, Category = "Sequence") + FMovieSceneSequencePlaybackSettings PlaybackSettings; + + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bPlayReverse; + + UPROPERTY(EditAnywhere, Category = "Sequence") + FLevelSequenceCameraSettings CameraSettings; + + // Level Sequence playback can be moved to any place in the world by applying Transform Origin + // Enabling this option will use actor that created Root Flow instance, i.e. World Settings or Player Controller + // https://docs.unrealengine.com/5.0/en-US/creating-level-sequences-with-dynamic-transforms-in-unreal-engine/ + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bUseGraphOwnerAsTransformOrigin; + + // If true, playback of this level sequence on the server will be synchronized across other clients + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bReplicates; + + // Always relevant for network (overrides bOnlyRelevantToOwner) + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bAlwaysRelevant; + + // If True, Play Rate will by multiplied by Custom Time Dilation + // Enabling this option will use Custom Time Dilation from actor that created Root Flow instance, i.e. World Settings or Player Controller + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bApplyOwnerTimeDilation; + protected: UPROPERTY() ULevelSequence* LoadedSequence; @@ -37,6 +69,9 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode UPROPERTY() UFlowLevelSequencePlayer* SequencePlayer; + // Play Rate set by the user in PlaybackSettings + float CachedPlayRate; + UPROPERTY(SaveGame) float StartTime; @@ -46,10 +81,12 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode UPROPERTY(SaveGame) float TimeDilation; + FStreamableManager StreamableManager; + public: #if WITH_EDITOR virtual bool SupportsContextPins() const override { return true; } - virtual TArray GetContextOutputs() override; + virtual TArray GetContextOutputs() override; virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; #endif @@ -57,11 +94,12 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode virtual void PreloadContent() override; virtual void FlushContent() override; - void CreatePlayer(const FMovieSceneSequencePlaybackSettings& PlaybackSettings); + virtual void InitializeInstance() override; + void CreatePlayer(); protected: virtual void ExecuteInput(const FName& PinName) override; - + virtual void OnSave_Implementation() override; virtual void OnLoad_Implementation() override; @@ -86,6 +124,8 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode #if WITH_EDITOR virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; + virtual FString GetStatusString() const override; virtual UObject* GetAssetToEdit() override; #endif diff --git a/Source/FlowEditor/FlowEditor.Build.cs b/Source/FlowEditor/FlowEditor.Build.cs index cf40227dd..00d4faf7d 100644 --- a/Source/FlowEditor/FlowEditor.Build.cs +++ b/Source/FlowEditor/FlowEditor.Build.cs @@ -1,44 +1,57 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + using UnrealBuildTool; public class FlowEditor : ModuleRules { - public FlowEditor(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + public FlowEditor(ReadOnlyTargetRules target) : base(target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange(new[] - { - "Flow" - }); + PublicDependencyModuleNames.AddRange(new[] + { + "EditorSubsystem", + "Flow", + "MessageLog" + }); - PrivateDependencyModuleNames.AddRange(new[] - { - "ApplicationCore", - "AssetTools", - "BlueprintGraph", - "ClassViewer", - "ContentBrowser", - "Core", - "CoreUObject", - "DetailCustomizations", - "DeveloperSettings", - "EditorStyle", - "Engine", - "GraphEditor", - "InputCore", - "KismetWidgets", - "LevelEditor", - "MovieScene", - "MovieSceneTracks", - "MovieSceneTools", - "Projects", - "PropertyEditor", - "RenderCore", - "Sequencer", - "Slate", - "SlateCore", - "ToolMenus", - "UnrealEd" - }); - } -} + PrivateDependencyModuleNames.AddRange(new[] + { + "ApplicationCore", + "AssetSearch", + "AssetTools", + "BlueprintGraph", + "ClassViewer", + "ContentBrowser", + "Core", + "CoreUObject", + "DetailCustomizations", + "DeveloperSettings", + "EditorFramework", + "EditorScriptingUtilities", + "EditorStyle", + "Engine", + "GraphEditor", + "InputCore", + "Json", + "JsonUtilities", + "Kismet", + "KismetWidgets", + "LevelEditor", + "LevelSequence", + "MovieScene", + "MovieSceneTools", + "MovieSceneTracks", + "Projects", + "PropertyEditor", + "PropertyPath", + "RenderCore", + "Sequencer", + "Slate", + "SlateCore", + "SourceControl", + "ToolMenus", + "UnrealEd" + }); + } +} \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/AssetTypeActions_FlowAsset.cpp b/Source/FlowEditor/Private/Asset/AssetTypeActions_FlowAsset.cpp index 180d87a0b..a9edee225 100644 --- a/Source/FlowEditor/Private/Asset/AssetTypeActions_FlowAsset.cpp +++ b/Source/FlowEditor/Private/Asset/AssetTypeActions_FlowAsset.cpp @@ -1,5 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Asset/AssetTypeActions_FlowAsset.h" +#include "Asset/SFlowDiff.h" #include "FlowEditorModule.h" +#include "Graph/FlowGraphSettings.h" #include "FlowAsset.h" @@ -14,7 +18,7 @@ FText FAssetTypeActions_FlowAsset::GetName() const uint32 FAssetTypeActions_FlowAsset::GetCategories() { - return FFlowEditorModule::FlowAssetCategory; + return UFlowGraphSettings::Get()->bExposeFlowAssetCreation ? FFlowEditorModule::FlowAssetCategory : 0; } UClass* FAssetTypeActions_FlowAsset::GetSupportedClass() const @@ -30,10 +34,31 @@ void FAssetTypeActions_FlowAsset::OpenAssetEditor(const TArray& InObje { if (UFlowAsset* FlowAsset = Cast(*ObjIt)) { - FFlowEditorModule* FlowModule = &FModuleManager::LoadModuleChecked("FlowEditor"); + const FFlowEditorModule* FlowModule = &FModuleManager::LoadModuleChecked("FlowEditor"); FlowModule->CreateFlowAssetEditor(Mode, EditWithinLevelEditor, FlowAsset); } } } +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Visual-Diff + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9659 + */ +#if ENABLE_FLOW_DIFF +void FAssetTypeActions_FlowAsset::PerformAssetDiff(UObject* OldAsset, UObject* NewAsset, const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision) const +{ + const UFlowAsset* OldFlow = CastChecked(OldAsset); + const UFlowAsset* NewFlow = CastChecked(NewAsset); + + // sometimes we're comparing different revisions of one single asset (other + // times we're comparing two completely separate assets altogether) + const bool bIsSingleAsset = (OldFlow->GetName() == NewFlow->GetName()); + + static const FText BasicWindowTitle = LOCTEXT("FlowAssetDiff", "FlowAsset Diff"); + const FText WindowTitle = !bIsSingleAsset ? BasicWindowTitle : FText::Format(LOCTEXT("FlowAsset Diff", "{0} - FlowAsset Diff"), FText::FromString(NewFlow->GetName())); + + SFlowDiff::CreateDiffWindow(WindowTitle, OldFlow, NewFlow, OldRevision, NewRevision); +} +#endif + #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp b/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp index 8866a0d35..5a3e8a0d6 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp @@ -1,44 +1,46 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Asset/FlowAssetEditor.h" -#include "Asset/FlowAssetToolbar.h" -#include "Asset/FlowDebugger.h" -#include "Asset/FlowDebuggerToolbar.h" #include "FlowEditorCommands.h" #include "FlowEditorModule.h" -#include "Graph/FlowGraph.h" +#include "FlowMessageLog.h" + +#include "Asset/FlowAssetEditorContext.h" +#include "Asset/FlowAssetToolbar.h" +#include "Asset/FlowMessageLogListing.h" +#include "Graph/FlowGraphEditor.h" #include "Graph/FlowGraphSchema.h" -#include "Graph/FlowGraphSchema_Actions.h" -#include "Graph/Nodes/FlowGraphNode.h" #include "Graph/Widgets/SFlowPalette.h" #include "FlowAsset.h" -#include "Nodes/FlowNode.h" -#include "Nodes/Route/FlowNode_SubGraph.h" -#include "EdGraphUtilities.h" #include "EdGraph/EdGraphNode.h" #include "Editor.h" -#include "EditorStyleSet.h" -#include "Framework/Commands/GenericCommands.h" +#include "EditorClassUtils.h" #include "GraphEditor.h" -#include "GraphEditorActions.h" -#include "HAL/PlatformApplicationMisc.h" #include "IDetailsView.h" -#include "Kismet2/BlueprintEditorUtils.h" +#include "IMessageLogListing.h" #include "Kismet2/DebuggerCommands.h" -#include "LevelEditor.h" +#include "MessageLogModule.h" +#include "Misc/UObjectToken.h" #include "Modules/ModuleManager.h" #include "PropertyEditorModule.h" -#include "ScopedTransaction.h" -#include "SNodePanel.h" #include "ToolMenus.h" #include "Widgets/Docking/SDockTab.h" -#define LOCTEXT_NAMESPACE "FlowEditor" +#if ENABLE_SEARCH_IN_ASSET_EDITOR +#include "Source/Private/Widgets/SSearchBrowser.h" +#endif + +#define LOCTEXT_NAMESPACE "FlowAssetEditor" const FName FFlowAssetEditor::DetailsTab(TEXT("Details")); const FName FFlowAssetEditor::GraphTab(TEXT("Graph")); const FName FFlowAssetEditor::PaletteTab(TEXT("Palette")); +const FName FFlowAssetEditor::RuntimeLogTab(TEXT("RuntimeLog")); +const FName FFlowAssetEditor::SearchTab(TEXT("Search")); +const FName FFlowAssetEditor::ValidationLogTab(TEXT("ValidationLog")); FFlowAssetEditor::FFlowAssetEditor() : FlowAsset(nullptr) @@ -68,7 +70,7 @@ void FFlowAssetEditor::PostRedo(bool bSuccess) void FFlowAssetEditor::HandleUndoTransaction() { SetUISelectionState(NAME_None); - FocusedGraphEditor->NotifyGraphChanged(); + GraphEditor->NotifyGraphChanged(); FSlateApplication::Get().DismissAllMenus(); } @@ -76,7 +78,7 @@ void FFlowAssetEditor::NotifyPostChange(const FPropertyChangedEvent& PropertyCha { if (PropertyChangedEvent.ChangeType != EPropertyChangeType::Interactive) { - FocusedGraphEditor->NotifyGraphChanged(); + GraphEditor->NotifyGraphChanged(); } } @@ -107,29 +109,93 @@ void FFlowAssetEditor::RegisterTabSpawners(const TSharedRef& FAssetEditorToolkit::RegisterTabSpawners(InTabManager); - InTabManager->RegisterTabSpawner(GraphTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_GraphCanvas)) - .SetDisplayName(LOCTEXT("GraphTab", "Viewport")) - .SetGroup(WorkspaceMenuCategoryRef) - .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "GraphEditor.EventGraph_16x")); - InTabManager->RegisterTabSpawner(DetailsTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Details)) .SetDisplayName(LOCTEXT("DetailsTab", "Details")) .SetGroup(WorkspaceMenuCategoryRef) .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details")); + InTabManager->RegisterTabSpawner(GraphTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Graph)) + .SetDisplayName(LOCTEXT("GraphTab", "Graph")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "GraphEditor.EventGraph_16x")); + InTabManager->RegisterTabSpawner(PaletteTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Palette)) .SetDisplayName(LOCTEXT("PaletteTab", "Palette")) .SetGroup(WorkspaceMenuCategoryRef) .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "Kismet.Tabs.Palette")); + + InTabManager->RegisterTabSpawner(RuntimeLogTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_RuntimeLog)) + .SetDisplayName(LOCTEXT("RuntimeLog", "Runtime Log")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "Kismet.Tabs.CompilerResults")); + +#if ENABLE_SEARCH_IN_ASSET_EDITOR + InTabManager->RegisterTabSpawner(SearchTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Search)) + .SetDisplayName(LOCTEXT("SearchTab", "Search")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "Kismet.Tabs.FindResults")); +#endif + + InTabManager->RegisterTabSpawner(ValidationLogTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_ValidationLog)) + .SetDisplayName(LOCTEXT("ValidationLog", "Validation Log")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "Debug")); } void FFlowAssetEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); - InTabManager->UnregisterTabSpawner(GraphTab); InTabManager->UnregisterTabSpawner(DetailsTab); + InTabManager->UnregisterTabSpawner(GraphTab); + InTabManager->UnregisterTabSpawner(ValidationLogTab); InTabManager->UnregisterTabSpawner(PaletteTab); +#if ENABLE_SEARCH_IN_ASSET_EDITOR + InTabManager->UnregisterTabSpawner(SearchTab); +#endif +} + +void FFlowAssetEditor::InitToolMenuContext(FToolMenuContext& MenuContext) +{ + FAssetEditorToolkit::InitToolMenuContext(MenuContext); + + UFlowAssetEditorContext* Context = NewObject(); + Context->FlowAssetEditor = SharedThis(this); + MenuContext.AddObject(Context); +} + +void FFlowAssetEditor::PostRegenerateMenusAndToolbars() +{ + // Provide a hyperlink to view our class + const TSharedRef MenuOverlayBox = SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .ColorAndOpacity(FSlateColor::UseSubduedForeground()) + .ShadowOffset(FVector2D::UnitVector) + .Text(LOCTEXT("FlowAssetEditor_AssetType", "Asset Type: ")) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + FEditorClassUtils::GetSourceLink(FlowAsset->GetClass(), nullptr) + ]; + + SetMenuOverlay(MenuOverlayBox); +} + +bool FFlowAssetEditor::IsTabFocused(const FTabId& TabId) const +{ + if (const TSharedPtr CurrentGraphTab = GetToolkitHost()->GetTabManager()->FindExistingLiveTab(TabId)) + { + return CurrentGraphTab->IsActive(); + } + + return false; } TSharedRef FFlowAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args) const @@ -137,23 +203,22 @@ TSharedRef FFlowAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Arg check(Args.GetTabId() == DetailsTab); return SNew(SDockTab) - .Icon(FEditorStyle::GetBrush("LevelEditor.Tabs.Details")) .Label(LOCTEXT("FlowDetailsTitle", "Details")) [ DetailsView.ToSharedRef() ]; } -TSharedRef FFlowAssetEditor::SpawnTab_GraphCanvas(const FSpawnTabArgs& Args) const +TSharedRef FFlowAssetEditor::SpawnTab_Graph(const FSpawnTabArgs& Args) const { check(Args.GetTabId() == GraphTab); TSharedRef SpawnedTab = SNew(SDockTab) .Label(LOCTEXT("FlowGraphTitle", "Graph")); - if (FocusedGraphEditor.IsValid()) + if (GraphEditor.IsValid()) { - SpawnedTab->SetContent(FocusedGraphEditor.ToSharedRef()); + SpawnedTab->SetContent(GraphEditor.ToSharedRef()); } return SpawnedTab; @@ -164,11 +229,49 @@ TSharedRef FFlowAssetEditor::SpawnTab_Palette(const FSpawnTabArgs& Arg check(Args.GetTabId() == PaletteTab); return SNew(SDockTab) - .Icon(FEditorStyle::GetBrush("Kismet.Tabs.Palette")) .Label(LOCTEXT("FlowPaletteTitle", "Palette")) - [ - Palette.ToSharedRef() - ]; + [ + Palette.ToSharedRef() + ]; +} + +TSharedRef FFlowAssetEditor::SpawnTab_RuntimeLog(const FSpawnTabArgs& Args) const +{ + check(Args.GetTabId() == RuntimeLogTab); + + return SNew(SDockTab) + .Label(LOCTEXT("FlowRuntimeLogTitle", "Runtime Log")) + [ + RuntimeLog.ToSharedRef() + ]; +} + +#if ENABLE_SEARCH_IN_ASSET_EDITOR +TSharedRef FFlowAssetEditor::SpawnTab_Search(const FSpawnTabArgs& Args) const +{ + check(Args.GetTabId() == SearchTab); + + return SNew(SDockTab) + .Label(LOCTEXT("FlowSearchTitle", "Search")) + [ + SNew(SBox) + .AddMetaData(FTagMetaData(TEXT("FlowSearch"))) + [ + SearchBrowser.ToSharedRef() + ] + ]; +} +#endif + +TSharedRef FFlowAssetEditor::SpawnTab_ValidationLog(const FSpawnTabArgs& Args) const +{ + check(Args.GetTabId() == ValidationLogTab); + + return SNew(SDockTab) + .Label(LOCTEXT("FlowValidationLogTitle", "Validation Log")) + [ + ValidationLog.ToSharedRef() + ]; } void FFlowAssetEditor::InitFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit) @@ -181,93 +284,69 @@ void FFlowAssetEditor::InitFlowAssetEditor(const EToolkitMode::Type Mode, const UFlowGraphSchema::SubscribeToAssetChanges(); - FGraphEditorCommands::Register(); - FFlowGraphCommands::Register(); - FFlowSpawnNodeCommands::Register(); + BindToolbarCommands(); + CreateToolbar(); CreateWidgets(); - BindGraphCommands(); - const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("FlowAssetEditor_Layout_v2") + const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("FlowAssetEditor_Layout_v5.1") ->AddArea ( - FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.1f) - ->AddTab(GetToolbarTabId(), ETabState::OpenedTab)->SetHideTabWell(true) - ) - ->Split - ( - FTabManager::NewSplitter()->SetOrientation(Orient_Horizontal)->SetSizeCoefficient(0.9f) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.225f) - ->AddTab(DetailsTab, ETabState::OpenedTab) - ) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.65f) - ->AddTab(GraphTab, ETabState::OpenedTab)->SetHideTabWell(true) - ) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.125f) - ->AddTab(PaletteTab, ETabState::OpenedTab) - ) - ) + FTabManager::NewPrimaryArea()->SetOrientation(Orient_Horizontal) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.225f) + ->AddTab(DetailsTab, ETabState::OpenedTab) + ) + ->Split + ( + FTabManager::NewSplitter() + ->SetSizeCoefficient(0.65f) + ->SetOrientation(Orient_Vertical) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.8f) + ->SetHideTabWell(true) + ->AddTab(GraphTab, ETabState::OpenedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.15f) + ->AddTab(RuntimeLogTab, ETabState::ClosedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.15f) + ->AddTab(SearchTab, ETabState::ClosedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.15f) + ->AddTab(ValidationLogTab, ETabState::ClosedTab) + ) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.125f) + ->AddTab(PaletteTab, ETabState::OpenedTab) + ) ); - const bool bCreateDefaultStandaloneMenu = true; - const bool bCreateDefaultToolbar = true; - FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, TEXT("FlowEditorApp"), StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ObjectToEdit, false); - - FFlowToolbarCommands::Register(); - - AddFlowAssetToolbar(); - AddPlayWorldToolbar(); - CreateFlowDebugger(); + constexpr bool bCreateDefaultStandaloneMenu = true; + constexpr bool bCreateDefaultToolbar = true; + InitAssetEditor(Mode, InitToolkitHost, TEXT("FlowEditorApp"), StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ObjectToEdit, false); - FFlowEditorModule* FlowEditorModule = &FModuleManager::LoadModuleChecked("FlowEditor"); - AddMenuExtender(FlowEditorModule->GetFlowAssetMenuExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects())); - - FlowAsset->OnRegenerateToolbars().AddSP(this, &FFlowAssetEditor::RegenerateMenusAndToolbars); -} - -void FFlowAssetEditor::AddFlowAssetToolbar() -{ - AssetToolbar = MakeShareable(new FFlowAssetToolbar(SharedThis(this))); - - BindAssetCommands(); - - TSharedPtr ToolbarExtender = MakeShareable(new FExtender); - ToolbarExtender->AddToolBarExtension( - "Asset", - EExtensionHook::After, - GetToolkitCommands(), - FToolBarExtensionDelegate::CreateSP(AssetToolbar.Get(), &FFlowAssetToolbar::AddToolbar) - ); - AddToolbarExtender(ToolbarExtender); -} - -void FFlowAssetEditor::BindAssetCommands() -{ - const FFlowToolbarCommands& NodeCommands = FFlowToolbarCommands::Get(); - - ToolkitCommands->MapAction(NodeCommands.RefreshAsset, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::RefreshAsset), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); + RegenerateMenusAndToolbars(); } -void FFlowAssetEditor::AddPlayWorldToolbar() const +void FFlowAssetEditor::CreateToolbar() { - // Append play world commands - ToolkitCommands->Append(FPlayWorldCommands::GlobalPlayWorldActions.ToSharedRef()); - FName ParentToolbarName; const FName ToolBarName = GetToolMenuToolbarName(ParentToolbarName); @@ -280,1053 +359,226 @@ void FFlowAssetEditor::AddPlayWorldToolbar() const if (FoundMenu) { - FToolMenuSection& Section = FoundMenu->AddSection("Debugging"); - Section.InsertPosition = FToolMenuInsert("Asset", EToolMenuInsertType::After); - - Section.AddDynamicEntry("DebuggingCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) - { - FPlayWorldCommands::BuildToolbar(InSection); - })); + AssetToolbar = MakeShareable(new FFlowAssetToolbar(SharedThis(this), FoundMenu)); } } -void FFlowAssetEditor::CreateFlowDebugger() -{ - Debugger = MakeShareable(new FFlowDebugger); - DebuggerToolbar = MakeShareable(new FFlowDebuggerToolbar(SharedThis(this))); - - BindDebuggerCommands(); - - TSharedPtr ToolbarExtender = MakeShareable(new FExtender); - ToolbarExtender->AddToolBarExtension( - "Debugging", - EExtensionHook::After, - GetToolkitCommands(), - FToolBarExtensionDelegate::CreateSP(DebuggerToolbar.Get(), &FFlowDebuggerToolbar::AddToolbar) - ); - AddToolbarExtender(ToolbarExtender); - - RegenerateMenusAndToolbars(); -} - -void FFlowAssetEditor::BindDebuggerCommands() +void FFlowAssetEditor::BindToolbarCommands() { - const FFlowToolbarCommands& NodeCommands = FFlowToolbarCommands::Get(); - - ToolkitCommands->MapAction(NodeCommands.GoToMasterInstance, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::GoToMasterInstance), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanGoToMasterInstance), - FIsActionChecked(), - FIsActionButtonVisible::CreateStatic(&FFlowAssetEditor::IsPIE)); -} + FFlowToolbarCommands::Register(); + const FFlowToolbarCommands& ToolbarCommands = FFlowToolbarCommands::Get(); -void FFlowAssetEditor::RefreshAsset() -{ - TArray FlowGraphNodes; - FlowAsset->GetGraph()->GetNodesOfClass(FlowGraphNodes); + // Editing + ToolkitCommands->MapAction(ToolbarCommands.RefreshAsset, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::RefreshAsset), + FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - for (UFlowGraphNode* GraphNode : FlowGraphNodes) - { - GraphNode->RefreshContextPins(true); - } -} + ToolkitCommands->MapAction(ToolbarCommands.ValidateAsset, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::ValidateAsset_Internal), + FCanExecuteAction()); -void FFlowAssetEditor::GoToMasterInstance() -{ - UFlowAsset* AssetThatInstancedThisAsset = FlowAsset->GetInspectedInstance()->GetMasterInstance(); +#if ENABLE_SEARCH_IN_ASSET_EDITOR + ToolkitCommands->MapAction(ToolbarCommands.SearchInAsset, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::SearchInAsset), + FCanExecuteAction()); +#endif - GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetThatInstancedThisAsset->GetTemplateAsset()); - AssetThatInstancedThisAsset->GetTemplateAsset()->SetInspectedInstance(AssetThatInstancedThisAsset->GetDisplayName()); -} + ToolkitCommands->MapAction(ToolbarCommands.EditAssetDefaults, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::EditAssetDefaults_Clicked), + FCanExecuteAction()); -bool FFlowAssetEditor::CanGoToMasterInstance() -{ - return FlowAsset->GetInspectedInstance() && FlowAsset->GetInspectedInstance()->GetNodeOwningThisAssetInstance() != nullptr; -} - -void FFlowAssetEditor::CreateWidgets() -{ - FocusedGraphEditor = CreateGraphWidget(); - - FDetailsViewArgs Args; - Args.bHideSelectionTip = true; - Args.bShowPropertyMatrixButton = false; - Args.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide; - Args.NotifyHook = this; - - FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); - DetailsView = PropertyModule.CreateDetailView(Args); - DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateStatic(&FFlowAssetEditor::CanEdit)); - DetailsView->SetObject(FlowAsset); + // Engine's Play commands + ToolkitCommands->Append(FPlayWorldCommands::GlobalPlayWorldActions.ToSharedRef()); - Palette = SNew(SFlowPalette, SharedThis(this)); + // Debugging + ToolkitCommands->MapAction(ToolbarCommands.GoToParentInstance, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::GoToParentInstance), + FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanGoToParentInstance), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanGoToParentInstance)); } -TSharedRef FFlowAssetEditor::CreateGraphWidget() +void FFlowAssetEditor::RefreshAsset() { - SGraphEditor::FGraphEditorEvents InEvents; - InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FFlowAssetEditor::OnSelectedNodesChanged); - InEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &FFlowAssetEditor::OnNodeDoubleClicked); - InEvents.OnTextCommitted = FOnNodeTextCommitted::CreateSP(this, &FFlowAssetEditor::OnNodeTitleCommitted); - InEvents.OnSpawnNodeByShortcut = SGraphEditor::FOnSpawnNodeByShortcut::CreateSP(this, &FFlowAssetEditor::OnSpawnGraphNodeByShortcut, static_cast(FlowAsset->GetGraph())); - - return SNew(SGraphEditor) - .AdditionalCommands(ToolkitCommands) - .IsEditable(true) - .Appearance(GetGraphAppearanceInfo()) - .GraphToEdit(FlowAsset->GetGraph()) - .GraphEvents(InEvents) - .AutoExpandActionMenu(true) - .ShowGraphStateOverlay(false); + // attempt to refresh graph, fix common issues automatically + CastChecked(FlowAsset->GetGraph())->RefreshGraph(); } -FGraphAppearanceInfo FFlowAssetEditor::GetGraphAppearanceInfo() const +void FFlowAssetEditor::ValidateAsset_Internal() { - FGraphAppearanceInfo AppearanceInfo; - AppearanceInfo.CornerText = GetCornerText(); + FFlowMessageLog LogResults; + ValidateAsset(LogResults); - if (Debugger.IsValid() && FFlowDebugger::IsPlaySessionPaused()) + // push messages to its window + ValidationLogListing->ClearMessages(); + if (LogResults.Messages.Num() > 0) { - AppearanceInfo.PIENotifyText = LOCTEXT("PausedLabel", "PAUSED"); + TabManager->TryInvokeTab(ValidationLogTab); + ValidationLogListing->AddMessages(LogResults.Messages); } - - return AppearanceInfo; -} - -FText FFlowAssetEditor::GetCornerText() const -{ - return LOCTEXT("AppearanceCornerText_FlowAsset", "FLOW"); + ValidationLogListing->OnDataChanged().Broadcast(); } -void FFlowAssetEditor::BindGraphCommands() +void FFlowAssetEditor::ValidateAsset(FFlowMessageLog& MessageLog) { - const FGenericCommands& GenericCommands = FGenericCommands::Get(); - const FGraphEditorCommandsImpl& GraphCommands = FGraphEditorCommands::Get(); - const FFlowGraphCommands& FlowGraphCommands = FFlowGraphCommands::Get(); - - // Graph commands - ToolkitCommands->MapAction(GraphCommands.CreateComment, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnCreateComment), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - - ToolkitCommands->MapAction(GraphCommands.StraightenConnections, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnStraightenConnections)); - - // Generic Node commands - ToolkitCommands->MapAction(GenericCommands.Undo, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::UndoGraphAction), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - - ToolkitCommands->MapAction(GenericCommands.Redo, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::RedoGraphAction), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - - ToolkitCommands->MapAction(GenericCommands.SelectAll, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::SelectAllNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanSelectAllNodes)); - - ToolkitCommands->MapAction(GenericCommands.Delete, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::DeleteSelectedNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDeleteNodes)); - - ToolkitCommands->MapAction(GenericCommands.Copy, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::CopySelectedNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanCopyNodes)); - - ToolkitCommands->MapAction(GenericCommands.Cut, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::CutSelectedNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanCutNodes)); - - ToolkitCommands->MapAction(GenericCommands.Paste, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::PasteNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanPasteNodes)); - - ToolkitCommands->MapAction(GenericCommands.Duplicate, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::DuplicateNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDuplicateNodes)); - - // Pin commands - ToolkitCommands->MapAction(FlowGraphCommands.RefreshContextPins, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::RefreshContextPins), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRefreshContextPins)); - - ToolkitCommands->MapAction(FlowGraphCommands.AddInput, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::AddInput), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddInput)); - - ToolkitCommands->MapAction(FlowGraphCommands.AddOutput, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::AddOutput), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddOutput)); - - ToolkitCommands->MapAction(FlowGraphCommands.RemovePin, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::RemovePin), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRemovePin)); - - // Breakpoint commands - ToolkitCommands->MapAction(GraphCommands.AddBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnAddBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanAddBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.RemoveBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnRemoveBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRemoveBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanRemoveBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.EnableBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnEnableBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanEnableBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanEnableBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.DisableBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnDisableBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDisableBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanDisableBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.ToggleBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnToggleBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanToggleBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanToggleBreakpoint) - ); - - // Pin Breakpoint commands - ToolkitCommands->MapAction(FlowGraphCommands.AddPinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnAddPinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddPinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanAddPinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.RemovePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnRemovePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRemovePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanRemovePinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.EnablePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnEnablePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanEnablePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanEnablePinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.DisablePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnDisablePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDisablePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanDisablePinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.TogglePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnTogglePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanTogglePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanTogglePinBreakpoint) - ); - - // Jump commands - ToolkitCommands->MapAction(FlowGraphCommands.FocusViewport, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::FocusViewport), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanFocusViewport)); - - ToolkitCommands->MapAction(FlowGraphCommands.JumpToNodeDefinition, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::JumpToNodeDefinition), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanJumpToNodeDefinition)); + FlowAsset->ValidateAsset(MessageLog); } -void FFlowAssetEditor::UndoGraphAction() +#if ENABLE_SEARCH_IN_ASSET_EDITOR +void FFlowAssetEditor::SearchInAsset() { - GEditor->UndoTransaction(); + TabManager->TryInvokeTab(SearchTab); + SearchBrowser->FocusForUse(); } +#endif -void FFlowAssetEditor::RedoGraphAction() +void FFlowAssetEditor::EditAssetDefaults_Clicked() const { - GEditor->RedoTransaction(); + DetailsView->SetObject(FlowAsset); } -FReply FFlowAssetEditor::OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2D& InPosition, UEdGraph* InGraph) +void FFlowAssetEditor::GoToParentInstance() { - UEdGraph* Graph = InGraph; - - if (FFlowSpawnNodeCommands::IsRegistered()) - { - TSharedPtr Action = FFlowSpawnNodeCommands::Get().GetActionByChord(InChord); - if (Action.IsValid()) - { - TArray DummyPins; - Action->PerformAction(Graph, DummyPins, InPosition); - return FReply::Handled(); - } - } + const UFlowAsset* AssetThatInstancedThisAsset = FlowAsset->GetInspectedInstance()->GetParentInstance(); - return FReply::Unhandled(); + GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetThatInstancedThisAsset->GetTemplateAsset()); + AssetThatInstancedThisAsset->GetTemplateAsset()->SetInspectedInstance(AssetThatInstancedThisAsset->GetDisplayName()); } -void FFlowAssetEditor::SetUISelectionState(const FName SelectionOwner) +bool FFlowAssetEditor::CanGoToParentInstance() { - if (SelectionOwner != CurrentUISelection) - { - ClearSelectionStateFor(CurrentUISelection); - CurrentUISelection = SelectionOwner; - } + return FlowAsset->GetInspectedInstance() && FlowAsset->GetInspectedInstance()->GetNodeOwningThisAssetInstance() != nullptr; } -void FFlowAssetEditor::ClearSelectionStateFor(const FName SelectionOwner) +void FFlowAssetEditor::CreateWidgets() { - if (SelectionOwner == GraphTab) - { - FocusedGraphEditor->ClearSelectionSet(); - } - else if (SelectionOwner == PaletteTab) + // Details View { - if (Palette.IsValid()) - { - Palette->ClearGraphActionMenuSelection(); - } - } -} - -void FFlowAssetEditor::OnCreateComment() const -{ - FFlowGraphSchemaAction_NewComment CommentAction; - CommentAction.PerformAction(FlowAsset->GetGraph(), nullptr, FocusedGraphEditor->GetPasteLocation()); -} - -void FFlowAssetEditor::OnStraightenConnections() const -{ - FocusedGraphEditor->OnStraightenConnections(); -} + FDetailsViewArgs Args; + Args.bHideSelectionTip = true; + Args.bShowPropertyMatrixButton = false; + Args.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide; + Args.NotifyHook = this; -bool FFlowAssetEditor::CanEdit() -{ - return GEditor->PlayWorld == nullptr; -} - -bool FFlowAssetEditor::IsPIE() -{ - return GEditor->PlayWorld != nullptr; -} - -EVisibility FFlowAssetEditor::GetDebuggerVisibility() -{ - return GEditor->PlayWorld ? EVisibility::Visible : EVisibility::Collapsed; -} - -TSet FFlowAssetEditor::GetSelectedFlowNodes() const -{ - TSet Result; - - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) - { - if (UFlowGraphNode* SelectedNode = Cast(*NodeIt)) - { - Result.Emplace(SelectedNode); - } + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + DetailsView = PropertyModule.CreateDetailView(Args); + DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateStatic(&FFlowAssetEditor::CanEdit)); + DetailsView->SetObject(FlowAsset); } - - return Result; -} -int32 FFlowAssetEditor::GetNumberOfSelectedNodes() const -{ - return FocusedGraphEditor->GetSelectedNodes().Num(); -} + // Graph + CreateGraphWidget(); + GraphEditor->OnSelectionChangedEvent.BindRaw(this, &FFlowAssetEditor::OnSelectedNodesChanged); -bool FFlowAssetEditor::GetBoundsForSelectedNodes(class FSlateRect& Rect, float Padding) const -{ - return FocusedGraphEditor->GetBoundsForSelectedNodes(Rect, Padding); -} + // Palette + Palette = SNew(SFlowPalette, SharedThis(this)); -void FFlowAssetEditor::OnSelectedNodesChanged(const TSet& Nodes) -{ - TArray SelectedObjects; + // Search +#if ENABLE_SEARCH_IN_ASSET_EDITOR + SearchBrowser = SNew(SSearchBrowser, GetFlowAsset()); +#endif - if (Nodes.Num() > 0) + // Logs + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); { - SetUISelectionState(GraphTab); - - for (TSet::TConstIterator SetIt(Nodes); SetIt; ++SetIt) - { - if (UFlowGraphNode* GraphNode = Cast(*SetIt)) - { - SelectedObjects.Add(Cast(GraphNode->GetFlowNode())); - } - else - { - SelectedObjects.Add(*SetIt); - } - } + RuntimeLogListing = FFlowMessageLogListing::GetLogListing(FlowAsset, EFlowLogType::Runtime); + RuntimeLogListing->OnMessageTokenClicked().AddSP(this, &FFlowAssetEditor::OnLogTokenClicked); + RuntimeLog = MessageLogModule.CreateLogListingWidget(RuntimeLogListing.ToSharedRef()); } - else { - SetUISelectionState(NAME_None); - SelectedObjects.Add(GetFlowAsset()); + ValidationLogListing = FFlowMessageLogListing::GetLogListing(FlowAsset, EFlowLogType::Validation); + ValidationLogListing->OnMessageTokenClicked().AddSP(this, &FFlowAssetEditor::OnLogTokenClicked); + ValidationLog = MessageLogModule.CreateLogListingWidget(ValidationLogListing.ToSharedRef()); } - - if (DetailsView.IsValid()) - { - DetailsView->SetObjects(SelectedObjects); - } -} - -void FFlowAssetEditor::SelectSingleNode(UEdGraphNode* Node) const -{ - FocusedGraphEditor->SetNodeSelection(Node, true); } -void FFlowAssetEditor::SelectAllNodes() const +void FFlowAssetEditor::CreateGraphWidget() { - FocusedGraphEditor->SelectAllNodes(); + SAssignNew(GraphEditor, SFlowGraphEditor, SharedThis(this)) + .DetailsView(DetailsView); } -bool FFlowAssetEditor::CanSelectAllNodes() const +bool FFlowAssetEditor::CanEdit() { - return true; + return GEditor->PlayWorld == nullptr; } -void FFlowAssetEditor::DeleteSelectedNodes() +void FFlowAssetEditor::SetUISelectionState(const FName SelectionOwner) { - const FScopedTransaction Transaction(LOCTEXT("DeleteSelectedNode", "Delete Selected Node")); - FocusedGraphEditor->GetCurrentGraph()->Modify(); - - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - SetUISelectionState(NAME_None); - - for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + if (SelectionOwner != CurrentUISelection) { - UEdGraphNode* Node = CastChecked(*NodeIt); - - if (Node->CanUserDeleteNode()) - { - if (UFlowGraphNode* FlowGraphNode = Cast(Node)) - { - if (FlowGraphNode->GetFlowNode()) - { - const FGuid NodeGuid = FlowGraphNode->GetFlowNode()->GetGuid(); - FBlueprintEditorUtils::RemoveNode(nullptr, Node, true); - FlowAsset->UnregisterNode(NodeGuid); - continue; - } - } - - FBlueprintEditorUtils::RemoveNode(nullptr, Node, true); - } + ClearSelectionStateFor(CurrentUISelection); + CurrentUISelection = SelectionOwner; } } -void FFlowAssetEditor::DeleteSelectedDuplicatableNodes() +void FFlowAssetEditor::ClearSelectionStateFor(const FName SelectionOwner) { - // Cache off the old selection - const FGraphPanelSelectionSet OldSelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - - // Clear the selection and only select the nodes that can be duplicated - FGraphPanelSelectionSet RemainingNodes; - FocusedGraphEditor->ClearSelectionSet(); - - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(OldSelectedNodes); SelectedIt; ++SelectedIt) - { - if (UEdGraphNode* Node = Cast(*SelectedIt)) - { - if (Node->CanDuplicateNode()) - { - FocusedGraphEditor->SetNodeSelection(Node, true); - } - else - { - RemainingNodes.Add(Node); - } - } - } - - // Delete the duplicatable nodes - DeleteSelectedNodes(); - - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(RemainingNodes); SelectedIt; ++SelectedIt) + if (SelectionOwner == GraphTab) { - if (UEdGraphNode* Node = Cast(*SelectedIt)) - { - FocusedGraphEditor->SetNodeSelection(Node, true); - } + GraphEditor->ClearSelectionSet(); } -} - -bool FFlowAssetEditor::CanDeleteNodes() const -{ - if (CanEdit()) + else if (SelectionOwner == PaletteTab) { - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + if (Palette.IsValid()) { - if (const UEdGraphNode* Node = Cast(*NodeIt)) - { - if (!Node->CanUserDeleteNode()) - { - return false; - } - } + Palette->ClearGraphActionMenuSelection(); } - - return SelectedNodes.Num() > 0; } - - return false; -} - -void FFlowAssetEditor::CutSelectedNodes() -{ - CopySelectedNodes(); - - // Cut should only delete nodes that can be duplicated - DeleteSelectedDuplicatableNodes(); } -bool FFlowAssetEditor::CanCutNodes() const +FName FFlowAssetEditor::GetUISelectionState() const { - return CanCopyNodes() && CanDeleteNodes(); + return CurrentUISelection; } -void FFlowAssetEditor::CopySelectedNodes() const +#if ENABLE_JUMP_TO_INNER_OBJECT +void FFlowAssetEditor::JumpToInnerObject(UObject* InnerObject) { - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + if (const UFlowNode* FlowNode = Cast(InnerObject)) { - if (UFlowGraphNode* Node = Cast(*SelectedIt)) - { - Node->PrepareForCopying(); - } + GraphEditor->JumpToNode(FlowNode->GetGraphNode(), true); } - - // Export the selected nodes and place the text on the clipboard - FString ExportedText; - FEdGraphUtilities::ExportNodesToText(SelectedNodes, /*out*/ ExportedText); - FPlatformApplicationMisc::ClipboardCopy(*ExportedText); - - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + else if (const UEdGraphNode* GraphNode = Cast(InnerObject)) { - if (UFlowGraphNode* Node = Cast(*SelectedIt)) - { - Node->PostCopyNode(); - } + GraphEditor->JumpToNode(GraphNode, true); } } +#endif -bool FFlowAssetEditor::CanCopyNodes() const +void FFlowAssetEditor::OnLogTokenClicked(const TSharedRef& Token) const { - if (CanEdit()) + if (Token->GetType() == EMessageToken::Object) { - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + const TSharedRef ObjectToken = StaticCastSharedRef(Token); + if (const UObject* Object = ObjectToken->GetObject().Get()) { - UEdGraphNode* Node = Cast(*SelectedIt); - if (Node && Node->CanDuplicateNode()) + if (Object->IsAsset()) { - return true; + GEditor->GetEditorSubsystem()->OpenEditorForAsset(const_cast(Object)); } - } - } - - return false; -} - -void FFlowAssetEditor::PasteNodes() -{ - PasteNodesHere(FocusedGraphEditor->GetPasteLocation()); -} - -void FFlowAssetEditor::PasteNodesHere(const FVector2D& Location) -{ - SetUISelectionState(NAME_None); - - // Undo/Redo support - const FScopedTransaction Transaction(LOCTEXT("PasteNode", "Paste Node")); - FlowAsset->GetGraph()->Modify(); - FlowAsset->Modify(); - - // Clear the selection set (newly pasted stuff will be selected) - FocusedGraphEditor->ClearSelectionSet(); - - // Grab the text to paste from the clipboard. - FString TextToImport; - FPlatformApplicationMisc::ClipboardPaste(TextToImport); - - // Import the nodes - TSet PastedNodes; - FEdGraphUtilities::ImportNodesFromText(FlowAsset->GetGraph(), TextToImport, /*out*/ PastedNodes); - - //Average position of nodes so we can move them while still maintaining relative distances to each other - FVector2D AvgNodePosition(0.0f, 0.0f); - - for (TSet::TIterator It(PastedNodes); It; ++It) - { - UEdGraphNode* Node = *It; - AvgNodePosition.X += Node->NodePosX; - AvgNodePosition.Y += Node->NodePosY; - } - - if (PastedNodes.Num() > 0) - { - const float InvNumNodes = 1.0f / static_cast(PastedNodes.Num()); - AvgNodePosition.X *= InvNumNodes; - AvgNodePosition.Y *= InvNumNodes; - } - - for (TSet::TIterator It(PastedNodes); It; ++It) - { - UEdGraphNode* Node = *It; - - // Give new node a different Guid from the old one - Node->CreateNewGuid(); - - if (UFlowGraphNode* FlowGraphNode = Cast(Node)) - { - FlowAsset->RegisterNode(Node->NodeGuid, FlowGraphNode->GetFlowNode()); - } - - // Select the newly pasted stuff - FocusedGraphEditor->SetNodeSelection(Node, true); - - Node->NodePosX = (Node->NodePosX - AvgNodePosition.X) + Location.X; - Node->NodePosY = (Node->NodePosY - AvgNodePosition.Y) + Location.Y; - - Node->SnapToGrid(SNodePanel::GetSnapGridSize()); - } - - // Force new pasted FlowNodes to have same connections as graph nodes - FlowAsset->HarvestNodeConnections(); - - // Update UI - FocusedGraphEditor->NotifyGraphChanged(); - - FlowAsset->PostEditChange(); - FlowAsset->MarkPackageDirty(); -} - -bool FFlowAssetEditor::CanPasteNodes() const -{ - if (CanEdit()) - { - FString ClipboardContent; - FPlatformApplicationMisc::ClipboardPaste(ClipboardContent); - - return FEdGraphUtilities::CanImportNodesFromText(FlowAsset->GetGraph(), ClipboardContent); - } - - return false; -} - -void FFlowAssetEditor::DuplicateNodes() -{ - CopySelectedNodes(); - PasteNodes(); -} - -bool FFlowAssetEditor::CanDuplicateNodes() const -{ - return CanCopyNodes(); -} - -void FFlowAssetEditor::OnNodeDoubleClicked(class UEdGraphNode* Node) const -{ - UFlowNode* FlowNode = Cast(Node)->GetFlowNode(); - - if (FlowNode) - { - const FString AssetPath = FlowNode->GetAssetPath(); - if (!AssetPath.IsEmpty()) - { - GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetPath); - } - else if (UObject* AssetToEdit = FlowNode->GetAssetToEdit()) - { - GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetToEdit); - - if (IsPIE()) + else { - if (UFlowNode_SubGraph* SubGraphNode = Cast(FlowNode)) - { - const TWeakObjectPtr SubFlowInstance = SubGraphNode->GetFlowAsset()->GetFlowInstance(SubGraphNode); - if (SubFlowInstance.IsValid()) - { - SubGraphNode->GetFlowAsset()->GetTemplateAsset()->SetInspectedInstance(SubFlowInstance->GetDisplayName()); - } - } + UE_LOG(LogFlowEditor, Warning, TEXT("Unknown type of hyperlinked object (%s), cannot focus it"), *GetNameSafe(Object)); } } } -} - -void FFlowAssetEditor::OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged) -{ - if (NodeBeingChanged) - { - const FScopedTransaction Transaction(LOCTEXT("RenameNode", "Rename Node")); - NodeBeingChanged->Modify(); - NodeBeingChanged->OnRenameNode(NewText.ToString()); - } -} - -void FFlowAssetEditor::RefreshContextPins() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + else if (Token->GetType() == EMessageToken::EdGraph && GraphEditor.IsValid()) { - SelectedNode->RefreshContextPins(true); - } -} + const TSharedRef EdGraphToken = StaticCastSharedRef(Token); -bool FFlowAssetEditor::CanRefreshContextPins() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + if (const UEdGraphPin* GraphPin = EdGraphToken->GetPin()) { - return SelectedNode->SupportsContextPins(); - } - } - - return false; -} - -void FFlowAssetEditor::AddInput() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->AddUserInput(); - } -} - -bool FFlowAssetEditor::CanAddInput() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->CanUserAddInput(); - } - } - - return false; -} - -void FFlowAssetEditor::AddOutput() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->AddUserOutput(); - } -} - -bool FFlowAssetEditor::CanAddOutput() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->CanUserAddOutput(); - } - } - - return false; -} - -void FFlowAssetEditor::RemovePin() const -{ - if (UEdGraphPin* SelectedPin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* SelectedNode = Cast(SelectedPin->GetOwningNode())) - { - SelectedNode->RemoveInstancePin(SelectedPin); - } - } -} - -bool FFlowAssetEditor::CanRemovePin() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + if (!GraphPin->IsPendingKill()) { - if (Pin->Direction == EGPD_Input) - { - return GraphNode->CanUserRemoveInput(Pin); - } - else - { - return GraphNode->CanUserRemoveOutput(Pin); - } + GraphEditor->JumpToPin(GraphPin); } } - } - - return false; -} - -void FFlowAssetEditor::OnAddBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.AddBreakpoint(); - } -} - -void FFlowAssetEditor::OnAddPinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints.Add(Pin, FFlowBreakpoint()); - GraphNode->PinBreakpoints[Pin].AddBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanAddBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return !SelectedNode->NodeBreakpoint.HasBreakpoint(); - } - - return false; -} - -bool FFlowAssetEditor::CanAddPinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return !GraphNode->PinBreakpoints.Contains(Pin) || !GraphNode->PinBreakpoints[Pin].HasBreakpoint(); - } - } - - return false; -} - -void FFlowAssetEditor::OnRemoveBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.RemoveBreakpoint(); - } -} - -void FFlowAssetEditor::OnRemovePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints.Remove(Pin); - } - } -} - -bool FFlowAssetEditor::CanRemoveBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->NodeBreakpoint.HasBreakpoint(); - } - - return false; -} - -bool FFlowAssetEditor::CanRemovePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + else if (const UEdGraphNode* GraphNode = EdGraphToken->GetGraphNode()) { - return GraphNode->PinBreakpoints.Contains(Pin); + GraphEditor->JumpToNode(GraphNode, true); } } - - return false; -} - -void FFlowAssetEditor::OnEnableBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.EnableBreakpoint(); - } -} - -void FFlowAssetEditor::OnEnablePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints[Pin].EnableBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanEnableBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return GraphNode->PinBreakpoints.Contains(Pin); - } - } - - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->NodeBreakpoint.CanEnableBreakpoint(); - } - - return false; -} - -bool FFlowAssetEditor::CanEnablePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return GraphNode->PinBreakpoints.Contains(Pin) && GraphNode->PinBreakpoints[Pin].CanEnableBreakpoint(); - } - } - - return false; -} - -void FFlowAssetEditor::OnDisableBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.DisableBreakpoint(); - } -} - -void FFlowAssetEditor::OnDisablePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints[Pin].DisableBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanDisableBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->NodeBreakpoint.IsBreakpointEnabled(); - } - - return false; -} - -bool FFlowAssetEditor::CanDisablePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return GraphNode->PinBreakpoints.Contains(Pin) && GraphNode->PinBreakpoints[Pin].IsBreakpointEnabled(); - } - } - - return false; -} - -void FFlowAssetEditor::OnToggleBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.ToggleBreakpoint(); - } -} - -void FFlowAssetEditor::OnTogglePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints.Add(Pin, FFlowBreakpoint()); - GraphNode->PinBreakpoints[Pin].ToggleBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanToggleBreakpoint() const -{ - return GetSelectedFlowNodes().Num() > 0; -} - -bool FFlowAssetEditor::CanTogglePinBreakpoint() const -{ - return FocusedGraphEditor->GetGraphPinForMenu() != nullptr; -} - -void FFlowAssetEditor::FocusViewport() const -{ - // Iterator used but should only contain one node - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - UFlowNode* FlowNode = Cast(SelectedNode)->GetFlowNode(); - if (UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) - { - if (AActor* ActorToFocus = NodeInstance->GetActorToFocus()) - { - GEditor->SelectNone(false, false, false); - GEditor->SelectActor(ActorToFocus, true, true, true); - GEditor->NoteSelectionChange(); - - GEditor->MoveViewportCamerasToActor(*ActorToFocus, false); - - FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); - TSharedPtr LevelEditorTab = LevelEditorModule.GetLevelEditorInstanceTab().Pin(); - if (LevelEditorTab.IsValid()) - { - LevelEditorTab->DrawAttention(); - } - } - } - - return; - } -} - -bool FFlowAssetEditor::CanFocusViewport() const -{ - return GetSelectedFlowNodes().Num() == 1; -} - -void FFlowAssetEditor::JumpToNodeDefinition() const -{ - // Iterator used but should only contain one node - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->JumpToDefinition(); - return; - } -} - -bool FFlowAssetEditor::CanJumpToNodeDefinition() const -{ - return GetSelectedFlowNodes().Num() == 1; } #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetEditorContext.cpp b/Source/FlowEditor/Private/Asset/FlowAssetEditorContext.cpp new file mode 100644 index 000000000..0d5501ddf --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowAssetEditorContext.cpp @@ -0,0 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetEditorContext.h" +#include "Asset/FlowAssetEditor.h" + +UFlowAsset* UFlowAssetEditorContext::GetFlowAsset() const +{ + return FlowAssetEditor.IsValid() ? FlowAssetEditor.Pin()->GetFlowAsset() : nullptr; +} diff --git a/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp b/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp index 128576097..c0900979a 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp @@ -1,6 +1,61 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Asset/FlowAssetFactory.h" #include "FlowAsset.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphSettings.h" + +#include "ClassViewerFilter.h" +#include "ClassViewerModule.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Kismet2/SClassPickerDialog.h" +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "FlowAssetFactory" + +class FAssetClassParentFilter : public IClassViewerFilter +{ +public: + FAssetClassParentFilter() + : DisallowedClassFlags(CLASS_None) + , bDisallowBlueprintBase(false) + { + } + + /** All children of these classes will be included unless filtered out by another setting. */ + TSet AllowedChildrenOfClasses; + + /** Disallowed class flags. */ + EClassFlags DisallowedClassFlags; + + /** Disallow blueprint base classes. */ + bool bDisallowBlueprintBase; + + virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, const TSharedRef InFilterFuncs) override + { + const bool bAllowed = !InClass->HasAnyClassFlags(DisallowedClassFlags) && InFilterFuncs->IfInChildOfClassesSet(AllowedChildrenOfClasses, InClass) != EFilterReturn::Failed; + + if (bAllowed && bDisallowBlueprintBase) + { + if (FKismetEditorUtilities::CanCreateBlueprintOfClass(InClass)) + { + return false; + } + } + + return bAllowed; + } + + virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef InUnloadedClassData, TSharedRef InFilterFuncs) override + { + if (bDisallowBlueprintBase) + { + return false; + } + + return !InUnloadedClassData->HasAnyClassFlags(DisallowedClassFlags) && InFilterFuncs->IfInChildOfClassesSet(AllowedChildrenOfClasses, InUnloadedClassData) != EFilterReturn::Failed; + } +}; UFlowAssetFactory::UFlowAssetFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -12,9 +67,60 @@ UFlowAssetFactory::UFlowAssetFactory(const FObjectInitializer& ObjectInitializer bEditAfterNew = true; } +bool UFlowAssetFactory::ConfigureProperties() +{ + const FText TitleText = LOCTEXT("CreateFlowAssetOptions", "Pick Flow Asset Class"); + + return ConfigurePropertiesInternal(TitleText); +} + +bool UFlowAssetFactory::ConfigurePropertiesInternal(const FText& TitleText) +{ + AssetClass = UFlowGraphSettings::Get()->DefaultFlowAssetClass; + if (AssetClass) // Class was set in settings + { + return true; + } + + // Load the Class Viewer module to display a class picker + FModuleManager::LoadModuleChecked("ClassViewer"); + + // Fill in options + FClassViewerInitializationOptions Options; + Options.Mode = EClassViewerMode::ClassPicker; + + const TSharedPtr Filter = MakeShareable(new FAssetClassParentFilter); + Filter->DisallowedClassFlags = CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists | CLASS_HideDropDown; + Filter->AllowedChildrenOfClasses.Add(SupportedClass); + + Options.ClassFilters = {Filter.ToSharedRef()}; + + UClass* ChosenClass = nullptr; + const bool bPressedOk = SClassPickerDialog::PickClass(TitleText, Options, ChosenClass, SupportedClass); + + if (bPressedOk) + { + AssetClass = ChosenClass; + } + + return bPressedOk; +} + UObject* UFlowAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { - UFlowAsset* NewFlow = NewObject(InParent, Class, Name, Flags | RF_Transactional, Context); - UFlowGraph::CreateGraph(NewFlow); - return NewFlow; + UFlowAsset* NewFlowAsset; + if (AssetClass) + { + NewFlowAsset = NewObject(InParent, AssetClass, Name, Flags | RF_Transactional, Context); + } + else + { + // if we have no asset class, use the passed-in class instead + NewFlowAsset = NewObject(InParent, Class, Name, Flags | RF_Transactional, Context); + } + + UFlowGraph::CreateGraph(NewFlowAsset); + return NewFlowAsset; } + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetIndexer.cpp b/Source/FlowEditor/Private/Asset/FlowAssetIndexer.cpp new file mode 100644 index 000000000..ccb58de6e --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowAssetIndexer.cpp @@ -0,0 +1,135 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetIndexer.h" + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9070 + */ +#include "FlowEditorDefines.h" +#if ENABLE_FLOW_SEARCH +#include "FlowAsset.h" +#include "Nodes/FlowNode.h" + +#include "Graph/Nodes/FlowGraphNode_Reroute.h" + +#include "EdGraph/EdGraphPin.h" +#include "EdGraphNode_Comment.h" +#include "Internationalization/Text.h" +#include "SearchSerializer.h" +#include "Utility/IndexerUtilities.h" + +#define LOCTEXT_NAMESPACE "FFlowAssetIndexer" + +enum class EFlowAssetIndexerVersion +{ + Empty, + Initial, + + // ------------------------------------------------------ + VersionPlusOne, + LatestVersion = VersionPlusOne - 1 +}; + +int32 FFlowAssetIndexer::GetVersion() const +{ + return static_cast(EFlowAssetIndexerVersion::LatestVersion); +} + +void FFlowAssetIndexer::IndexAsset(const UObject* InAssetObject, FSearchSerializer& Serializer) const +{ + const UFlowAsset* FlowAsset = CastChecked(InAssetObject); + + { + Serializer.BeginIndexingObject(FlowAsset, TEXT("$self")); + + FIndexerUtilities::IterateIndexableProperties(FlowAsset, [&Serializer](const FProperty* Property, const FString& Value) + { + Serializer.IndexProperty(Property, Value); + }); + + Serializer.EndIndexingObject(); + } + + IndexGraph(FlowAsset, Serializer); +} + +void FFlowAssetIndexer::IndexGraph(const UFlowAsset* InFlowAsset, FSearchSerializer& Serializer) const +{ + for (UEdGraphNode* Node : InFlowAsset->GetGraph()->Nodes) + { + // Ignore Reroutes + if (Cast(Node)) + { + continue; + } + + // Special rules for comment nodes + if (Cast(Node)) + { + Serializer.BeginIndexingObject(Node, Node->NodeComment); + Serializer.IndexProperty(TEXT("Comment"), Node->NodeComment); + Serializer.EndIndexingObject(); + continue; + } + + // Indexing UEdGraphNode + { + const FText NodeText = Node->GetNodeTitle(ENodeTitleType::MenuTitle); + Serializer.BeginIndexingObject(Node, NodeText); + Serializer.IndexProperty(TEXT("Title"), NodeText); + + if (!Node->NodeComment.IsEmpty()) + { + Serializer.IndexProperty(TEXT("Comment"), Node->NodeComment); + } + + for (const UEdGraphPin* Pin : Node->GetAllPins()) + { + if (Pin->Direction == EGPD_Input && Pin->LinkedTo.Num() == 0) + { + const FText PinText = Pin->GetDisplayName(); + if (PinText.IsEmpty()) + { + continue; + } + + const FText PinValue = Pin->GetDefaultAsText(); + if (PinValue.IsEmpty()) + { + continue; + } + + const FString PinLabel = TEXT("[Pin] ") + *FTextInspector::GetSourceString(PinText); + Serializer.IndexProperty(PinLabel, PinValue); + } + } + + // This will serialize any user exposed options for the node that are editable in the Details + FIndexerUtilities::IterateIndexableProperties(Node, [&Serializer](const FProperty* Property, const FString& Value) + { + Serializer.IndexProperty(Property, Value); + }); + + Serializer.EndIndexingObject(); + } + + // Indexing Flow Node + if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + if (const UFlowNode* FlowNode = FlowGraphNode->GetFlowNode()) + { + const FString NodeFriendlyName = FString::Printf(TEXT("%s: %s"), *FlowNode->GetClass()->GetName(), *FlowNode->GetNodeDescription()); + Serializer.BeginIndexingObject(FlowNode, NodeFriendlyName); + FIndexerUtilities::IterateIndexableProperties(FlowNode, [&Serializer](const FProperty* Property, const FString& Value) + { + Serializer.IndexProperty(Property, Value); + }); + Serializer.EndIndexingObject(); + } + } + } +} + +#undef LOCTEXT_NAMESPACE +#endif diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index 8fd02b2e5..e7f7d7fcc 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp @@ -1,23 +1,333 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Asset/FlowAssetToolbar.h" + #include "Asset/FlowAssetEditor.h" +#include "Asset/FlowAssetEditorContext.h" +#include "Asset/SAssetRevisionMenu.h" #include "FlowEditorCommands.h" +#include "FlowEditorDefines.h" + +#include "FlowAsset.h" + +#include "EditorStyleSet.h" +#include "Kismet2/DebuggerCommands.h" +#include "Misc/Attribute.h" +#include "Misc/MessageDialog.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "ToolMenu.h" +#include "ToolMenuSection.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" + +#include "AssetToolsModule.h" +#include "IAssetTypeActions.h" +#include "ISourceControlModule.h" +#include "ISourceControlProvider.h" +#include "SourceControlHelpers.h" + +#define LOCTEXT_NAMESPACE "FlowDebuggerToolbar" + +////////////////////////////////////////////////////////////////////////// +// Flow Asset Instance List + +FText SFlowAssetInstanceList::NoInstanceSelectedText = LOCTEXT("NoInstanceSelected", "No instance selected"); + +void SFlowAssetInstanceList::Construct(const FArguments& InArgs, const TWeakObjectPtr InTemplateAsset) +{ + TemplateAsset = InTemplateAsset; + if (TemplateAsset.IsValid()) + { + TemplateAsset->OnDebuggerRefresh().AddSP(this, &SFlowAssetInstanceList::RefreshInstances); + RefreshInstances(); + } + + // create dropdown + SAssignNew(Dropdown, SComboBox>) + .OptionsSource(&InstanceNames) + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + .OnGenerateWidget(this, &SFlowAssetInstanceList::OnGenerateWidget) + .OnSelectionChanged(this, &SFlowAssetInstanceList::OnSelectionChanged) + [ + SNew(STextBlock) + .Text(this, &SFlowAssetInstanceList::GetSelectedInstanceName) + ]; + + ChildSlot + [ + Dropdown.ToSharedRef() + ]; +} + +SFlowAssetInstanceList::~SFlowAssetInstanceList() +{ + if (TemplateAsset.IsValid()) + { + TemplateAsset->OnDebuggerRefresh().RemoveAll(this); + } +} + +void SFlowAssetInstanceList::RefreshInstances() +{ + // collect instance names of this Flow Asset + InstanceNames = {MakeShareable(new FName(*NoInstanceSelectedText.ToString()))}; + TemplateAsset->GetInstanceDisplayNames(InstanceNames); + + // select instance + if (const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) + { + const FName& InspectedInstanceName = InspectedInstance->GetDisplayName(); + for (const TSharedPtr& Instance : InstanceNames) + { + if (*Instance == InspectedInstanceName) + { + SelectedInstance = Instance; + break; + } + } + } + else + { + // default object is always available + SelectedInstance = InstanceNames[0]; + } +} + +EVisibility SFlowAssetInstanceList::GetDebuggerVisibility() +{ + return GEditor->PlayWorld ? EVisibility::Visible : EVisibility::Collapsed; +} + +TSharedRef SFlowAssetInstanceList::OnGenerateWidget(const TSharedPtr Item) const +{ + return SNew(STextBlock).Text(FText::FromName(*Item.Get())); +} + +void SFlowAssetInstanceList::OnSelectionChanged(const TSharedPtr SelectedItem, const ESelectInfo::Type SelectionType) +{ + if (SelectionType != ESelectInfo::Direct) + { + SelectedInstance = SelectedItem; + + if (TemplateAsset.IsValid()) + { + const FName NewSelectedInstanceName = (SelectedInstance.IsValid() && *SelectedInstance != *InstanceNames[0]) ? *SelectedInstance : NAME_None; + TemplateAsset->SetInspectedInstance(NewSelectedInstanceName); + } + } +} + +FText SFlowAssetInstanceList::GetSelectedInstanceName() const +{ + return SelectedInstance.IsValid() ? FText::FromName(*SelectedInstance) : NoInstanceSelectedText; +} + +////////////////////////////////////////////////////////////////////////// +// Flow Asset Breadcrumb + +void SFlowAssetBreadcrumb::Construct(const FArguments& InArgs, const TWeakObjectPtr InTemplateAsset) +{ + TemplateAsset = InTemplateAsset; + + // create breadcrumb + SAssignNew(BreadcrumbTrail, SBreadcrumbTrail) + .OnCrumbClicked(this, &SFlowAssetBreadcrumb::OnCrumbClicked) + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + .ButtonStyle(FEditorStyle::Get(), "FlatButton") + .DelimiterImage(FEditorStyle::GetBrush("Sequencer.BreadcrumbIcon")) + .PersistentBreadcrumbs(true) + .TextStyle(FEditorStyle::Get(), "Sequencer.BreadcrumbText"); + + ChildSlot + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .AutoHeight() + .Padding(25.0f, 10.0f) + [ + BreadcrumbTrail.ToSharedRef() + ] + ]; + + // fill breadcrumb + BreadcrumbTrail->ClearCrumbs(); + if (UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) + { + TArray> InstancesFromRoot = {InspectedInstance}; + + const UFlowAsset* CheckedInstance = InspectedInstance; + while (UFlowAsset* ParentInstance = CheckedInstance->GetParentInstance()) + { + InstancesFromRoot.Insert(ParentInstance, 0); + CheckedInstance = ParentInstance; + } + + for (TWeakObjectPtr Instance : InstancesFromRoot) + { + if (Instance.IsValid()) + { + const FFlowBreadcrumb NewBreadcrumb = FFlowBreadcrumb(Instance); + BreadcrumbTrail->PushCrumb(FText::FromName(NewBreadcrumb.InstanceName), FFlowBreadcrumb(Instance)); + } + } + } +} + +void SFlowAssetBreadcrumb::OnCrumbClicked(const FFlowBreadcrumb& Item) const +{ + const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance(); + if (InspectedInstance == nullptr || Item.InstanceName != InspectedInstance->GetDisplayName()) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(Item.AssetPathName); + } +} + +////////////////////////////////////////////////////////////////////////// +// Flow Asset Toolbar + +FFlowAssetToolbar::FFlowAssetToolbar(const TSharedPtr InAssetEditor, UToolMenu* ToolbarMenu) + : FlowAssetEditor(InAssetEditor) +{ + BuildAssetToolbar(ToolbarMenu); + BuildDebuggerToolbar(ToolbarMenu); +} -#include "Framework/MultiBox/MultiBoxBuilder.h" +void FFlowAssetToolbar::BuildAssetToolbar(UToolMenu* ToolbarMenu) const +{ + { + FToolMenuSection& Section = ToolbarMenu->AddSection("FlowAsset"); + Section.InsertPosition = FToolMenuInsert("Asset", EToolMenuInsertType::After); + + // add buttons + Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().RefreshAsset)); + Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().ValidateAsset)); + } + + { + FToolMenuSection& Section = ToolbarMenu->AddSection("View"); + Section.InsertPosition = FToolMenuInsert("FlowAsset", EToolMenuInsertType::After); + +#if ENABLE_FLOW_DIFF + // Visual Diff: menu to choose asset revision compared with the current one + Section.AddDynamicEntry("SourceControlCommands", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection) + { + InSection.InsertPosition = FToolMenuInsert(); + FToolMenuEntry DiffEntry = FToolMenuEntry::InitComboButton( + "Diff", + FUIAction(), + FOnGetContent::CreateRaw(this, &FFlowAssetToolbar::MakeDiffMenu), + LOCTEXT("Diff", "Diff"), + LOCTEXT("FlowAssetEditorDiffToolTip", "Diff against previous revisions"), + FSlateIcon(FAppStyle::Get().GetStyleSetName(), "BlueprintDiff.ToolbarIcon") + ); + DiffEntry.StyleNameOverride = "CalloutToolbar"; + InSection.AddEntry(DiffEntry); + })); +#endif -#define LOCTEXT_NAMESPACE "FlowAssetToolbar" +#if ENABLE_SEARCH_IN_ASSET_EDITOR + Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().SearchInAsset)); +#endif -FFlowAssetToolbar::FFlowAssetToolbar(TSharedPtr InNodeEditor) - : FlowAssetEditor(InNodeEditor) + Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().EditAssetDefaults)); + } +} + +/** Delegate called to diff a specific revision with the current */ +// Copy from FBlueprintEditorToolbar::OnDiffRevisionPicked +static void OnDiffRevisionPicked(FRevisionInfo const& RevisionInfo, const FString& Filename, TWeakObjectPtr CurrentAsset) { + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + + // Get the SCC state + const FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Filename, EStateCacheUsage::Use); + if (SourceControlState.IsValid()) + { + for (int32 HistoryIndex = 0; HistoryIndex < SourceControlState->GetHistorySize(); HistoryIndex++) + { + TSharedPtr Revision = SourceControlState->GetHistoryItem(HistoryIndex); + check(Revision.IsValid()); + if (Revision->GetRevision() == RevisionInfo.Revision) + { + // Get the revision of this package from source control + FString PreviousTempPkgName; + if (Revision->Get(PreviousTempPkgName)) + { + // Try and load that package + UPackage* PreviousTempPkg = LoadPackage(nullptr, *PreviousTempPkgName, LOAD_ForDiff | LOAD_DisableCompileOnLoad); + if (PreviousTempPkg) + { + const FString PreviousAssetName = FPaths::GetBaseFilename(Filename, true); + UObject* PreviousAsset = FindObject(PreviousTempPkg, *PreviousAssetName); + if (PreviousAsset) + { + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); + const FRevisionInfo OldRevision = {Revision->GetRevision(), Revision->GetCheckInIdentifier(), Revision->GetDate()}; + const FRevisionInfo CurrentRevision = {TEXT(""), Revision->GetCheckInIdentifier(), Revision->GetDate()}; + AssetToolsModule.Get().DiffAssets(PreviousAsset, CurrentAsset.Get(), OldRevision, CurrentRevision); + } + } + else + { + FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("SourceControl.HistoryWindow", "UnableToLoadAssets", "Unable to load assets to diff. Content may no longer be supported?")); + } + } + break; + } + } + } } -void FFlowAssetToolbar::AddToolbar(FToolBarBuilder& ToolbarBuilder) +// Variant of FBlueprintEditorToolbar::MakeDiffMenu +TSharedRef FFlowAssetToolbar::MakeDiffMenu() const { - ToolbarBuilder.BeginSection("FlowAsset"); + if (ISourceControlModule::Get().IsEnabled() && ISourceControlModule::Get().GetProvider().IsAvailable()) { - ToolbarBuilder.AddToolBarButton(FFlowToolbarCommands::Get().RefreshAsset); + UFlowAsset* FlowAsset = FlowAssetEditor.Pin()->GetFlowAsset(); + if (FlowAsset) + { + FString Filename = SourceControlHelpers::PackageFilename(FlowAsset->GetPathName()); + TWeakObjectPtr AssetPtr = FlowAsset; + + // Add our async SCC task widget + return SNew(SAssetRevisionMenu, Filename) + .OnRevisionSelected_Static(&OnDiffRevisionPicked, AssetPtr); + } + else + { + // if asset is null then this means that multiple assets are selected + FMenuBuilder MenuBuilder(true, nullptr); + MenuBuilder.AddMenuEntry(LOCTEXT("NoRevisionsForMultipleFlowAssets", "Multiple Flow Assets selected"), FText(), FSlateIcon(), FUIAction()); + return MenuBuilder.MakeWidget(); + } } - ToolbarBuilder.EndSection(); + + FMenuBuilder MenuBuilder(true, nullptr); + MenuBuilder.AddMenuEntry(LOCTEXT("SourceControlDisabled", "Source control is disabled"), FText(), FSlateIcon(), FUIAction()); + return MenuBuilder.MakeWidget(); +} + +void FFlowAssetToolbar::BuildDebuggerToolbar(UToolMenu* ToolbarMenu) const +{ + FToolMenuSection& Section = ToolbarMenu->AddSection("Debug"); + Section.InsertPosition = FToolMenuInsert("View", EToolMenuInsertType::After); + + Section.AddDynamicEntry("DebuggingCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) + { + const UFlowAssetEditorContext* Context = InSection.FindContext(); + if (Context && Context->GetFlowAsset()) + { + FPlayWorldCommands::BuildToolbar(InSection); + + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetInstances", SNew(SFlowAssetInstanceList, Context->GetFlowAsset()), FText(), true)); + + InSection.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().GoToParentInstance)); + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetBreadcrumb", SNew(SFlowAssetBreadcrumb, Context->GetFlowAsset()), FText(), true)); + } + })); } #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowDebugger.cpp b/Source/FlowEditor/Private/Asset/FlowDebugger.cpp deleted file mode 100644 index 7b918af01..000000000 --- a/Source/FlowEditor/Private/Asset/FlowDebugger.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "Asset/FlowDebugger.h" - -#include "Engine/Engine.h" -#include "Engine/World.h" -#include "Templates/Function.h" -#include "UnrealEd.h" - -FFlowDebugger::FFlowDebugger() -{ -} - -FFlowDebugger::~FFlowDebugger() -{ -} - -void ForEachGameWorld(const TFunction& Func) -{ - for (const FWorldContext& PieContext : GUnrealEd->GetWorldContexts()) - { - UWorld* PlayWorld = PieContext.World(); - if (PlayWorld && PlayWorld->IsGameWorld()) - { - Func(PlayWorld); - } - } -} - -bool AreAllGameWorldPaused() -{ - bool bPaused = true; - ForEachGameWorld([&](UWorld* World) - { - bPaused = bPaused && World->bDebugPauseExecution; - }); - return bPaused; -} - -void FFlowDebugger::PausePlaySession() -{ - bool bPaused = false; - ForEachGameWorld([&](UWorld* World) - { - if (!World->bDebugPauseExecution) - { - World->bDebugPauseExecution = true; - bPaused = true; - } - }); - if (bPaused) - { - GUnrealEd->PlaySessionPaused(); - } -} - -bool FFlowDebugger::IsPlaySessionPaused() -{ - return AreAllGameWorldPaused(); -} diff --git a/Source/FlowEditor/Private/Asset/FlowDebuggerSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebuggerSubsystem.cpp new file mode 100644 index 000000000..635882999 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowDebuggerSubsystem.cpp @@ -0,0 +1,129 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowDebuggerSubsystem.h" +#include "Asset/FlowAssetEditor.h" +#include "Asset/FlowMessageLogListing.h" + +#include "FlowSubsystem.h" + +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Templates/Function.h" +#include "UnrealEd.h" +#include "Widgets/Notifications/SNotificationList.h" + +#define LOCTEXT_NAMESPACE "FlowDebuggerSubsystem" + +UFlowDebuggerSubsystem::UFlowDebuggerSubsystem() +{ + FEditorDelegates::BeginPIE.AddUObject(this, &UFlowDebuggerSubsystem::OnBeginPIE); + FEditorDelegates::EndPIE.AddUObject(this, &UFlowDebuggerSubsystem::OnEndPIE); + + UFlowSubsystem::OnInstancedTemplateAdded.BindUObject(this, &UFlowDebuggerSubsystem::OnInstancedTemplateAdded); + UFlowSubsystem::OnInstancedTemplateRemoved.BindUObject(this, &UFlowDebuggerSubsystem::OnInstancedTemplateRemoved); +} + +void UFlowDebuggerSubsystem::OnInstancedTemplateAdded(UFlowAsset* FlowAsset) +{ + if (!RuntimeLogs.Contains(FlowAsset)) + { + RuntimeLogs.Add(FlowAsset, FFlowMessageLogListing::GetLogListing(FlowAsset, EFlowLogType::Runtime)); + FlowAsset->OnRuntimeMessageAdded().AddUObject(this, &UFlowDebuggerSubsystem::OnRuntimeMessageAdded); + } +} + +void UFlowDebuggerSubsystem::OnInstancedTemplateRemoved(UFlowAsset* FlowAsset) +{ + FlowAsset->OnRuntimeMessageAdded().RemoveAll(this); +} + +void UFlowDebuggerSubsystem::OnRuntimeMessageAdded(UFlowAsset* FlowAsset, const TSharedRef& Message) const +{ + const TSharedPtr Log = RuntimeLogs.FindRef(FlowAsset); + if (Log.IsValid()) + { + Log->AddMessage(Message); + Log->OnDataChanged().Broadcast(); + } +} + +void UFlowDebuggerSubsystem::OnBeginPIE(const bool bIsSimulating) +{ + // clear all logs from a previous session + RuntimeLogs.Empty(); +} + +void UFlowDebuggerSubsystem::OnEndPIE(const bool bIsSimulating) +{ + for (const TPair, TSharedPtr>& Log : RuntimeLogs) + { + if (Log.Key.IsValid() && Log.Value->NumMessages(EMessageSeverity::Warning) > 0) + { + FNotificationInfo Info{FText::FromString(TEXT("Flow Graph reported in-game issues"))}; + Info.ExpireDuration = 15.0; + + Info.HyperlinkText = FText::Format(LOCTEXT("OpenFlowAssetHyperlink", "Open {0}"), FText::FromString(Log.Key->GetName())); + Info.Hyperlink = FSimpleDelegate::CreateLambda([this, Log]() + { + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); + if (AssetEditorSubsystem->OpenEditorForAsset(Log.Key.Get())) + { + AssetEditorSubsystem->FindEditorForAsset(Log.Key.Get(), true)->InvokeTab(FFlowAssetEditor::RuntimeLogTab); + } + }); + + const TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification(Info); + if (Notification.IsValid()) + { + Notification->SetCompletionState(SNotificationItem::CS_Fail); + } + } + } +} + +void ForEachGameWorld(const TFunction& Func) +{ + for (const FWorldContext& PieContext : GUnrealEd->GetWorldContexts()) + { + UWorld* PlayWorld = PieContext.World(); + if (PlayWorld && PlayWorld->IsGameWorld()) + { + Func(PlayWorld); + } + } +} + +bool AreAllGameWorldPaused() +{ + bool bPaused = true; + ForEachGameWorld([&](const UWorld* World) + { + bPaused = bPaused && World->bDebugPauseExecution; + }); + return bPaused; +} + +void UFlowDebuggerSubsystem::PausePlaySession() +{ + bool bPaused = false; + ForEachGameWorld([&](UWorld* World) + { + if (!World->bDebugPauseExecution) + { + World->bDebugPauseExecution = true; + bPaused = true; + } + }); + if (bPaused) + { + GUnrealEd->PlaySessionPaused(); + } +} + +bool UFlowDebuggerSubsystem::IsPlaySessionPaused() +{ + return AreAllGameWorldPaused(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowDebuggerToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowDebuggerToolbar.cpp deleted file mode 100644 index 087020ded..000000000 --- a/Source/FlowEditor/Private/Asset/FlowDebuggerToolbar.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#include "Asset/FlowDebuggerToolbar.h" -#include "Asset/FlowAssetEditor.h" -#include "FlowEditorCommands.h" - -#include "FlowAsset.h" - -#include "EditorStyleSet.h" -#include "Framework/MultiBox/MultiBoxBuilder.h" -#include "Misc/Attribute.h" -#include "Subsystems/AssetEditorSubsystem.h" -#include "Widgets/Input/SComboBox.h" -#include "Widgets/SBoxPanel.h" -#include "Widgets/Text/STextBlock.h" - -#define LOCTEXT_NAMESPACE "FlowDebuggerToolbar" - -////////////////////////////////////////////////////////////////////////// -// Flow Instance List - -FText SFlowInstanceList::NoInstanceSelectedText = LOCTEXT("NoInstanceSelected", "No instance selected"); - -void SFlowInstanceList::Construct(const FArguments& InArgs, TWeakPtr InFlowAssetEditor) -{ - FlowAssetEditor = InFlowAssetEditor; - - // collect instance names of this Flow Asset - InstanceNames = {MakeShareable(new FName(*NoInstanceSelectedText.ToString()))}; - FlowAssetEditor.Pin()->GetFlowAsset()->GetInstanceDisplayNames(InstanceNames); - - // select instance - if (const UFlowAsset* InspectedInstance = FlowAssetEditor.Pin()->GetFlowAsset()->GetInspectedInstance()) - { - const FName& InspectedInstanceName = InspectedInstance->GetDisplayName(); - for (const TSharedPtr& Instance : InstanceNames) - { - if (*Instance == InspectedInstanceName) - { - SelectedInstance = Instance; - break; - } - } - } - else - { - // default object is always available - SelectedInstance = InstanceNames[0]; - } - - // create dropdown - SAssignNew(Dropdown, SComboBox>) - .OptionsSource(&InstanceNames) - .OnGenerateWidget(this, &SFlowInstanceList::OnGenerateWidget) - .OnSelectionChanged(this, &SFlowInstanceList::OnSelectionChanged) - .Visibility_Static(&FFlowAssetEditor::GetDebuggerVisibility) - [ - SNew(STextBlock) - .Text(this, &SFlowInstanceList::GetSelectedInstanceName) - ]; - - ChildSlot - [ - Dropdown.ToSharedRef() - ]; -} - -TSharedRef SFlowInstanceList::OnGenerateWidget(const TSharedPtr Item) const -{ - return SNew(STextBlock).Text(FText::FromName(*Item.Get())); -} - -void SFlowInstanceList::OnSelectionChanged(const TSharedPtr SelectedItem, const ESelectInfo::Type SelectionType) -{ - SelectedInstance = SelectedItem; - - if (FlowAssetEditor.IsValid() && FlowAssetEditor.Pin()->GetFlowAsset()) - { - const FName NewSelectedInstanceName = (SelectedInstance.IsValid() && *SelectedInstance != *InstanceNames[0]) ? *SelectedInstance : NAME_None; - FlowAssetEditor.Pin()->GetFlowAsset()->SetInspectedInstance(NewSelectedInstanceName); - } -} - -FText SFlowInstanceList::GetSelectedInstanceName() const -{ - return SelectedInstance.IsValid() ? FText::FromName(*SelectedInstance) : NoInstanceSelectedText; -} - -////////////////////////////////////////////////////////////////////////// -// Flow Breadcrumb - -void SFlowBreadcrumb::Construct(const FArguments& InArgs, TWeakPtr InFlowAssetEditor) -{ - FlowAssetEditor = InFlowAssetEditor; - - // create breadcrumb - SAssignNew(BreadcrumbTrail, SBreadcrumbTrail) - .OnCrumbClicked(this, &SFlowBreadcrumb::OnCrumbClicked) - .Visibility_Static(&FFlowAssetEditor::GetDebuggerVisibility) - .ButtonStyle(FEditorStyle::Get(), "FlatButton") - .DelimiterImage(FEditorStyle::GetBrush("Sequencer.BreadcrumbIcon")) - .PersistentBreadcrumbs(true) - .TextStyle(FEditorStyle::Get(), "Sequencer.BreadcrumbText"); - - ChildSlot - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .HAlign(HAlign_Right) - .VAlign(VAlign_Center) - .AutoHeight() - .Padding(25.0f, 0.0f) - [ - BreadcrumbTrail.ToSharedRef() - ] - ]; - - // fill breadcrumb - BreadcrumbTrail->ClearCrumbs(); - if (UFlowAsset* InspectedInstance = FlowAssetEditor.Pin()->GetFlowAsset()->GetInspectedInstance()) - { - TArray InstancesFromRoot = {InspectedInstance}; - - UFlowAsset* CheckedInstance = InspectedInstance; - while (UFlowAsset* MasterInstance = CheckedInstance->GetMasterInstance()) - { - InstancesFromRoot.Insert(MasterInstance, 0); - CheckedInstance = MasterInstance; - } - - for (UFlowAsset* Instance : InstancesFromRoot) - { - TAttribute CrumbNameAttribute = MakeAttributeSP(this, &SFlowBreadcrumb::GetBreadcrumbText, Instance); - BreadcrumbTrail->PushCrumb(CrumbNameAttribute, FFlowBreadcrumb(Instance)); - } - } -} - -void SFlowBreadcrumb::OnCrumbClicked(const FFlowBreadcrumb& Item) -{ - ensure(FlowAssetEditor.Pin()->GetFlowAsset()->GetInspectedInstance()); - - if (Item.InstanceName != FlowAssetEditor.Pin()->GetFlowAsset()->GetInspectedInstance()->GetDisplayName()) - { - GEditor->GetEditorSubsystem()->OpenEditorForAsset(Item.AssetPathName); - } -} - -FText SFlowBreadcrumb::GetBreadcrumbText(const TWeakObjectPtr FlowInstance) const -{ - return FlowInstance.IsValid() ? FText::FromName(FlowInstance->GetDisplayName()) : FText(); -} - -////////////////////////////////////////////////////////////////////////// -// Flow Debugger Toolbar - -FFlowDebuggerToolbar::FFlowDebuggerToolbar(TSharedPtr InNodeEditor) - : FlowAssetEditor(InNodeEditor) -{ -} - -void FFlowDebuggerToolbar::AddToolbar(FToolBarBuilder& ToolbarBuilder) -{ - ToolbarBuilder.BeginSection(""); - { - FlowInstanceList = SNew(SFlowInstanceList, FlowAssetEditor); - ToolbarBuilder.AddWidget(FlowInstanceList.ToSharedRef()); - - ToolbarBuilder.AddToolBarButton(FFlowToolbarCommands::Get().GoToMasterInstance); - - FlowBreadcrumb = SNew(SFlowBreadcrumb, FlowAssetEditor); - ToolbarBuilder.AddWidget(FlowBreadcrumb.ToSharedRef()); - } - ToolbarBuilder.EndSection(); -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowDiffControl.cpp b/Source/FlowEditor/Private/Asset/FlowDiffControl.cpp new file mode 100644 index 000000000..b71e2563d --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowDiffControl.cpp @@ -0,0 +1,195 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowDiffControl.h" + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Visual-Diff + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9659 + */ + +#if ENABLE_FLOW_DIFF +#include "Asset/SFlowDiff.h" + +#include "FlowAsset.h" + +#include "EdGraph/EdGraph.h" +#include "GraphDiffControl.h" +#include "Launch/Resources/Version.h" +#include "SBlueprintDiff.h" + +#define LOCTEXT_NAMESPACE "SFlowDiffControl" + +///////////////////////////////////////////////////////////////////////////// +/// FFlowAssetDiffControl + +FFlowAssetDiffControl::FFlowAssetDiffControl(const UFlowAsset* InOldFlowAsset, const UFlowAsset* InNewFlowAsset, FOnDiffEntryFocused InSelectionCallback) + : TDetailsDiffControl(InOldFlowAsset, InNewFlowAsset, InSelectionCallback) +{ +} + +// TDetailsDiffControl::GenerateTreeEntries + "NoDifferences" entry + category label +void FFlowAssetDiffControl::GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) +{ + TDetailsDiffControl::GenerateTreeEntries(OutTreeEntries, OutRealDifferences); + + const bool bHasDifferences = Children.Num() != 0; + if (!bHasDifferences) + { + // make one child informing the user that there are no differences: + Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry()); + } + + static const FText AssetPropertiesLabel = LOCTEXT("AssetPropertiesLabel", "Properties"); + static const FText AssetPropertiesTooltip = LOCTEXT("AssetPropertiesTooltip", "The list of changes made to Flow Asset properties."); + OutTreeEntries.Push(FBlueprintDifferenceTreeEntry::CreateCategoryEntry( + AssetPropertiesLabel, + AssetPropertiesTooltip, + SelectionCallback, + Children, + bHasDifferences + )); +} + +///////////////////////////////////////////////////////////////////////////// +/// FFlowGraphToDiff + +FFlowGraphToDiff::FFlowGraphToDiff(SFlowDiff* InDiffWidget, UEdGraph* InGraphOld, UEdGraph* InGraphNew, const FRevisionInfo& InRevisionOld, const FRevisionInfo& InRevisionNew) + : FoundDiffs(MakeShared>()) + , DiffWidget(InDiffWidget) + , GraphOld(InGraphOld) + , GraphNew(InGraphNew) + , RevisionOld(InRevisionOld) + , RevisionNew(InRevisionNew) +{ + check(InGraphOld || InGraphNew); + + if (InGraphNew) + { + OnGraphChangedDelegateHandle = InGraphNew->AddOnGraphChangedHandler(FOnGraphChanged::FDelegate::CreateRaw(this, &FFlowGraphToDiff::OnGraphChanged)); + } + + BuildDiffSourceArray(); +} + +FFlowGraphToDiff::~FFlowGraphToDiff() +{ + if (GraphNew) + { + GraphNew->RemoveOnGraphChangedHandler(OnGraphChangedDelegateHandle); + } +} + +void FFlowGraphToDiff::GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) +{ + if (!DiffListSource.IsEmpty()) + { + RealDifferencesStartIndex = OutRealDifferences.Num(); + } + + TArray> Children; + for (const TSharedPtr& Difference : DiffListSource) + { + TSharedPtr ChildEntry = MakeShared( + FOnDiffEntryFocused::CreateRaw(DiffWidget, &SFlowDiff::OnDiffListSelectionChanged, Difference), + FGenerateDiffEntryWidget::CreateSP(Difference.ToSharedRef(), &FDiffResultItem::GenerateWidget)); + Children.Push(ChildEntry); + OutRealDifferences.Push(ChildEntry); + } + + if (Children.Num() == 0) + { + // make one child informing the user that there are no differences: + Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry()); + } + + const TSharedPtr Entry = MakeShared( + FOnDiffEntryFocused::CreateRaw(DiffWidget, &SFlowDiff::OnGraphSelectionChanged, TSharedPtr(AsShared()), ESelectInfo::Direct), + FGenerateDiffEntryWidget::CreateSP(AsShared(), &FFlowGraphToDiff::GenerateCategoryWidget), + Children); + OutTreeEntries.Push(Entry); +} + +FText FFlowGraphToDiff::GetToolTip() const +{ + if (GraphOld && GraphNew) + { + if (DiffListSource.Num() > 0) + { + return LOCTEXT("ContainsDifferences", "Revisions are different"); + } + else + { + return LOCTEXT("GraphsIdentical", "Revisions appear to be identical"); + } + } + else + { + const UEdGraph* GoodGraph = GraphOld ? GraphOld : GraphNew; + check(GoodGraph); + const FRevisionInfo& Revision = GraphNew ? RevisionOld : RevisionNew; + FText RevisionText = LOCTEXT("CurrentRevision", "Current Revision"); + + if (!Revision.Revision.IsEmpty()) + { + RevisionText = FText::Format(LOCTEXT("Revision Number", "Revision {0}"), FText::FromString(Revision.Revision)); + } + + return FText::Format(LOCTEXT("MissingGraph", "Graph '{0}' missing from {1}"), FText::FromString(GoodGraph->GetName()), RevisionText); + } +} + +TSharedRef FFlowGraphToDiff::GenerateCategoryWidget() const +{ + const UEdGraph* Graph = GraphOld ? GraphOld : GraphNew; + check(Graph); + + FLinearColor Color = (GraphOld && GraphNew) ? DiffViewUtils::Identical() : FLinearColor(0.3f, 0.3f, 1.f); + + const bool bHasDiffs = DiffListSource.Num() > 0; + + if (bHasDiffs) + { + Color = DiffViewUtils::Differs(); + } + + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .ColorAndOpacity(Color) + .Text(FText::FromString(TEXT("Graph"))) + .ToolTipText(GetToolTip()) + ] + + DiffViewUtils::Box(GraphOld != nullptr, Color) + + DiffViewUtils::Box(GraphNew != nullptr, Color); +} + +void FFlowGraphToDiff::BuildDiffSourceArray() +{ + FoundDiffs->Empty(); + FGraphDiffControl::DiffGraphs(GraphOld, GraphNew, *FoundDiffs); + + struct SortDiff + { + bool operator ()(const FDiffSingleResult& A, const FDiffSingleResult& B) const + { + return A.Diff < B.Diff; + } + }; + + Sort(FoundDiffs->GetData(), FoundDiffs->Num(), SortDiff()); + + DiffListSource.Empty(); + for (const FDiffSingleResult& Diff : *FoundDiffs) + { + DiffListSource.Add(MakeShared(Diff)); + } +} + +void FFlowGraphToDiff::OnGraphChanged(const FEdGraphEditAction& Action) const +{ + DiffWidget->OnGraphChanged(this); +} + +#undef LOCTEXT_NAMESPACE +#endif diff --git a/Source/FlowEditor/Private/Asset/FlowImportUtils.cpp b/Source/FlowEditor/Private/Asset/FlowImportUtils.cpp new file mode 100644 index 000000000..f2c2e173a --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowImportUtils.cpp @@ -0,0 +1,412 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowImportUtils.h" + +#include "Asset/FlowAssetFactory.h" +#include "FlowEditorDefines.h" +#include "FlowEditorModule.h" +#include "Graph/FlowGraphSchema_Actions.h" +#include "Graph/FlowGraph.h" + +#include "FlowAsset.h" +#include "Nodes/FlowPin.h" +#include "Nodes/Route/FlowNode_Start.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "EdGraphSchema_K2.h" +#include "EditorAssetLibrary.h" +#include "Misc/ScopedSlowTask.h" + +#if ENABLE_ASYNC_NODES_IMPORT +#include "K2Node_BaseAsyncTask.h" +#endif +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_Knot.h" + +#define LOCTEXT_NAMESPACE "FlowImportUtils" + +TMap> UFlowImportUtils::FunctionsToFlowNodes = TMap>(); +TMap, FBlueprintToFlowPinName> UFlowImportUtils::PinMappings = TMap, FBlueprintToFlowPinName>(); + +UFlowAsset* UFlowImportUtils::ImportBlueprintGraph(UObject* BlueprintAsset, const TSubclassOf FlowAssetClass, const FString FlowAssetName, + const TMap> InFunctionsToFlowNodes, const TMap, FBlueprintToFlowPinName> InPinMappings, const FName StartEventName) +{ + if (BlueprintAsset == nullptr || FlowAssetClass == nullptr || FlowAssetName.IsEmpty() || StartEventName.IsNone()) + { + return nullptr; + } + + UBlueprint* Blueprint = Cast(BlueprintAsset); + UFlowAsset* FlowAsset = nullptr; + + // we assume that users want to have a converted asset in the same folder as the legacy blueprint + const FString PackageFolder = FPaths::GetPath(Blueprint->GetOuter()->GetPathName()); + + if (!FPackageName::DoesPackageExist(PackageFolder / FlowAssetName, nullptr)) // create a new asset + { + IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); + UFactory* Factory = Cast(UFlowAssetFactory::StaticClass()->GetDefaultObject()); + + if (UObject* NewAsset = AssetTools.CreateAsset(FlowAssetName, PackageFolder, FlowAssetClass, Factory)) + { + FlowAsset = Cast(NewAsset); + } + } + else // load existing asset + { + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); + + const FString PackageName = PackageFolder / (FlowAssetName + TEXT(".") + FlowAssetName); + const FAssetData& FoundAssetData = AssetRegistryModule.GetRegistry().GetAssetByObjectPath(*PackageName); + + FlowAsset = Cast(FoundAssetData.GetAsset()); + } + + // import graph + if (FlowAsset) + { + FunctionsToFlowNodes = InFunctionsToFlowNodes; + PinMappings = InPinMappings; + + ImportBlueprintGraph(Blueprint, FlowAsset, StartEventName); + FunctionsToFlowNodes.Empty(); + PinMappings.Empty(); + + Cast(FlowAsset->GetGraph())->RefreshGraph(); + UEditorAssetLibrary::SaveLoadedAsset(FlowAsset->GetPackage()); + } + + return FlowAsset; +} + +void UFlowImportUtils::ImportBlueprintGraph(UBlueprint* Blueprint, UFlowAsset* FlowAsset, const FName StartEventName) +{ + ensureAlways(Blueprint && FlowAsset); + + UEdGraph* BlueprintGraph = Blueprint->UbergraphPages.IsValidIndex(0) ? Blueprint->UbergraphPages[0] : nullptr; + if (BlueprintGraph == nullptr) + { + return; + } + + FScopedSlowTask ExecuteAssetTask(BlueprintGraph->Nodes.Num(), FText::Format(LOCTEXT("FFlowGraphUtils::ImportBlueprintGraph", "Reading {0}"), FText::FromString(Blueprint->GetFriendlyName()))); + ExecuteAssetTask.MakeDialog(); + + TMap SourceNodes; + UEdGraphNode* StartNode = nullptr; + + for (UEdGraphNode* ThisNode : BlueprintGraph->Nodes) + { + ExecuteAssetTask.EnterProgressFrame(1, FText::Format(LOCTEXT("FFlowGraphUtils::ImportBlueprintGraph", "Processing blueprint node: {0}"), ThisNode->GetNodeTitle(ENodeTitleType::ListView))); + + // non-pure K2Nodes or UK2Node_Knot + const UK2Node* K2Node = Cast(ThisNode); + if (K2Node && (!K2Node->IsNodePure() || Cast(K2Node))) + { + FImportedGraphNode& NodeImport = SourceNodes.FindOrAdd(ThisNode->NodeGuid); + NodeImport.SourceGraphNode = ThisNode; + + // create map of all non-pure blueprint nodes with theirs pin connections + for (const UEdGraphPin* ThisPin : ThisNode->Pins) + { + for (const UEdGraphPin* LinkedPin : ThisPin->LinkedTo) + { + if (LinkedPin && LinkedPin->GetOwningNode()) + { + const FConnectedPin ConnectedPin(LinkedPin->GetOwningNode()->NodeGuid, LinkedPin->PinName); + + if (ThisPin->Direction == EGPD_Input) + { + NodeImport.Incoming.Add(ThisPin->PinName, ConnectedPin); + } + else + { + NodeImport.Outgoing.Add(ThisPin->PinName, ConnectedPin); + } + } + } + } + + // we need to know the default entry point of blueprint graph + const UK2Node_Event* EventNode = Cast(ThisNode); + if (EventNode && (EventNode->EventReference.GetMemberName() == StartEventName || EventNode->CustomFunctionName == StartEventName)) + { + StartNode = ThisNode; + } + } + } + + // can't start import if provided graph doesn't have required start node + if (StartNode == nullptr) + { + return; + } + + // clear existing graph + UFlowGraph* FlowGraph = Cast(FlowAsset->GetGraph()); + for (const TPair& Node : FlowAsset->GetNodes()) + { + if (UFlowGraphNode* FlowGraphNode = Cast(Node.Value->GetGraphNode())) + { + FlowGraph->GetSchema()->BreakNodeLinks(*FlowGraphNode); + FlowGraphNode->DestroyNode(); + } + + FlowAsset->UnregisterNode(Node.Key); + } + + TMap TargetNodes; + + // recreated UFlowNode_Start, assign it a blueprint node FGuid + UFlowGraphNode* StartGraphNode = FFlowGraphSchemaAction_NewNode::CreateNode(FlowGraph, nullptr, UFlowNode_Start::StaticClass(), FVector2D::ZeroVector); + FlowGraph->GetSchema()->SetNodeMetaData(StartGraphNode, FNodeMetadata::DefaultGraphNode); + StartGraphNode->NodeGuid = StartNode->NodeGuid; + StartGraphNode->GetFlowNode()->SetGuid(StartNode->NodeGuid); + TargetNodes.Add(StartGraphNode->NodeGuid, StartGraphNode); + + // execute graph import + // iterate all nodes separately, ensures we import all possible nodes and connect them together + for (const TPair& SourceNode : SourceNodes) + { + ImportBlueprintFunction(FlowAsset, SourceNode.Value, SourceNodes, TargetNodes); + } +} + +void UFlowImportUtils::ImportBlueprintFunction(const UFlowAsset* FlowAsset, const FImportedGraphNode& NodeImport, const TMap& SourceNodes, TMap& TargetNodes) +{ + ensureAlways(NodeImport.SourceGraphNode); + TSubclassOf MatchingFlowNodeClass = nullptr; + + // find FlowNode class matching provided UFunction name + FName FunctionName = NAME_None; + if (const UK2Node_CallFunction* FunctionNode = Cast(NodeImport.SourceGraphNode)) + { + FunctionName = FunctionNode->GetFunctionName(); + } +#if ENABLE_ASYNC_NODES_IMPORT + else if (const UK2Node_BaseAsyncTask* AsyncTaskNode = Cast(NodeImport.SourceGraphNode)) + { + FunctionName = AsyncTaskNode->GetProxyFactoryFunctionName(); + } +#endif + else if (Cast(NodeImport.SourceGraphNode)) + { + FunctionName = TEXT("Reroute"); + } + else if (Cast(NodeImport.SourceGraphNode)) + { + FunctionName = TEXT("Branch"); + } + + if (!FunctionName.IsNone()) + { + // find FlowNode class matching provided UFunction name + MatchingFlowNodeClass = FunctionsToFlowNodes.FindRef(FunctionName); + } + + if (MatchingFlowNodeClass == nullptr) + { + UE_LOG(LogFlowEditor, Error, TEXT("Can't find Flow Node class for K2Node, function name %s"), *FunctionName.ToString()); + return; + } + + const FGuid& NodeGuid = NodeImport.SourceGraphNode->NodeGuid; + + // create a new Flow Graph node + const FVector2d Location = FVector2D(NodeImport.SourceGraphNode->NodePosX, NodeImport.SourceGraphNode->NodePosY); + UFlowGraphNode* FlowGraphNode = FFlowGraphSchemaAction_NewNode::ImportNode(FlowAsset->GetGraph(), nullptr, MatchingFlowNodeClass, NodeGuid, Location); + + if (FlowGraphNode == nullptr) + { + return; + } + TargetNodes.Add(NodeGuid, FlowGraphNode); + + // transfer properties from UFunction input parameters to Flow Node properties + { + TMap InputPins; + GetValidInputPins(NodeImport.SourceGraphNode, InputPins); + + UClass* FlowNodeClass = FlowGraphNode->GetFlowNode()->GetClass(); + for (TFieldIterator PropIt(FlowNodeClass, EFieldIteratorFlags::IncludeSuper); PropIt && (PropIt->PropertyFlags & CPF_Edit); ++PropIt) + { + const FProperty* Param = *PropIt; + const bool bIsEditable = !Param->HasAnyPropertyFlags(CPF_Deprecated); + if (bIsEditable) + { + if (const UEdGraphPin* MatchingInputPin = FindPinMatchingToProperty(FlowNodeClass, Param, InputPins)) + { + if (MatchingInputPin->LinkedTo.Num() == 0) // nothing connected to pin, so user can set value directly on this pin + { + FString const PinValue = MatchingInputPin->GetDefaultAsString(); + uint8* Offset = Param->ContainerPtrToValuePtr(FlowGraphNode->GetFlowNode()); + Param->ImportText(*PinValue, Offset, PPF_Copy, nullptr, GLog); + } + } + else // try to find matching Pin in connected pure nodes + { + bool bPinFound = false; + for (const TPair InputPin : InputPins) + { + for (const UEdGraphPin* LinkedPin : InputPin.Value->LinkedTo) + { + if (LinkedPin && LinkedPin->GetOwningNode()) // try to read value from the first pure node connected to the pin + { + // in theory, we could put this part in recursive loop, iterating pure nodes until we find one with matching Pin Name + // in practice, iterating blueprint graph isn't that easy as might encounter Make/Break nodes, array builders + // if someone is willing put work to it, you're welcome to make a pull request + + UK2Node* LinkedK2Node = Cast(LinkedPin->GetOwningNode()); + if (LinkedK2Node && LinkedK2Node->IsNodePure()) + { + TMap PureNodePins; + GetValidInputPins(LinkedK2Node, PureNodePins); + + if (const UEdGraphPin* PureInputPin = FindPinMatchingToProperty(FlowNodeClass, Param, PureNodePins)) + { + if (PureInputPin->LinkedTo.Num() == 0) // nothing connected to pin, so user can set value directly on this pin + { + FString const PinValue = PureInputPin->GetDefaultAsString(); + uint8* Offset = Param->ContainerPtrToValuePtr(FlowGraphNode->GetFlowNode()); + Param->ImportText(*PinValue, Offset, PPF_Copy, nullptr, GLog); + + bPinFound = true; + } + } + } + + // there can be only single valid connection on input parameter pin + break; + } + } + + if (bPinFound) + { + break; + } + } + } + } + } + } + + // Flow Nodes with Context Pins needs to update related data and call OnReconstructionRequested.ExecuteIfBound() in order to fully construct a graph node + FlowGraphNode->GetFlowNode()->PostImport(); + + // connect new node to all already recreated nodes + for (const TPair& Connection : NodeImport.Incoming) + { + UEdGraphPin* ThisPin = nullptr; + for (UEdGraphPin* FlowInputPin : FlowGraphNode->InputPins) + { + if (FlowGraphNode->InputPins.Num() == 1 || Connection.Key == FlowInputPin->PinName) + { + ThisPin = FlowInputPin; + break; + } + } + if (ThisPin == nullptr) + { + continue; + } + + UEdGraphPin* ConnectedPin = nullptr; + if (UFlowGraphNode* ConnectedNode = TargetNodes.FindRef(Connection.Value.NodeGuid)) + { + for (UEdGraphPin* FlowOutputPin : ConnectedNode->OutputPins) + { + if (ConnectedNode->OutputPins.Num() == 1 || Connection.Value.PinName == FlowOutputPin->PinName + || (Connection.Value.PinName == UEdGraphSchema_K2::PN_Then && FlowOutputPin->PinName == FName("TRUE")) + || (Connection.Value.PinName == UEdGraphSchema_K2::PN_Else && FlowOutputPin->PinName == FName("FALSE"))) + { + ConnectedPin = FlowOutputPin; + break; + } + } + } + + // link the pin to existing node + if (ConnectedPin) + { + FlowAsset->GetGraph()->GetSchema()->TryCreateConnection(ThisPin, ConnectedPin); + } + } + for (const TPair& Connection : NodeImport.Outgoing) + { + UEdGraphPin* ThisPin = nullptr; + for (UEdGraphPin* FlowOutputPin : FlowGraphNode->OutputPins) + { + if (FlowGraphNode->OutputPins.Num() == 1 || Connection.Key == FlowOutputPin->PinName + || (Connection.Key == UEdGraphSchema_K2::PN_Then && FlowOutputPin->PinName == FName("TRUE")) + || (Connection.Key == UEdGraphSchema_K2::PN_Else && FlowOutputPin->PinName == FName("FALSE"))) + { + ThisPin = FlowOutputPin; + break; + } + } + if (ThisPin == nullptr) + { + continue; + } + + UEdGraphPin* ConnectedPin = nullptr; + if (UFlowGraphNode* ConnectedNode = TargetNodes.FindRef(Connection.Value.NodeGuid)) + { + for (UEdGraphPin* FlowInputPin : ConnectedNode->InputPins) + { + if (ConnectedNode->InputPins.Num() == 1 || Connection.Value.PinName == FlowInputPin->PinName) + { + ConnectedPin = FlowInputPin; + break; + } + } + } + + // link the pin to existing node + if (ConnectedPin) + { + FlowAsset->GetGraph()->GetSchema()->TryCreateConnection(ThisPin, ConnectedPin); + } + } +} + +void UFlowImportUtils::GetValidInputPins(const UEdGraphNode* GraphNode, TMap& Result) +{ + for (const UEdGraphPin* Pin : GraphNode->Pins) + { + if (Pin->Direction == EGPD_Input && !Pin->bHidden && !Pin->bOrphanedPin) + { + Result.Add(Pin->PinName, Pin); + } + } +} + +const UEdGraphPin* UFlowImportUtils::FindPinMatchingToProperty(UClass* FlowNodeClass, const FProperty* Property, const TMap Pins) +{ + const FName& PropertyAuthoredName = *Property->GetAuthoredName(); + + // if Pin Name is exactly the same as Flow Node property name + if (const UEdGraphPin* Pin = Pins.FindRef(PropertyAuthoredName)) + { + return Pin; + } + + // if not, check if appropriate Pin Mapping has been provided + if (const FBlueprintToFlowPinName* PinMapping = PinMappings.Find(FlowNodeClass)) + { + if (const FName* MappedPinName = PinMapping->NodePropertiesToFunctionPins.Find(PropertyAuthoredName)) + { + if (const UEdGraphPin* Pin = Pins.FindRef(*MappedPinName)) + { + return Pin; + } + } + } + + return nullptr; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowMessageLogListing.cpp b/Source/FlowEditor/Private/Asset/FlowMessageLogListing.cpp new file mode 100644 index 000000000..9bd7a6f73 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowMessageLogListing.cpp @@ -0,0 +1,71 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowMessageLogListing.h" + +#include "MessageLogModule.h" + +#define LOCTEXT_NAMESPACE "FlowMessageLogListing" + +FFlowMessageLogListing::FFlowMessageLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type) + : Log(RegisterLogListing(InFlowAsset, Type)) +{ +} + +FFlowMessageLogListing::~FFlowMessageLogListing() +{ + // Unregister the log so it will be ref-counted to zero if it has no messages + if (Log->NumMessages(EMessageSeverity::Info) == 0) + { + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); + MessageLogModule.UnregisterLogListing(Log->GetName()); + } +} + +TSharedRef FFlowMessageLogListing::RegisterLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type) +{ + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); + + const FName LogName = GetListingName(InFlowAsset, Type); + + // Register the log (this will return an existing log if it has been used before) + FMessageLogInitializationOptions LogInitOptions; + LogInitOptions.bShowInLogWindow = false; + MessageLogModule.RegisterLogListing(LogName, LOCTEXT("FlowGraphLogLabel", "FlowGraph"), LogInitOptions); + return MessageLogModule.GetLogListing(LogName); +} + +TSharedRef FFlowMessageLogListing::GetLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type) +{ + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); + const FName LogName = GetListingName(InFlowAsset, Type); + + // Create a new message log + if (!MessageLogModule.IsRegisteredLogListing(LogName)) + { + MessageLogModule.RegisterLogListing(LogName, FText::FromString(GetLogLabel(Type))); + } + + return MessageLogModule.GetLogListing(LogName); +} + +FString FFlowMessageLogListing::GetLogLabel(const EFlowLogType Type) +{ + const FString TypeAsString = StaticEnum()->GetNameStringByIndex(static_cast(Type)); + return FString::Printf(TEXT("Flow%sLog"), *TypeAsString); +} + +FName FFlowMessageLogListing::GetListingName(const UFlowAsset* InFlowAsset, const EFlowLogType Type) +{ + FName LogListingName; + if (InFlowAsset) + { + LogListingName = *FString::Printf(TEXT("%s::%s::%s"), *GetLogLabel(Type), *InFlowAsset->GetName(), *InFlowAsset->AssetGuid.ToString()); + } + else + { + LogListingName = "FlowGraph"; + } + return LogListingName; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/SAssetRevisionMenu.cpp b/Source/FlowEditor/Private/Asset/SAssetRevisionMenu.cpp new file mode 100644 index 000000000..60c912488 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/SAssetRevisionMenu.cpp @@ -0,0 +1,236 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/SAssetRevisionMenu.h" + +#include "IAssetTypeActions.h" +#include "ISourceControlModule.h" +#include "ISourceControlRevision.h" +#include "SourceControlOperations.h" +#include "Widgets/Images/SThrobber.h" + +#define LOCTEXT_NAMESPACE "SFlowRevisionMenu" + +/** */ +namespace ESourceControlQueryState +{ + enum Type + { + NotQueried, + QueryInProgress, + Queried, + }; +} + +//------------------------------------------------------------------------------ +SAssetRevisionMenu::~SAssetRevisionMenu() +{ + // cancel any operation if this widget is destroyed while in progress + if (SourceControlQueryState == ESourceControlQueryState::QueryInProgress) + { + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + if (SourceControlQueryOp.IsValid() && SourceControlProvider.CanCancelOperation(SourceControlQueryOp.ToSharedRef())) + { + SourceControlProvider.CancelOperation(SourceControlQueryOp.ToSharedRef()); + } + } +} + +//------------------------------------------------------------------------------ +void SAssetRevisionMenu::Construct(const FArguments& InArgs, const FString& InFilename) +{ + bIncludeLocalRevision = InArgs._bIncludeLocalRevision; + OnRevisionSelected = InArgs._OnRevisionSelected; + + SourceControlQueryState = ESourceControlQueryState::NotQueried; + + ChildSlot + [ + SAssignNew(MenuBox, SVerticalBox) + + SVerticalBox::Slot() + [ + SNew(SBorder) + .Visibility(this, &SAssetRevisionMenu::GetInProgressVisibility) + .BorderImage(FEditorStyle::GetBrush("Menu.Background")) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SThrobber) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(2.0f, 0.0f, 4.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("DiffMenuOperationInProgress", "Updating history...")) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .Visibility(this, &SAssetRevisionMenu::GetCancelButtonVisibility) + .OnClicked(this, &SAssetRevisionMenu::OnCancelButtonClicked) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("DiffMenuCancelButton", "Cancel")) + ] + ] + ] + ] + ]; + + Filename = InFilename; + if (!Filename.IsEmpty()) + { + // make sure the history info is up to date + SourceControlQueryOp = ISourceControlOperation::Create(); + SourceControlQueryOp->SetUpdateHistory(true); + ISourceControlModule::Get().GetProvider().Execute(SourceControlQueryOp.ToSharedRef(), Filename, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SAssetRevisionMenu::OnSourceControlQueryComplete)); + + SourceControlQueryState = ESourceControlQueryState::QueryInProgress; + } +} + +//------------------------------------------------------------------------------ +EVisibility SAssetRevisionMenu::GetInProgressVisibility() const +{ + return (SourceControlQueryState == ESourceControlQueryState::QueryInProgress) ? EVisibility::Visible : EVisibility::Collapsed; +} + +//------------------------------------------------------------------------------ +EVisibility SAssetRevisionMenu::GetCancelButtonVisibility() const +{ + const ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + return SourceControlQueryOp.IsValid() && SourceControlProvider.CanCancelOperation(SourceControlQueryOp.ToSharedRef()) ? EVisibility::Visible : EVisibility::Collapsed; +} + +//------------------------------------------------------------------------------ +FReply SAssetRevisionMenu::OnCancelButtonClicked() const +{ + if (SourceControlQueryOp.IsValid()) + { + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + SourceControlProvider.CancelOperation(SourceControlQueryOp.ToSharedRef()); + } + + return FReply::Handled(); +} + +//------------------------------------------------------------------------------ +void SAssetRevisionMenu::OnSourceControlQueryComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) +{ + check(SourceControlQueryOp == InOperation); + + + // Add pop-out menu for each revision + FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection =*/true, /*InCommandList =*/nullptr); + + MenuBuilder.BeginSection("AddDiffRevision", LOCTEXT("Revisions", "Revisions")); + if (bIncludeLocalRevision) + { + FText const ToolTipText = LOCTEXT("LocalRevisionToolTip", "The current copy you have saved to disk (locally)"); + + FOnRevisionSelected OnRevisionSelectedDelegate = OnRevisionSelected; + auto OnMenuItemSelected = [OnRevisionSelectedDelegate, this]() + { + OnRevisionSelectedDelegate.ExecuteIfBound(FRevisionInfo::InvalidRevision(), Filename); + }; + + MenuBuilder.AddMenuEntry(LOCTEXT("LocalRevision", "Local"), ToolTipText, FSlateIcon(), FUIAction(FExecuteAction::CreateLambda(OnMenuItemSelected))); + } + + if (InResult == ECommandResult::Succeeded) + { + // get the cached state + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Filename, EStateCacheUsage::Use); + + if (SourceControlState.IsValid() && SourceControlState->GetHistorySize() > 0) + { + // Figure out the highest revision # (so we can label it "Depot") + int32 LatestRevision = 0; + for (int32 HistoryIndex = 0; HistoryIndex < SourceControlState->GetHistorySize(); HistoryIndex++) + { + TSharedPtr Revision = SourceControlState->GetHistoryItem(HistoryIndex); + if (Revision.IsValid() && Revision->GetRevisionNumber() > LatestRevision) + { + LatestRevision = Revision->GetRevisionNumber(); + } + } + + for (int32 HistoryIndex = 0; HistoryIndex < SourceControlState->GetHistorySize(); HistoryIndex++) + { + TSharedPtr Revision = SourceControlState->GetHistoryItem(HistoryIndex); + if (Revision.IsValid()) + { + FInternationalization& I18N = FInternationalization::Get(); + + FText Label = FText::Format(LOCTEXT("RevisionNumber", "Revision {0}"), FText::AsNumber(Revision->GetRevisionNumber(), nullptr, I18N.GetInvariantCulture())); + + FFormatNamedArguments Args; + Args.Add(TEXT("CheckInNumber"), FText::AsNumber(Revision->GetCheckInIdentifier(), nullptr, I18N.GetInvariantCulture())); + Args.Add(TEXT("Revision"), FText::FromString(Revision->GetRevision())); + Args.Add(TEXT("UserName"), FText::FromString(Revision->GetUserName())); + Args.Add(TEXT("DateTime"), FText::AsDate(Revision->GetDate())); + Args.Add(TEXT("ChanglistDescription"), FText::FromString(Revision->GetDescription())); + FText ToolTipText; + if (ISourceControlModule::Get().GetProvider().UsesChangelists()) + { + ToolTipText = FText::Format(LOCTEXT("ChangelistToolTip", "CL #{CheckInNumber} {UserName} \n{DateTime} \n{ChanglistDescription}"), Args); + } + else + { + ToolTipText = FText::Format(LOCTEXT("RevisionToolTip", "{Revision} {UserName} \n{DateTime} \n{ChanglistDescription}"), Args); + } + + if (LatestRevision == Revision->GetRevisionNumber()) + { + Label = LOCTEXT("Depo", "Depot"); + } + + FRevisionInfo RevisionInfo = { + Revision->GetRevision(), + Revision->GetCheckInIdentifier(), + Revision->GetDate() + }; + FOnRevisionSelected OnRevisionSelectedDelegate = OnRevisionSelected; + auto OnMenuItemSelected = [RevisionInfo, OnRevisionSelectedDelegate, this]() + { + OnRevisionSelectedDelegate.ExecuteIfBound(RevisionInfo, Filename); + }; + MenuBuilder.AddMenuEntry(TAttribute(Label), ToolTipText, FSlateIcon(), FUIAction(FExecuteAction::CreateLambda(OnMenuItemSelected))); + } + } + } + else if (!bIncludeLocalRevision) + { + // Show 'empty' item in toolbar + MenuBuilder.AddMenuEntry(LOCTEXT("NoRevisonHistory", "No revisions found"), FText(), FSlateIcon(), FUIAction()); + } + } + else if (!bIncludeLocalRevision) + { + // Show 'empty' item in toolbar + MenuBuilder.AddMenuEntry(LOCTEXT("NoRevisonHistory", "No revisions found"), FText(), FSlateIcon(), FUIAction()); + } + + MenuBuilder.EndSection(); + MenuBox->AddSlot() + [ + MenuBuilder.MakeWidget(nullptr, 500) + ]; + + SourceControlQueryOp.Reset(); + SourceControlQueryState = ESourceControlQueryState::Queried; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/SFlowDiff.cpp b/Source/FlowEditor/Private/Asset/SFlowDiff.cpp new file mode 100644 index 000000000..d02bebca3 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/SFlowDiff.cpp @@ -0,0 +1,867 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Visual-Diff + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9659 + */ + +#include "Asset/SFlowDiff.h" + +#if ENABLE_FLOW_DIFF +#include "Asset/FlowDiffControl.h" + +#include "FlowAsset.h" + +#include "EdGraphUtilities.h" +#include "Editor.h" +#include "Framework/Commands/GenericCommands.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/MultiBox/MultiBoxDefs.h" +#include "GraphDiffControl.h" +#include "HAL/PlatformApplicationMisc.h" +#include "Internationalization/Text.h" +#include "SBlueprintDiff.h" +#include "SlateOptMacros.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "Widgets/Layout/SSpacer.h" + +#define LOCTEXT_NAMESPACE "SFlowDiff" + +static const FName DetailsMode = FName(TEXT("DetailsMode")); +static const FName GraphMode = FName(TEXT("GraphMode")); + +FFlowDiffPanel::FFlowDiffPanel() + : FlowAsset(nullptr) + , bShowAssetName(false) +{ +} + +static int32 GetCurrentIndex(SListView> const& ListView, const TArray>& ListViewSource) +{ + const TArray>& Selected = ListView.GetSelectedItems(); + if (Selected.Num() == 1) + { + for (const TSharedPtr& Diff : ListViewSource) + { + if (Diff == Selected[0]) + { + return 0; + } + } + } + return -1; +} + +void FlowDiffUtils::SelectNextRow(SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + if (CurrentIndex == ListViewSource.Num() - 1) + { + return; + } + + ListView.SetSelection(ListViewSource[CurrentIndex + 1]); +} + +void FlowDiffUtils::SelectPrevRow(SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + if (CurrentIndex == 0) + { + return; + } + + ListView.SetSelection(ListViewSource[CurrentIndex - 1]); +} + +bool FlowDiffUtils::HasNextDifference(const SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + return ListViewSource.IsValidIndex(CurrentIndex + 1); +} + +bool FlowDiffUtils::HasPrevDifference(const SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + return ListViewSource.IsValidIndex(CurrentIndex - 1); +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void SFlowDiff::Construct(const FArguments& InArgs) +{ + check(InArgs._OldFlow && InArgs._NewFlow); + PanelOld.FlowAsset = InArgs._OldFlow; + PanelNew.FlowAsset = InArgs._NewFlow; + PanelOld.RevisionInfo = InArgs._OldRevision; + PanelNew.RevisionInfo = InArgs._NewRevision; + + // sometimes we want to clearly identify the assets being diffed (when it's + // not the same asset in each panel) + PanelOld.bShowAssetName = InArgs._ShowAssetNames; + PanelNew.bShowAssetName = InArgs._ShowAssetNames; + + bLockViews = true; + + if (InArgs._ParentWindow.IsValid()) + { + WeakParentWindow = InArgs._ParentWindow; + + AssetEditorCloseDelegate = GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().AddSP(this, &SFlowDiff::OnCloseAssetEditor); + } + + FToolBarBuilder NavToolBarBuilder(TSharedPtr(), FMultiBoxCustomization::None); + NavToolBarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateSP(this, &SFlowDiff::PrevDiff), + FCanExecuteAction::CreateSP(this, &SFlowDiff::HasPrevDiff) + ) + , NAME_None + , LOCTEXT("PrevDiffLabel", "Prev") + , LOCTEXT("PrevDiffTooltip", "Go to previous difference") + , FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.PrevDiff") + ); + NavToolBarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateSP(this, &SFlowDiff::NextDiff), + FCanExecuteAction::CreateSP(this, &SFlowDiff::HasNextDiff) + ) + , NAME_None + , LOCTEXT("NextDiffLabel", "Next") + , LOCTEXT("NextDiffTooltip", "Go to next difference") + , FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.NextDiff") + ); + + FToolBarBuilder GraphToolbarBuilder(TSharedPtr(), FMultiBoxCustomization::None); + GraphToolbarBuilder.AddToolBarButton( + FUIAction(FExecuteAction::CreateSP(this, &SFlowDiff::OnToggleLockView)) + , NAME_None + , LOCTEXT("LockGraphsLabel", "Lock/Unlock") + , LOCTEXT("LockGraphsTooltip", "Force all graph views to change together, or allow independent scrolling/zooming") + , TAttribute(this, &SFlowDiff::GetLockViewImage) + ); + GraphToolbarBuilder.AddToolBarButton( + FUIAction(FExecuteAction::CreateSP(this, &SFlowDiff::OnToggleSplitViewMode)) + , NAME_None + , LOCTEXT("SplitGraphsModeLabel", "Vertical/Horizontal") + , LOCTEXT("SplitGraphsModeLabelTooltip", "Toggles the split view of graphs between vertical and horizontal") + , TAttribute(this, &SFlowDiff::GetSplitViewModeImage) + ); + + DifferencesTreeView = DiffTreeView::CreateTreeView(&PrimaryDifferencesList); + + GenerateDifferencesList(); + + const auto TextBlock = [](FText Text) -> TSharedRef + { + return SNew(SBox) + .Padding(FMargin(4.0f, 10.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Visibility(EVisibility::HitTestInvisible) + .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") + .Text(Text) + ]; + }; + + TopRevisionInfoWidget = + SNew(SSplitter) + .Visibility(EVisibility::HitTestInvisible) + + SSplitter::Slot() + .Value(.2f) + [ + SNew(SBox) + ] + + SSplitter::Slot() + .Value(.8f) + [ + SNew(SSplitter) + .PhysicalSplitterHandleSize(10.0f) + + SSplitter::Slot() + .Value(.5f) + [ + TextBlock(DiffViewUtils::GetPanelLabel(PanelOld.FlowAsset, PanelOld.RevisionInfo, FText())) + ] + + SSplitter::Slot() + .Value(.5f) + [ + TextBlock(DiffViewUtils::GetPanelLabel(PanelNew.FlowAsset, PanelNew.RevisionInfo, FText())) + ] + ]; + + GraphToolBarWidget = + SNew(SSplitter) + .Visibility(EVisibility::HitTestInvisible) + + SSplitter::Slot() + .Value(.2f) + [ + SNew(SBox) + ] + + SSplitter::Slot() + .Value(.8f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + GraphToolbarBuilder.MakeWidget() + ] + ]; + + this->ChildSlot + [ + SNew(SBorder) + .BorderImage(FEditorStyle::GetBrush("Docking.Tab", ".ContentAreaBrush")) + [ + SNew(SOverlay) + + SOverlay::Slot() + .VAlign(VAlign_Top) + [ + TopRevisionInfoWidget.ToSharedRef() + ] + + SOverlay::Slot() + .VAlign(VAlign_Top) + .Padding(0.0f, 6.0f, 0.0f, 4.0f) + [ + GraphToolBarWidget.ToSharedRef() + ] + + SOverlay::Slot() + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 2.0f, 0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(4.f) + .AutoWidth() + [ + NavToolBarBuilder.MakeWidget() + ] + + SHorizontalBox::Slot() + [ + SNew(SSpacer) + ] + ] + + SVerticalBox::Slot() + [ + SNew(SSplitter) + + SSplitter::Slot() + .Value(.2f) + [ + SNew(SBorder) + .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) + [ + DifferencesTreeView.ToSharedRef() + ] + ] + + SSplitter::Slot() + .Value(.8f) + [ + SAssignNew(ModeContents, SBox) + ] + ] + ] + ] + ]; + + SetCurrentMode(DetailsMode); +} + +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +SFlowDiff::~SFlowDiff() +{ + if (AssetEditorCloseDelegate.IsValid()) + { + GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().Remove(AssetEditorCloseDelegate); + } +} + +void SFlowDiff::OnCloseAssetEditor(UObject* Asset, const EAssetEditorCloseReason CloseReason) +{ + if (PanelOld.FlowAsset == Asset || PanelNew.FlowAsset == Asset || CloseReason == EAssetEditorCloseReason::CloseAllAssetEditors) + { + // Tell our window to close and set our selves to collapsed to try and stop it from ticking + SetVisibility(EVisibility::Collapsed); + + if (AssetEditorCloseDelegate.IsValid()) + { + GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().Remove(AssetEditorCloseDelegate); + } + + if (WeakParentWindow.IsValid()) + { + WeakParentWindow.Pin()->RequestDestroyWindow(); + } + } +} + +void SFlowDiff::OnGraphSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectionType) +{ + if (!Item.IsValid()) + { + return; + } + + FocusOnGraphRevisions(Item.Get()); +} + +void SFlowDiff::OnGraphChanged(const FFlowGraphToDiff* Diff) +{ + if (PanelNew.GraphEditor.IsValid() && PanelNew.GraphEditor.Pin()->GetCurrentGraph() == Diff->GetGraphNew()) + { + FocusOnGraphRevisions(Diff); + } +} + +TSharedRef SFlowDiff::DefaultEmptyPanel() +{ + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("BlueprintDifGraphsToolTip", "Select Graph to Diff")) + ]; +} + +TSharedPtr SFlowDiff::CreateDiffWindow(const FText WindowTitle, const UFlowAsset* OldFlow, const UFlowAsset* NewFlow, const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision) +{ + // sometimes we're comparing different revisions of one single asset (other + // times we're comparing two completely separate assets altogether) + const bool bIsSingleAsset = (NewFlow->GetName() == OldFlow->GetName()); + + TSharedPtr Window = SNew(SWindow) + .Title(WindowTitle) + .ClientSize(FVector2D(1000, 800)); + + Window->SetContent(SNew(SFlowDiff) + .OldFlow(OldFlow) + .NewFlow(NewFlow) + .OldRevision(OldRevision) + .NewRevision(NewRevision) + .ShowAssetNames(!bIsSingleAsset) + .ParentWindow(Window)); + + // Make this window a child of the modal window if we've been spawned while one is active. + const TSharedPtr ActiveModal = FSlateApplication::Get().GetActiveModalWindow(); + if (ActiveModal.IsValid()) + { + FSlateApplication::Get().AddWindowAsNativeChild(Window.ToSharedRef(), ActiveModal.ToSharedRef()); + } + else + { + FSlateApplication::Get().AddWindow(Window.ToSharedRef()); + } + + return Window; +} + +void SFlowDiff::NextDiff() const +{ + DiffTreeView::HighlightNextDifference(DifferencesTreeView.ToSharedRef(), RealDifferences, PrimaryDifferencesList); +} + +void SFlowDiff::PrevDiff() const +{ + DiffTreeView::HighlightPrevDifference(DifferencesTreeView.ToSharedRef(), RealDifferences, PrimaryDifferencesList); +} + +bool SFlowDiff::HasNextDiff() const +{ + return DiffTreeView::HasNextDifference(DifferencesTreeView.ToSharedRef(), RealDifferences); +} + +bool SFlowDiff::HasPrevDiff() const +{ + return DiffTreeView::HasPrevDifference(DifferencesTreeView.ToSharedRef(), RealDifferences); +} + +FFlowGraphToDiff* SFlowDiff::FindGraphToDiffEntry(const FString& GraphPath) const +{ + const FString SearchGraphPath = GraphToDiff->GetGraphOld() ? FGraphDiffControl::GetGraphPath(GraphToDiff->GetGraphOld()) : FGraphDiffControl::GetGraphPath(GraphToDiff->GetGraphNew()); + if (SearchGraphPath.Equals(GraphPath, ESearchCase::CaseSensitive)) + { + return GraphToDiff.Get(); + } + + return nullptr; +} + +void SFlowDiff::FocusOnGraphRevisions(const FFlowGraphToDiff* Diff) +{ + UEdGraph* Graph = Diff->GetGraphOld() ? Diff->GetGraphOld() : Diff->GetGraphNew(); + + const FString GraphPath = FGraphDiffControl::GetGraphPath(Graph); + HandleGraphChanged(GraphPath); + + ResetGraphEditors(); +} + +void SFlowDiff::OnDiffListSelectionChanged(TSharedPtr TheDiff) +{ + check(!TheDiff->Result.OwningObjectPath.IsEmpty()); + FocusOnGraphRevisions(FindGraphToDiffEntry(TheDiff->Result.OwningObjectPath)); + const FDiffSingleResult Result = TheDiff->Result; + + const auto SafeClearSelection = [](TWeakPtr GraphEditor) + { + const TSharedPtr GraphEditorPtr = GraphEditor.Pin(); + if (GraphEditorPtr.IsValid()) + { + GraphEditorPtr->ClearSelectionSet(); + } + }; + + SafeClearSelection(PanelNew.GraphEditor); + SafeClearSelection(PanelOld.GraphEditor); + + if (Result.Pin1) + { + GetDiffPanelForNode(*Result.Pin1->GetOwningNode()).FocusDiff(*Result.Pin1); + if (Result.Pin2) + { + GetDiffPanelForNode(*Result.Pin2->GetOwningNode()).FocusDiff(*Result.Pin2); + } + } + else if (Result.Node1) + { + GetDiffPanelForNode(*Result.Node1).FocusDiff(*Result.Node1); + if (Result.Node2) + { + GetDiffPanelForNode(*Result.Node2).FocusDiff(*Result.Node2); + } + } +} + +void SFlowDiff::OnToggleLockView() +{ + bLockViews = !bLockViews; + ResetGraphEditors(); +} + +void SFlowDiff::OnToggleSplitViewMode() +{ + bVerticalSplitGraphMode = !bVerticalSplitGraphMode; + + if (SSplitter* DiffGraphSplitterPtr = DiffGraphSplitter.Get()) + { + DiffGraphSplitterPtr->SetOrientation(bVerticalSplitGraphMode ? Orient_Horizontal : Orient_Vertical); + } +} + +FSlateIcon SFlowDiff::GetLockViewImage() const +{ + return FSlateIcon(FAppStyle::GetAppStyleSetName(), bLockViews ? "Icons.Lock" : "Icons.Unlock"); +} + +FSlateIcon SFlowDiff::GetSplitViewModeImage() const +{ + return FSlateIcon(FAppStyle::GetAppStyleSetName(), bVerticalSplitGraphMode ? "BlueprintDif.VerticalDiff.Small" : "BlueprintDif.HorizontalDiff.Small"); +} + +void SFlowDiff::ResetGraphEditors() const +{ + if (PanelOld.GraphEditor.IsValid() && PanelNew.GraphEditor.IsValid()) + { + if (bLockViews) + { + PanelOld.GraphEditor.Pin()->LockToGraphEditor(PanelNew.GraphEditor); + PanelNew.GraphEditor.Pin()->LockToGraphEditor(PanelOld.GraphEditor); + } + else + { + PanelOld.GraphEditor.Pin()->UnlockFromGraphEditor(PanelNew.GraphEditor); + PanelNew.GraphEditor.Pin()->UnlockFromGraphEditor(PanelOld.GraphEditor); + } + } +} + +void FFlowDiffPanel::GeneratePanel(UEdGraph* NewGraph, UEdGraph* OldGraph) +{ + const TSharedPtr> Diff = MakeShared>(); + FGraphDiffControl::DiffGraphs(OldGraph, NewGraph, *Diff); + GeneratePanel(NewGraph, Diff, {}); +} + +void FFlowDiffPanel::GeneratePanel(UEdGraph* Graph, TSharedPtr> DiffResults, TAttribute FocusedDiffResult) +{ + if (GraphEditor.IsValid() && GraphEditor.Pin()->GetCurrentGraph() == Graph) + { + return; + } + + TSharedPtr Widget = SNew(SBorder) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock).Text(LOCTEXT("FlowDiffPanelNoGraphTip", "Graph does not exist in this revision")) + ]; + + if (Graph) + { + SGraphEditor::FGraphEditorEvents InEvents; + { + const auto SelectionChangedHandler = [](const FGraphPanelSelectionSet& SelectionSet, TSharedPtr Container) + { + Container->SetObjects(SelectionSet.Array()); + }; + + const auto ContextMenuHandler = [](UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) + { + MenuBuilder->AddMenuEntry(FGenericCommands::Get().Copy); + return FActionMenuContent(MenuBuilder->MakeWidget()); + }; + + InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateStatic(SelectionChangedHandler, DetailsView); + InEvents.OnCreateNodeOrPinMenu = SGraphEditor::FOnCreateNodeOrPinMenu::CreateStatic(ContextMenuHandler); + } + + if (!GraphEditorCommands.IsValid()) + { + GraphEditorCommands = MakeShared(); + + GraphEditorCommands->MapAction(FGenericCommands::Get().Copy, + FExecuteAction::CreateRaw(this, &FFlowDiffPanel::CopySelectedNodes), + FCanExecuteAction::CreateRaw(this, &FFlowDiffPanel::CanCopyNodes) + ); + } + + const TSharedRef Editor = SNew(SGraphEditor) + .AdditionalCommands(GraphEditorCommands) + .GraphToEdit(Graph) + .GraphToDiff(nullptr) + //.DiffResults(DiffResults) + //.FocusedDiffResult(FocusedDiffResult) + .IsEditable(false) + .GraphEvents(InEvents); + + GraphEditor = Editor; + Widget = Editor; + } + + GraphEditorBox->SetContent(Widget.ToSharedRef()); +} + +FGraphPanelSelectionSet FFlowDiffPanel::GetSelectedNodes() const +{ + FGraphPanelSelectionSet CurrentSelection; + const TSharedPtr FocusedGraphEd = GraphEditor.Pin(); + if (FocusedGraphEd.IsValid()) + { + CurrentSelection = FocusedGraphEd->GetSelectedNodes(); + } + return CurrentSelection; +} + +void FFlowDiffPanel::CopySelectedNodes() const +{ + // Export the selected nodes and place the text on the clipboard + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + + FString ExportedText; + FEdGraphUtilities::ExportNodesToText(SelectedNodes, /*out*/ ExportedText); + FPlatformApplicationMisc::ClipboardCopy(*ExportedText); +} + +bool FFlowDiffPanel::CanCopyNodes() const +{ + // If any of the nodes can be duplicated then we should allow copying + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) + { + const UEdGraphNode* Node = Cast(*SelectedIter); + if ((Node != nullptr) && Node->CanDuplicateNode()) + { + return true; + } + } + return false; +} + +void FFlowDiffPanel::FocusDiff(const UEdGraphPin& Pin) const +{ + GraphEditor.Pin()->JumpToPin(&Pin); +} + +void FFlowDiffPanel::FocusDiff(const UEdGraphNode& Node) const +{ + if (GraphEditor.IsValid()) + { + GraphEditor.Pin()->JumpToNode(&Node, false); + } +} + +FFlowDiffPanel& SFlowDiff::GetDiffPanelForNode(const UEdGraphNode& Node) +{ + const TSharedPtr OldGraphEditorPtr = PanelOld.GraphEditor.Pin(); + if (OldGraphEditorPtr.IsValid() && Node.GetGraph() == OldGraphEditorPtr->GetCurrentGraph()) + { + return PanelOld; + } + + const TSharedPtr NewGraphEditorPtr = PanelNew.GraphEditor.Pin(); + if (NewGraphEditorPtr.IsValid() && Node.GetGraph() == NewGraphEditorPtr->GetCurrentGraph()) + { + return PanelNew; + } + + ensureMsgf(false, TEXT("Looking for node %s but it cannot be found in provided panels"), *Node.GetName()); + static FFlowDiffPanel Default; + return Default; +} + +void SFlowDiff::HandleGraphChanged(const FString& GraphPath) +{ + SetCurrentMode(GraphMode); + + UEdGraph* GraphOld = nullptr; + UEdGraph* GraphNew = nullptr; + TSharedPtr> DiffResults; + int32 RealDifferencesStartIndex = INDEX_NONE; + { + UEdGraph* NewGraph = GraphToDiff->GetGraphNew(); + UEdGraph* OldGraph = GraphToDiff->GetGraphOld(); + const FString OtherGraphPath = NewGraph ? FGraphDiffControl::GetGraphPath(NewGraph) : FGraphDiffControl::GetGraphPath(OldGraph); + if (GraphPath.Equals(OtherGraphPath)) + { + GraphNew = NewGraph; + GraphOld = OldGraph; + DiffResults = GraphToDiff->FoundDiffs; + RealDifferencesStartIndex = GraphToDiff->RealDifferencesStartIndex; + } + } + + const TAttribute FocusedDiffResult = TAttribute::CreateLambda( + [this, RealDifferencesStartIndex]() + { + int32 FocusedDiffResult = INDEX_NONE; + if (RealDifferencesStartIndex != INDEX_NONE) + { + FocusedDiffResult = DiffTreeView::CurrentDifference(DifferencesTreeView.ToSharedRef(), RealDifferences) - RealDifferencesStartIndex; + } + + // find selected index in all the graphs, and subtract the index of the first entry in this graph + return FocusedDiffResult; + }); + + // only regenerate PanelOld if the old graph has changed + if (!PanelOld.GraphEditor.IsValid() || GraphOld != PanelOld.GraphEditor.Pin()->GetCurrentGraph()) + { + PanelOld.GeneratePanel(GraphOld, DiffResults, FocusedDiffResult); + } + + // only regenerate PanelNew if the old graph has changed + if (!PanelNew.GraphEditor.IsValid() || GraphNew != PanelNew.GraphEditor.Pin()->GetCurrentGraph()) + { + PanelNew.GeneratePanel(GraphNew, DiffResults, FocusedDiffResult); + } +} + +void SFlowDiff::GenerateDifferencesList() +{ + PrimaryDifferencesList.Empty(); + RealDifferences.Empty(); + ModePanels.Empty(); + + const auto CreateInspector = [](const UObject* Object) + { + FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked("PropertyEditor"); + + FNotifyHook* NotifyHook = nullptr; + + FDetailsViewArgs DetailsViewArgs; + DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; + DetailsViewArgs.bHideSelectionTip = true; + DetailsViewArgs.NotifyHook = NotifyHook; + DetailsViewArgs.ViewIdentifier = FName("ObjectInspector"); + TSharedRef DetailsView = EditModule.CreateDetailView(DetailsViewArgs); + DetailsView->SetObject(const_cast(Object)); + + return DetailsView; + }; + + // TODO: construct DetailsView of PanelOld and PanelNew + PanelOld.DetailsView = CreateInspector(PanelOld.FlowAsset); + PanelNew.DetailsView = CreateInspector(PanelOld.FlowAsset); + + // Now that we have done the diffs, create the panel widgets + ModePanels.Add(DetailsMode, GenerateDetailsPanel()); + ModePanels.Add(GraphMode, GenerateGraphPanel()); + + DifferencesTreeView->RebuildList(); +} + +SFlowDiff::FDiffControl SFlowDiff::GenerateDetailsPanel() +{ + const TSharedPtr NewDiffControl = MakeShared(PanelOld.FlowAsset, PanelNew.FlowAsset, FOnDiffEntryFocused::CreateRaw(this, &SFlowDiff::SetCurrentMode, DetailsMode)); + NewDiffControl->GenerateTreeEntries(PrimaryDifferencesList, RealDifferences); + + SFlowDiff::FDiffControl Ret; + Ret.DiffControl = NewDiffControl; + Ret.Widget = SNew(SSplitter) + .PhysicalSplitterHandleSize(10.0f) + + SSplitter::Slot() + .Value(0.5f) + [ + NewDiffControl->OldDetailsWidget() + ] + + SSplitter::Slot() + .Value(0.5f) + [ + NewDiffControl->NewDetailsWidget() + ]; + + return Ret; +} + +SFlowDiff::FDiffControl SFlowDiff::GenerateGraphPanel() +{ + // We only have a single permanent graph in Flow Asset + GraphToDiff = MakeShared(this, PanelOld.FlowAsset->GetGraph(), PanelNew.FlowAsset->GetGraph(), PanelOld.RevisionInfo, PanelNew.RevisionInfo); + GraphToDiff->GenerateTreeEntries(PrimaryDifferencesList, RealDifferences); + + FDiffControl Ret; + + Ret.Widget = SNew(SVerticalBox) + + SVerticalBox::Slot() + .FillHeight(1.f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.f) + [ + //diff window + SNew(SSplitter) + .Orientation(Orient_Vertical) + + SSplitter::Slot() + .Value(.8f) + [ + SAssignNew(DiffGraphSplitter, SSplitter) + .PhysicalSplitterHandleSize(10.0f) + .Orientation(bVerticalSplitGraphMode ? Orient_Horizontal : Orient_Vertical) + + SSplitter::Slot() // Old revision graph slot + [ + GenerateGraphWidgetForPanel(PanelOld) + ] + + SSplitter::Slot() // New revision graph slot + [ + GenerateGraphWidgetForPanel(PanelNew) + ] + ] + + SSplitter::Slot() + .Value(.2f) + [ + SNew(SSplitter) + .PhysicalSplitterHandleSize(10.0f) + + SSplitter::Slot() + [ + PanelOld.DetailsView.ToSharedRef() + ] + + SSplitter::Slot() + [ + PanelNew.DetailsView.ToSharedRef() + ] + ] + ] + ]; + + return Ret; +} + +TSharedRef SFlowDiff::GenerateGraphWidgetForPanel(FFlowDiffPanel& OutDiffPanel) const +{ + return SNew(SOverlay) + + SOverlay::Slot() // Graph slot + [ + SAssignNew(OutDiffPanel.GraphEditorBox, SBox) + .HAlign(HAlign_Fill) + [ + DefaultEmptyPanel() + ] + ] + + SOverlay::Slot() // Revision info slot + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .Padding(FMargin(20.0f, 10.0f)) + [ + GenerateRevisionInfoWidgetForPanel(OutDiffPanel.OverlayGraphRevisionInfo, DiffViewUtils::GetPanelLabel(OutDiffPanel.FlowAsset, OutDiffPanel.RevisionInfo, FText())) + ]; +} + +TSharedRef SFlowDiff::GenerateRevisionInfoWidgetForPanel(TSharedPtr& OutGeneratedWidget, const FText& InRevisionText) const +{ + return SAssignNew(OutGeneratedWidget, SBox) + .Padding(FMargin(4.0f, 10.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") + .Text(InRevisionText) + .ShadowColorAndOpacity(FColor::Black) + .ShadowOffset(FVector2D(1.4, 1.4)) + ]; +} + +void SFlowDiff::SetCurrentMode(FName NewMode) +{ + if (CurrentMode == NewMode) + { + return; + } + + CurrentMode = NewMode; + + const FDiffControl* FoundControl = ModePanels.Find(NewMode); + + if (FoundControl) + { + // Reset inspector view + PanelOld.DetailsView->SetObjects(TArray()); + PanelNew.DetailsView->SetObjects(TArray()); + + ModeContents->SetContent(FoundControl->Widget.ToSharedRef()); + } + else + { + ensureMsgf(false, TEXT("Diff panel does not support mode %s"), *NewMode.ToString()); + } + + OnModeChanged(NewMode); +} + +void SFlowDiff::UpdateTopSectionVisibility(const FName& InNewViewMode) const +{ + SSplitter* GraphToolBarPtr = GraphToolBarWidget.Get(); + SSplitter* TopRevisionInfoWidgetPtr = TopRevisionInfoWidget.Get(); + + if (!GraphToolBarPtr || !TopRevisionInfoWidgetPtr) + { + return; + } + + if (InNewViewMode == GraphMode) + { + GraphToolBarPtr->SetVisibility(EVisibility::Visible); + TopRevisionInfoWidgetPtr->SetVisibility(EVisibility::Collapsed); + } + else + { + GraphToolBarPtr->SetVisibility(EVisibility::Collapsed); + TopRevisionInfoWidgetPtr->SetVisibility(EVisibility::HitTestInvisible); + } +} + +void SFlowDiff::OnModeChanged(const FName& InNewViewMode) const +{ + UpdateTopSectionVisibility(InNewViewMode); +} + +#undef LOCTEXT_NAMESPACE +#endif diff --git a/Source/FlowEditor/Private/Asset/FlowAssetDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowAssetDetails.cpp similarity index 60% rename from Source/FlowEditor/Private/Asset/FlowAssetDetails.cpp rename to Source/FlowEditor/Private/DetailCustomizations/FlowAssetDetails.cpp index aa37b8293..41e6b8861 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetDetails.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowAssetDetails.cpp @@ -1,4 +1,6 @@ -#include "FlowAssetDetails.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowAssetDetails.h" #include "FlowAsset.h" #include "Nodes/Route/FlowNode_SubGraph.h" @@ -9,26 +11,22 @@ #define LOCTEXT_NAMESPACE "FlowAssetDetails" -void FFlowAssetDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +void FFlowAssetDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { - IDetailCategoryBuilder& FlowAssetCategory = DetailLayout.EditCategory("FlowAsset", LOCTEXT("FlowAssetCategory", "FlowAsset")); - - const TSharedPtr InputsPropertyHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs)); - if (InputsPropertyHandle.IsValid() && InputsPropertyHandle->AsArray().IsValid()) - { - TSharedRef InputsArrayBuilder = MakeShareable(new FDetailArrayBuilder(InputsPropertyHandle.ToSharedRef())); - InputsArrayBuilder->OnGenerateArrayElementWidget(FOnGenerateArrayElementWidget::CreateSP(this, &FFlowAssetDetails::GenerateCustomPinArray)); - - FlowAssetCategory.AddCustomBuilder(InputsArrayBuilder); - } + IDetailCategoryBuilder& FlowAssetCategory = DetailBuilder.EditCategory("SubGraph", LOCTEXT("SubGraphCategory", "Sub Graph")); - const TSharedPtr OutputsPropertyHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs)); - if (OutputsPropertyHandle.IsValid() && OutputsPropertyHandle->AsArray().IsValid()) + TArray> ArrayPropertyHandles; + ArrayPropertyHandles.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs))); + ArrayPropertyHandles.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs))); + for (const TSharedPtr& PropertyHandle : ArrayPropertyHandles) { - TSharedRef OutputsArrayBuilder = MakeShareable(new FDetailArrayBuilder(OutputsPropertyHandle.ToSharedRef())); - OutputsArrayBuilder->OnGenerateArrayElementWidget(FOnGenerateArrayElementWidget::CreateSP(this, &FFlowAssetDetails::GenerateCustomPinArray)); + if (PropertyHandle.IsValid() && PropertyHandle->AsArray().IsValid()) + { + const TSharedRef ArrayBuilder = MakeShareable(new FDetailArrayBuilder(PropertyHandle.ToSharedRef())); + ArrayBuilder->OnGenerateArrayElementWidget(FOnGenerateArrayElementWidget::CreateSP(this, &FFlowAssetDetails::GenerateCustomPinArray)); - FlowAssetCategory.AddCustomBuilder(OutputsArrayBuilder); + FlowAssetCategory.AddCustomBuilder(ArrayBuilder); + } } } diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_ComponentObserverDetails.cpp similarity index 62% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.cpp rename to Source/FlowEditor/Private/DetailCustomizations/FlowNode_ComponentObserverDetails.cpp index 5ddf96576..297fb3afa 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_ComponentObserverDetails.cpp @@ -1,4 +1,6 @@ -#include "FlowNode_ComponentObserverDetails.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_ComponentObserverDetails.h" #include "Nodes/World/FlowNode_ComponentObserver.h" #include "DetailCategoryBuilder.h" @@ -8,4 +10,5 @@ void FFlowNode_ComponentObserverDetails::CustomizeDetails(IDetailLayoutBuilder& { IDetailCategoryBuilder& SequenceCategory = DetailBuilder.EditCategory("ObservedComponent"); SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_ComponentObserver, IdentityTags)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_ComponentObserver, IdentityMatchType)); } diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomEventBaseDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomEventBaseDetails.cpp new file mode 100644 index 000000000..3a8b96b99 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomEventBaseDetails.cpp @@ -0,0 +1,148 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_CustomEventBaseDetails.h" +#include "FlowAsset.h" +#include "Nodes/Route/FlowNode_CustomEventBase.h" + +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "PropertyEditing.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/SWidget.h" + + +void FFlowNode_CustomEventBaseDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // Subclasses must override this function (and call CustomizeDetailsInternal with the localized text) + checkNoEntry(); +} + +void FFlowNode_CustomEventBaseDetails::CustomizeDetailsInternal(IDetailLayoutBuilder& DetailLayout, const FText& CustomRowNameText, const FText& EventNameText) +{ + DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); + + if (ObjectsBeingEdited[0].IsValid()) + { + const UFlowNode_CustomEventBase* EventNode = CastChecked(ObjectsBeingEdited[0]); + CachedEventNameSelected = MakeShared(EventNode->GetEventName()); + } + + IDetailCategoryBuilder& Category = CreateDetailCategory(DetailLayout); + + Category.AddCustomRow(CustomRowNameText) + .NameContent() + [ + SNew(STextBlock) + .Text(EventNameText) + ] + .ValueContent() + .HAlign(HAlign_Fill) + [ + SAssignNew(EventTextListWidget, SComboBox>) + .OptionsSource(&EventNames) + .OnGenerateWidget(this, &FFlowNode_CustomEventBaseDetails::GenerateEventWidget) + .OnComboBoxOpening(this, &FFlowNode_CustomEventBaseDetails::OnComboBoxOpening) + .OnSelectionChanged(this, &FFlowNode_CustomEventBaseDetails::PinSelectionChanged) + [ + SNew(STextBlock) + .Text(this, &FFlowNode_CustomEventBaseDetails::GetSelectedEventText) + ] + ]; +} + +void FFlowNode_CustomEventBaseDetails::OnComboBoxOpening() +{ + RebuildEventNames(); +} + +void FFlowNode_CustomEventBaseDetails::RebuildEventNames() +{ + EventNames.Empty(); + + check(CachedEventNameSelected.IsValid()); + EventNames.Add(CachedEventNameSelected); + + if (ObjectsBeingEdited[0].IsValid() && ObjectsBeingEdited[0].Get()->GetOuter()) + { + const UFlowAsset* FlowAsset = CastChecked(ObjectsBeingEdited[0].Get()->GetOuter()); + TArray SortedNames = BuildEventNames(*FlowAsset); + + if (bExcludeReferencedEvents) + { + for (const TPair& Node : FlowAsset->GetNodes()) + { + if (Node.Value->GetClass()->IsChildOf(UFlowNode_CustomEventBase::StaticClass())) + { + SortedNames.Remove(Cast(Node.Value)->GetEventName()); + } + } + } + + SortedNames.Sort([](const FName& A, const FName& B) + { + return A.LexicalLess(B); + }); + + for (const FName& EventName : SortedNames) + { + const bool bIsCurrentSelection = (EventName == *CachedEventNameSelected); + if (!EventName.IsNone() && !bIsCurrentSelection) + { + EventNames.Add(MakeShared(EventName)); + } + } + } + + if (!IsInEventNames(NAME_None)) + { + EventNames.Add(MakeShared(NAME_None)); + } +} + +bool FFlowNode_CustomEventBaseDetails::IsInEventNames(const FName& EventName) const +{ + const bool bIsInEventNames = EventNames.ContainsByPredicate([&EventName](const TSharedPtr& ExistingName) + { + return *ExistingName == EventName; + }); + + return bIsInEventNames; +} + +TSharedRef FFlowNode_CustomEventBaseDetails::GenerateEventWidget(const TSharedPtr Item) const +{ + return SNew(STextBlock) + .Text(FText::FromName(*Item.Get())); +} + +FText FFlowNode_CustomEventBaseDetails::GetSelectedEventText() const +{ + check(CachedEventNameSelected.IsValid()); + return FText::FromName(*CachedEventNameSelected.Get()); +} + +void FFlowNode_CustomEventBaseDetails::PinSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectInfo) +{ + ensure(ObjectsBeingEdited[0].IsValid()); + + UFlowNode_CustomEventBase* Node = Cast(ObjectsBeingEdited[0].Get()); + if (IsValid(Node) && Item) + { + const bool bIsChanged = (*CachedEventNameSelected != *Item); + + if (bIsChanged) + { + CachedEventNameSelected = Item; + + const FName ItemAsName = *CachedEventNameSelected; + Node->SetEventName(ItemAsName); + + if (EventTextListWidget.IsValid()) + { + // Tell UDE to refresh the widget to show the new change + EventTextListWidget->Invalidate(EInvalidateWidgetReason::Paint); + } + } + } +} diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomInputDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomInputDetails.cpp new file mode 100644 index 000000000..5bcb3b60c --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomInputDetails.cpp @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_CustomInputDetails.h" +#include "DetailLayoutBuilder.h" +#include "FlowAsset.h" + +#define LOCTEXT_NAMESPACE "FlowNode_CustomInputDetails" + +FFlowNode_CustomInputDetails::FFlowNode_CustomInputDetails() +{ + bExcludeReferencedEvents = true; +} + +void FFlowNode_CustomInputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // For backward compatability, these localized texts are in FlowNode_CustomInputDetails, + // not FlowNode_CustomNodeBase, so passing them in to a common function. + + static const FText CustomRowNameText = LOCTEXT("CustomRowName", "Event Name"); + static const FText EventNameText = LOCTEXT("EventName", "Event Name"); + + CustomizeDetailsInternal(DetailLayout, CustomRowNameText, EventNameText); +} + +IDetailCategoryBuilder& FFlowNode_CustomInputDetails::CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const +{ + return DetailLayout.EditCategory("CustomInput", LOCTEXT("CustomInputCategory", "Custom Event")); +} + +TArray FFlowNode_CustomInputDetails::BuildEventNames(const UFlowAsset& FlowAsset) const +{ + return FlowAsset.GetCustomInputs(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomOutputDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomOutputDetails.cpp new file mode 100644 index 000000000..652e768ae --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomOutputDetails.cpp @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_CustomOutputDetails.h" +#include "DetailLayoutBuilder.h" +#include "FlowAsset.h" + +#define LOCTEXT_NAMESPACE "FlowNode_CustomOutputDetails" + +FFlowNode_CustomOutputDetails::FFlowNode_CustomOutputDetails() +{ + check(bExcludeReferencedEvents == false); +} + +void FFlowNode_CustomOutputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // For backward compatability, these localized texts are in FlowNode_CustomOutputDetails, + // not FlowNode_CustomNodeBase, so passing them in to a common function. + + static const FText CustomRowNameText = LOCTEXT("CustomRowName", "Event Name"); + static const FText EventNameText = LOCTEXT("EventName", "Event Name"); + + CustomizeDetailsInternal(DetailLayout, CustomRowNameText, EventNameText); +} + +IDetailCategoryBuilder& FFlowNode_CustomOutputDetails::CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const +{ + return DetailLayout.EditCategory("CustomOutput", LOCTEXT("CustomEventsCategory", "Custom Output")); +} + +TArray FFlowNode_CustomOutputDetails::BuildEventNames(const UFlowAsset& FlowAsset) const +{ + return FlowAsset.GetCustomOutputs(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp similarity index 69% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.cpp rename to Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp index 5c5c66f62..98074032c 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp @@ -1,4 +1,6 @@ -#include "FlowNode_Details.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_Details.h" #include "PropertyEditing.h" void FFlowNode_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.cpp new file mode 100644 index 000000000..76d5b1f4b --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.cpp @@ -0,0 +1,20 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h" +#include "Nodes/World/FlowNode_PlayLevelSequence.h" + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" + +void FFlowNode_PlayLevelSequenceDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + IDetailCategoryBuilder& SequenceCategory = DetailBuilder.EditCategory("Sequence"); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, Sequence)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, PlaybackSettings)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bPlayReverse)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, CameraSettings)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bUseGraphOwnerAsTransformOrigin)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bReplicates)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bAlwaysRelevant)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bApplyOwnerTimeDilation)); +} diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_SubGraphDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_SubGraphDetails.cpp new file mode 100644 index 000000000..81fbaadd5 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_SubGraphDetails.cpp @@ -0,0 +1,70 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_SubGraphDetails.h" + +#include "DetailLayoutBuilder.h" +#include "FlowAsset.h" +#include "Nodes/Route/FlowNode_SubGraph.h" + +void FFlowNode_SubGraphDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + TArray> ObjectsBeingEdited; + DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); + + if (ObjectsBeingEdited[0].IsValid()) + { + const UFlowNode_SubGraph* SubGraphNode = CastChecked(ObjectsBeingEdited[0]); + + // Generate the list of asset classes allowed or disallowed + TArray FlowAssetClasses; + GetDerivedClasses(UFlowAsset::StaticClass(), FlowAssetClasses, true); + FlowAssetClasses.Add(UFlowAsset::StaticClass()); + + TArray DisallowedClasses; + TArray AllowedClasses; + for (auto FlowAssetClass : FlowAssetClasses) + { + if (const UFlowAsset* DefaultAsset = Cast(FlowAssetClass->GetDefaultObject())) + { + if (DefaultAsset->DeniedInSubgraphNodeClasses.Contains(SubGraphNode->GetClass())) + { + DisallowedClasses.Add(FlowAssetClass); + } + + if (DefaultAsset->AllowedInSubgraphNodeClasses.Contains(SubGraphNode->GetClass())) + { + AllowedClasses.Add(FlowAssetClass); + } + } + } + + DisallowedClasses.Append(SubGraphNode->DeniedAssignedAssetClasses); + + for (auto FlowAssetClass : SubGraphNode->AllowedAssignedAssetClasses) + { + if (!DisallowedClasses.Contains(FlowAssetClass)) + { + AllowedClasses.AddUnique(FlowAssetClass); + } + } + + FString AllowedClassesString; + for (UClass* Class : AllowedClasses) + { + AllowedClassesString.Append(FString::Printf(TEXT("%s,"), *Class->GetFName().ToString())); + } + + FString DisallowedClassesString; + for (UClass* Class : DisallowedClasses) + { + DisallowedClassesString.Append(FString::Printf(TEXT("%s,"), *Class->GetFName().ToString())); + } + + const auto AssetProperty = DetailLayout.GetProperty(FName("Asset"), UFlowNode_SubGraph::StaticClass()); + if (FProperty* MetaDataProperty = AssetProperty->GetMetaDataProperty()) + { + MetaDataProperty->SetMetaData(TEXT("AllowedClasses"), *AllowedClassesString); + MetaDataProperty->SetMetaData(TEXT("DisallowedClasses"), *DisallowedClassesString); + } + } +} diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowOwnerFunctionRefCustomization.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowOwnerFunctionRefCustomization.cpp new file mode 100644 index 000000000..6d48f71e4 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowOwnerFunctionRefCustomization.cpp @@ -0,0 +1,158 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowOwnerFunctionRefCustomization.h" + +#include "FlowAsset.h" +#include "FlowOwnerInterface.h" +#include "Nodes/FlowNode.h" +#include "Nodes/World/FlowNode_CallOwnerFunction.h" + +#include "UObject/UnrealType.h" +#include "FlowOwnerFunctionParams.h" + + +// FFlowOwnerFunctionRefCustomization Implementation + +void FFlowOwnerFunctionRefCustomization::CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // Do not include children properties (the header is all we need to show for this struct) +} + +TSharedPtr FFlowOwnerFunctionRefCustomization::GetCuratedNamePropertyHandle() const +{ + check(StructPropertyHandle->IsValidHandle()); + + TSharedPtr FoundHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowOwnerFunctionRef, FunctionName)); + check(FoundHandle); + + return FoundHandle; +} + +TArray FFlowOwnerFunctionRefCustomization::GetCuratedNameOptions() const +{ + TArray Results; + + const UClass* ExpectedOwnerClass = TryGetExpectedOwnerClass(); + if (!IsValid(ExpectedOwnerClass)) + { + return Results; + } + + const UFlowNode_CallOwnerFunction* FlowNodeOwner = Cast(TryGetFlowNodeOuter()); + if (!IsValid(FlowNodeOwner)) + { + return Results; + } + + Results = GetFlowOwnerFunctionRefs(*FlowNodeOwner, *ExpectedOwnerClass); + + return Results; +} + +const UClass* FFlowOwnerFunctionRefCustomization::TryGetExpectedOwnerClass() const +{ + const UFlowNode* NodeOwner = TryGetFlowNodeOuter(); + const UFlowNode_CallOwnerFunction* CallOwnerFunctionNode = Cast(NodeOwner); + + if (IsValid(CallOwnerFunctionNode)) + { + return CallOwnerFunctionNode->TryGetExpectedOwnerClass(); + } + + return nullptr; +} + +TArray FFlowOwnerFunctionRefCustomization::GetFlowOwnerFunctionRefs( + const UFlowNode_CallOwnerFunction& FlowNodeOwner, + const UClass& ExpectedOwnerClass) +{ + TArray ValidFunctionNames; + + // Gather a list of potential functions + TArray PotentialFunctionNames; + ExpectedOwnerClass.GenerateFunctionList(PotentialFunctionNames); + + if (PotentialFunctionNames.Num() == 0) + { + return ValidFunctionNames; + } + + ValidFunctionNames.Reserve(PotentialFunctionNames.Num()); + + // Filter out any unusable functions (that do not match the expected signature) + for (const FName& PotentialFunctionName : PotentialFunctionNames) + { + const UFunction* PotentialFunction = ExpectedOwnerClass.FindFunctionByName(PotentialFunctionName); + check(IsValid(PotentialFunction)); + + if (IsFunctionUsable(*PotentialFunction, FlowNodeOwner)) + { + ValidFunctionNames.Add(PotentialFunctionName); + } + } + + return ValidFunctionNames; +} + +bool FFlowOwnerFunctionRefCustomization::IsFunctionUsable(const UFunction& Function, const UFlowNode_CallOwnerFunction& FlowNodeOwner) +{ + if (!UFlowNode_CallOwnerFunction::DoesFunctionHaveValidFlowOwnerFunctionSignature(Function)) + { + return false; + } + + if (!DoesFunctionHaveExpectedParamType(Function, FlowNodeOwner)) + { + return false; + } + + return true; +} + +bool FFlowOwnerFunctionRefCustomization::DoesFunctionHaveExpectedParamType(const UFunction& Function, const UFlowNode_CallOwnerFunction& FlowNodeOwner) +{ + const UClass* PropertyClass = UFlowNode_CallOwnerFunction::GetParamsClassForFunction(Function); + + return FlowNodeOwner.IsAcceptableParamsPropertyClass(PropertyClass); +} + +void FFlowOwnerFunctionRefCustomization::SetCuratedName(const FName& NewFunctionName) +{ + TSharedPtr FunctionNameHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowOwnerFunctionRef, FunctionName)); + + check(FunctionNameHandle); + + FunctionNameHandle->SetPerObjectValue(0, NewFunctionName.ToString()); +} + +FName FFlowOwnerFunctionRefCustomization::GetCuratedName() const +{ + const FFlowOwnerFunctionRef* FlowOwnerFunction = GetFlowOwnerFunctionRef(); + if (FlowOwnerFunction) + { + return FlowOwnerFunction->FunctionName; + } + else + { + return NAME_None; + } +} + +UFlowNode* FFlowOwnerFunctionRefCustomization::TryGetFlowNodeOuter() const +{ + check(StructPropertyHandle->IsValidHandle()); + + TArray OuterObjects; + StructPropertyHandle->GetOuterObjects(OuterObjects); + + for (UObject* OuterObject : OuterObjects) + { + UFlowNode* FlowNodeOuter = Cast(OuterObject); + if (IsValid(FlowNodeOuter)) + { + return FlowNodeOuter; + } + } + + return nullptr; +} diff --git a/Source/FlowEditor/Private/FlowEditorCommands.cpp b/Source/FlowEditor/Private/FlowEditorCommands.cpp index 87b4c06ec..17e852e34 100644 --- a/Source/FlowEditor/Private/FlowEditorCommands.cpp +++ b/Source/FlowEditor/Private/FlowEditorCommands.cpp @@ -1,13 +1,15 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowEditorCommands.h" #include "FlowEditorStyle.h" -#include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphSchema_Actions.h" #include "Nodes/FlowNode.h" #include "EditorStyleSet.h" #include "Misc/ConfigCacheIni.h" +#include "Styling/AppStyle.h" #define LOCTEXT_NAMESPACE "FlowGraphCommands" @@ -18,8 +20,13 @@ FFlowToolbarCommands::FFlowToolbarCommands() void FFlowToolbarCommands::RegisterCommands() { - UI_COMMAND(RefreshAsset, "Refresh Asset", "Refresh asset and all nodes", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(GoToMasterInstance, "Go To Master", "Open editor for the Flow Asset that created this Flow instance", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(RefreshAsset, "Refresh", "Refresh asset and all nodes", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(ValidateAsset, "Validate", "Validate asset and all nodes", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(SearchInAsset, "Search", "Search in the current Flow Graph", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control | EModifierKey::Shift, EKeys::F)); + UI_COMMAND(EditAssetDefaults, "Asset Defaults", "Edit the FlowAsset default properties", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(GoToParentInstance, "Go To Parent", "Open editor for the Flow Asset that created this Flow instance", EUserInterfaceActionType::Button, FInputChord()); } FFlowGraphCommands::FFlowGraphCommands() @@ -41,8 +48,13 @@ void FFlowGraphCommands::RegisterCommands() UI_COMMAND(DisablePinBreakpoint, "Disable Pin Breakpoint", "Disables a breakpoint on the pin", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(TogglePinBreakpoint, "Toggle Pin Breakpoint", "Toggles a breakpoint on the pin", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(EnableNode, "Enable Node", "Default state, node is fully executed.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(DisableNode, "Disable Node", "No logic executed, any Input Pin activation is ignored. Node instantly enters a deactivated state.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(SetPassThrough, "Set Pass Through", "Internal node logic not executed. All connected outputs are triggered, node finishes its work.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(ForcePinActivation, "Force Pin Activation", "Forces execution of the pin in a graph, used to bypass blockers", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(FocusViewport, "Focus Viewport", "Focus viewport on actor assigned to the node", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(JumpToNodeDefinition, "Jump to Node Definition", "Jump tp the node definition", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(JumpToNodeDefinition, "Jump to Node Definition", "Jump to the node definition", EUserInterfaceActionType::Button, FInputChord()); } FFlowSpawnNodeCommands::FFlowSpawnNodeCommands() diff --git a/Source/FlowEditor/Private/FlowEditorModule.cpp b/Source/FlowEditor/Private/FlowEditorModule.cpp index fdc871db6..a3382ffcb 100644 --- a/Source/FlowEditor/Private/FlowEditorModule.cpp +++ b/Source/FlowEditor/Private/FlowEditorModule.cpp @@ -1,36 +1,51 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowEditorModule.h" #include "FlowEditorStyle.h" #include "Asset/AssetTypeActions_FlowAsset.h" -#include "Asset/FlowAssetDetails.h" #include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraphConnectionDrawingPolicy.h" -#include "LevelEditor/SLevelEditorFlow.h" +#include "Graph/FlowGraphSettings.h" +#include "Utils/SLevelEditorFlow.h" #include "MovieScene/FlowTrackEditor.h" #include "Nodes/AssetTypeActions_FlowNodeBlueprint.h" -#include "Nodes/Customizations/FlowNode_Details.h" -#include "Nodes/Customizations/FlowNode_ComponentObserverDetails.h" -#include "Nodes/Customizations/FlowNode_CustomInputDetails.h" -#include "Nodes/Customizations/FlowNode_CustomOutputDetails.h" -#include "Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h" - -#include "Nodes/Customizations/FlowPinCustomization.h" +#include "Pins/SFlowInputPinHandle.h" +#include "Pins/SFlowOutputPinHandle.h" + +#include "FlowEditorDefines.h" +#if ENABLE_FLOW_SEARCH +#include "Asset/FlowAssetIndexer.h" +#endif + +#include "DetailCustomizations/FlowAssetDetails.h" +#include "DetailCustomizations/FlowNode_Details.h" +#include "DetailCustomizations/FlowNode_ComponentObserverDetails.h" +#include "DetailCustomizations/FlowNode_CustomInputDetails.h" +#include "DetailCustomizations/FlowNode_CustomOutputDetails.h" +#include "DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h" +#include "DetailCustomizations/FlowOwnerFunctionRefCustomization.h" +#include "DetailCustomizations/FlowNode_SubGraphDetails.h" #include "FlowAsset.h" #include "Nodes/Route/FlowNode_CustomInput.h" #include "Nodes/Route/FlowNode_CustomOutput.h" +#include "Nodes/Route/FlowNode_SubGraph.h" #include "Nodes/World/FlowNode_ComponentObserver.h" #include "Nodes/World/FlowNode_PlayLevelSequence.h" #include "AssetToolsModule.h" #include "EdGraphUtilities.h" +#include "IAssetSearchModule.h" #include "Framework/MultiBox/MultiBoxBuilder.h" -#include "ISequencerChannelInterface.h" +#include "ISequencerChannelInterface.h" // ignore Rider's false "unused include" warning #include "ISequencerModule.h" #include "LevelEditor.h" #include "Modules/ModuleManager.h" -#define LOCTEXT_NAMESPACE "FlowEditor" +static FName AssetSearchModuleName = TEXT("AssetSearch"); + +#define LOCTEXT_NAMESPACE "FlowEditorModule" EAssetTypeCategories::Type FFlowEditorModule::FlowAssetCategory = static_cast(0); @@ -42,74 +57,77 @@ void FFlowEditorModule::StartupModule() // register visual utilities FEdGraphUtilities::RegisterVisualPinConnectionFactory(MakeShareable(new FFlowGraphConnectionDrawingPolicyFactory)); - - // init menu extensibility - FlowAssetExtensibility.Init(); + FEdGraphUtilities::RegisterVisualPinFactory(MakeShareable(new FFlowInputPinHandleFactory())); + FEdGraphUtilities::RegisterVisualPinFactory(MakeShareable(new FFlowOutputPinHandleFactory())); // add Flow Toolbar - if (FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr(TEXT("LevelEditor"))) + if (UFlowGraphSettings::Get()->bShowAssetToolbarAboveLevelEditor) { - TSharedPtr MenuExtender = MakeShareable(new FExtender()); - MenuExtender->AddToolBarExtension("Game", EExtensionHook::After, nullptr, FToolBarExtensionDelegate::CreateRaw(this, &FFlowEditorModule::CreateFlowToolbar)); - LevelEditorModule->GetToolBarExtensibilityManager()->AddExtender(MenuExtender); + if (FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr(TEXT("LevelEditor"))) + { + const TSharedPtr MenuExtender = MakeShareable(new FExtender()); + MenuExtender->AddToolBarExtension("Play", EExtensionHook::After, nullptr, FToolBarExtensionDelegate::CreateRaw(this, &FFlowEditorModule::CreateFlowToolbar)); + LevelEditorModule->GetToolBarExtensibilityManager()->AddExtender(MenuExtender); + } } // register Flow sequence track ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked("Sequencer"); FlowTrackCreateEditorHandle = SequencerModule.RegisterTrackEditor(FOnCreateTrackEditor::CreateStatic(&FFlowTrackEditor::CreateTrackEditor)); - RegisterPropertyCustomizations(); + RegisterDetailCustomizations(); - // register detail customizations - RegisterCustomClassLayout(UFlowAsset::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowAssetDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_Details::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_ComponentObserver::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_ComponentObserverDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_CustomInput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomInputDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_CustomOutput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomOutputDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_PlayLevelSequence::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_PlayLevelSequenceDetails::MakeInstance)); - - FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); - PropertyModule.NotifyCustomizationModuleChanged(); + // register asset indexers + if (FModuleManager::Get().IsModuleLoaded(AssetSearchModuleName)) + { + RegisterAssetIndexers(); + } + ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddRaw(this, &FFlowEditorModule::ModulesChangesCallback); } void FFlowEditorModule::ShutdownModule() { FFlowEditorStyle::Shutdown(); - UnregisterAssets(); - - // unregister visual utilities - if (FlowGraphConnectionFactory.IsValid()) - { - FEdGraphUtilities::UnregisterVisualPinConnectionFactory(FlowGraphConnectionFactory); - } + UnregisterDetailCustomizations(); - // reset menu extensibility - FlowAssetExtensibility.Reset(); + UnregisterAssets(); // unregister track editors ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked("Sequencer"); SequencerModule.UnRegisterTrackEditor(FlowTrackCreateEditorHandle); - // unregister details customizations - if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + FModuleManager::Get().OnModulesChanged().Remove(ModulesChangedHandle); +} + +void FFlowEditorModule::RegisterAssets() +{ + IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); + + // try to merge asset category with a built-in one { - FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + const FText AssetCategoryText = UFlowGraphSettings::Get()->FlowAssetCategoryName; - for (auto It = CustomClassLayouts.CreateConstIterator(); It; ++It) + // Find matching built-in category + if (!AssetCategoryText.IsEmpty()) { - if (It->IsValid()) + TArray AllCategories; + AssetTools.GetAllAdvancedAssetCategories(AllCategories); + for (const FAdvancedAssetCategory& ExistingCategory : AllCategories) { - PropertyModule.UnregisterCustomClassLayout(*It); + if (ExistingCategory.CategoryName.EqualTo(AssetCategoryText)) + { + FlowAssetCategory = ExistingCategory.CategoryType; + break; + } } } - } -} -void FFlowEditorModule::RegisterAssets() -{ - IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); - FlowAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Flow")), LOCTEXT("FlowAssetCategory", "Flow")); + if (FlowAssetCategory == EAssetTypeCategories::None) + { + FlowAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Flow")), AssetCategoryText); + } + } const TSharedRef FlowAssetActions = MakeShareable(new FAssetTypeActions_FlowAsset()); RegisteredAssetActions.Add(FlowAssetActions); @@ -134,35 +152,92 @@ void FFlowEditorModule::UnregisterAssets() RegisteredAssetActions.Empty(); } -void FFlowEditorModule::RegisterPropertyCustomizations() const +void FFlowEditorModule::RegisterCustomClassLayout(const TSubclassOf Class, const FOnGetDetailCustomizationInstance DetailLayout) { - FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + if (Class) + { + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + PropertyModule.RegisterCustomClassLayout(Class->GetFName(), DetailLayout); + + CustomClassLayouts.Add(Class->GetFName()); + } +} - PropertyModule.RegisterCustomPropertyTypeLayout("FlowPin", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowPinCustomization::MakeInstance)); +void FFlowEditorModule::RegisterCustomStructLayout(const UScriptStruct& Struct, const FOnGetPropertyTypeCustomizationInstance DetailLayout) +{ + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + { + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + PropertyModule.RegisterCustomPropertyTypeLayout(Struct.GetFName(), DetailLayout); - // notify on customization change - PropertyModule.NotifyCustomizationModuleChanged(); + CustomStructLayouts.Add(Struct.GetFName()); + } } -void FFlowEditorModule::RegisterCustomClassLayout(const TSubclassOf Class, const FOnGetDetailCustomizationInstance DetailLayout) +void FFlowEditorModule::RegisterDetailCustomizations() { - if (Class) + // register detail customizations + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) { - CustomClassLayouts.Add(Class->GetFName()); + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + + RegisterCustomClassLayout(UFlowAsset::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowAssetDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_Details::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_ComponentObserver::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_ComponentObserverDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_CustomInput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomInputDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_CustomOutput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomOutputDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_PlayLevelSequence::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_PlayLevelSequenceDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_SubGraph::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_SubGraphDetails::MakeInstance)); + RegisterCustomStructLayout(*FFlowOwnerFunctionRef::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowOwnerFunctionRefCustomization::MakeInstance)); + PropertyModule.NotifyCustomizationModuleChanged(); + } +} + +void FFlowEditorModule::UnregisterDetailCustomizations() +{ + // unregister details customizations + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + { FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); - PropertyModule.RegisterCustomClassLayout(Class->GetFName(), DetailLayout); + + for (auto It = CustomClassLayouts.CreateConstIterator(); It; ++It) + { + if (It->IsValid()) + { + PropertyModule.UnregisterCustomClassLayout(*It); + } + } + + for (auto It = CustomStructLayouts.CreateConstIterator(); It; ++It) + { + if (It->IsValid()) + { + PropertyModule.UnregisterCustomPropertyTypeLayout(*It); + } + } + + PropertyModule.NotifyCustomizationModuleChanged(); } } -TSharedPtr FFlowEditorModule::GetFlowAssetMenuExtensibilityManager() const +void FFlowEditorModule::ModulesChangesCallback(const FName ModuleName, const EModuleChangeReason ReasonForChange) const { - return FlowAssetExtensibility.MenuExtensibilityManager; + if (ReasonForChange == EModuleChangeReason::ModuleLoaded && ModuleName == AssetSearchModuleName) + { + RegisterAssetIndexers(); + } } -TSharedPtr FFlowEditorModule::GetFlowAssetToolBarExtensibilityManager() const +void FFlowEditorModule::RegisterAssetIndexers() const { - return FlowAssetExtensibility.ToolBarExtensibilityManager; + /** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9070 + */ +#if ENABLE_FLOW_SEARCH + IAssetSearchModule::Get().RegisterAssetIndexer(UFlowAsset::StaticClass(), MakeUnique()); +#endif } void FFlowEditorModule::CreateFlowToolbar(FToolBarBuilder& ToolbarBuilder) const diff --git a/Source/FlowEditor/Private/FlowEditorStyle.cpp b/Source/FlowEditor/Private/FlowEditorStyle.cpp index c5d7a2a7e..23c067776 100644 --- a/Source/FlowEditor/Private/FlowEditorStyle.cpp +++ b/Source/FlowEditor/Private/FlowEditorStyle.cpp @@ -1,11 +1,14 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "FlowEditorStyle.h" #include "Interfaces/IPluginManager.h" #include "Styling/SlateStyleRegistry.h" -#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) -#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) #define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define IMAGE_BRUSH_SVG( RelativePath, ... ) FSlateVectorImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".svg")), __VA_ARGS__) TSharedPtr FFlowEditorStyle::StyleSet = nullptr; @@ -28,18 +31,20 @@ void FFlowEditorStyle::Initialize() // engine assets StyleSet->SetContentRoot(FPaths::EngineContentDir() / TEXT("Editor/Slate/")); - StyleSet->Set("FlowToolbar.RefreshAsset", new IMAGE_BRUSH("Automation/RefreshTests", Icon40)); - StyleSet->Set("FlowToolbar.RefreshAsset.Small", new IMAGE_BRUSH("Automation/RefreshTests", Icon20)); + StyleSet->Set("FlowToolbar.RefreshAsset", new IMAGE_BRUSH_SVG( "Starship/Common/Apply", Icon20)); + StyleSet->Set("FlowToolbar.ValidateAsset", new IMAGE_BRUSH_SVG( "Starship/Common/Debug", Icon20)); - StyleSet->Set("FlowToolbar.GoToMasterInstance", new IMAGE_BRUSH("Icons/icon_DebugStepOut_40x", Icon40)); - StyleSet->Set("FlowToolbar.GoToMasterInstance.Small", new IMAGE_BRUSH("Icons/icon_DebugStepOut_40x", Icon20)); + StyleSet->Set("FlowToolbar.SearchInAsset", new IMAGE_BRUSH_SVG( "Starship/Common/Search", Icon20)); + StyleSet->Set("FlowToolbar.EditAssetDefaults", new IMAGE_BRUSH_SVG("Starship/Common/Details", Icon20)); + + StyleSet->Set("FlowToolbar.GoToParentInstance", new IMAGE_BRUSH("Icons/icon_DebugStepOut_40x", Icon40)); StyleSet->Set("FlowGraph.BreakpointEnabled", new IMAGE_BRUSH("Old/Kismet2/Breakpoint_Valid", FVector2D(24.0f, 24.0f))); StyleSet->Set("FlowGraph.BreakpointDisabled", new IMAGE_BRUSH("Old/Kismet2/Breakpoint_Disabled", FVector2D(24.0f, 24.0f))); StyleSet->Set("FlowGraph.BreakpointHit", new IMAGE_BRUSH("Old/Kismet2/IP_Breakpoint", Icon40)); StyleSet->Set("FlowGraph.PinBreakpointHit", new IMAGE_BRUSH("Old/Kismet2/IP_Breakpoint", Icon30)); - StyleSet->Set( "GraphEditor.Sequence_16x", new IMAGE_BRUSH("Icons/icon_Blueprint_Sequence_16x", Icon16)); + StyleSet->Set("GraphEditor.Sequence_16x", new IMAGE_BRUSH("Icons/icon_Blueprint_Sequence_16x", Icon16)); // Flow assets StyleSet->SetContentRoot(IPluginManager::Get().FindPlugin(TEXT("Flow"))->GetBaseDir() / TEXT("Resources")); @@ -62,6 +67,7 @@ void FFlowEditorStyle::Shutdown() StyleSet.Reset(); } -#undef IMAGE_BRUSH -#undef BOX_BRUSH #undef BORDER_BRUSH +#undef BOX_BRUSH +#undef IMAGE_BRUSH +#undef IMAGE_BRUSH_SVG diff --git a/Source/FlowEditor/Private/Graph/FlowGraph.cpp b/Source/FlowEditor/Private/Graph/FlowGraph.cpp index 10157e1eb..b83c18312 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraph.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraph.cpp @@ -1,7 +1,12 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/FlowGraph.h" #include "Graph/FlowGraphSchema.h" +#include "Graph/FlowGraphSchema_Actions.h" #include "Graph/Nodes/FlowGraphNode.h" +#include "Nodes/FlowNode.h" + #include "Kismet2/BlueprintEditorUtils.h" void FFlowGraphInterface::OnInputTriggered(UEdGraphNode* GraphNode, const int32 Index) const @@ -25,7 +30,7 @@ UFlowGraph::UFlowGraph(const FObjectInitializer& ObjectInitializer) UEdGraph* UFlowGraph::CreateGraph(UFlowAsset* InFlowAsset) { - UFlowGraph* NewGraph = CastChecked(FBlueprintEditorUtils::CreateNewGraph(InFlowAsset, NAME_None, StaticClass(), UFlowGraphSchema::StaticClass())); + UEdGraph* NewGraph = CastChecked(FBlueprintEditorUtils::CreateNewGraph(InFlowAsset, NAME_None, StaticClass(), UFlowGraphSchema::StaticClass())); NewGraph->bAllowDeletion = false; InFlowAsset->FlowGraph = NewGraph; @@ -34,6 +39,36 @@ UEdGraph* UFlowGraph::CreateGraph(UFlowAsset* InFlowAsset) return NewGraph; } +void UFlowGraph::RefreshGraph() +{ + // don't run fixup in PIE + if (GEditor && !GEditor->PlayWorld) + { + // check if all Graph Nodes have expected, up-to-date type + CastChecked(GetSchema())->GatherNativeNodes(); + for (const TPair& Node : GetFlowAsset()->GetNodes()) + { + if (UFlowNode* FlowNode = Node.Value) + { + const UClass* ExpectGraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(FlowNode->GetClass()); + if (FlowNode->GetGraphNode() && FlowNode->GetGraphNode()->GetClass() != ExpectGraphNodeClass) + { + // Create a new Flow Graph Node of proper type + FFlowGraphSchemaAction_NewNode::RecreateNode(this, FlowNode->GetGraphNode(), FlowNode); + } + } + } + + // refresh nodes + TArray FlowGraphNodes; + GetNodesOfClass(FlowGraphNodes); + for (UFlowGraphNode* GraphNode : FlowGraphNodes) + { + GraphNode->OnGraphRefresh(); + } + } +} + void UFlowGraph::NotifyGraphChanged() { GetFlowAsset()->HarvestNodeConnections(); @@ -44,5 +79,5 @@ void UFlowGraph::NotifyGraphChanged() UFlowAsset* UFlowGraph::GetFlowAsset() const { - return CastChecked(GetOuter()); + return GetTypedOuter(); } diff --git a/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp b/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp index 348596dac..42510dddd 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp @@ -1,13 +1,17 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/FlowGraphConnectionDrawingPolicy.h" -#include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphEditorSettings.h" #include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphSettings.h" #include "Graph/FlowGraphUtils.h" #include "Graph/Nodes/FlowGraphNode.h" #include "FlowAsset.h" +#include "Graph/Nodes/FlowGraphNode_Reroute.h" #include "Nodes/FlowNode.h" #include "Misc/App.h" @@ -24,8 +28,6 @@ FConnectionDrawingPolicy* FFlowGraphConnectionDrawingPolicyFactory::CreateConnec ///////////////////////////////////////////////////// // FFlowGraphConnectionDrawingPolicy -class UGraphEditorSettings; - FFlowGraphConnectionDrawingPolicy::FFlowGraphConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) : FConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, ZoomFactor, InClippingRect, InDrawElements) , GraphObj(InGraphObj) @@ -77,17 +79,17 @@ void FFlowGraphConnectionDrawingPolicy::BuildPaths() } } - if (GraphObj && (UFlowGraphSettings::Get()->bHighlightInputWiresOfSelectedNodes || UFlowGraphSettings::Get()->bHighlightOutputWiresOfSelectedNodes)) + if (GraphObj && (UFlowGraphEditorSettings::Get()->bHighlightInputWiresOfSelectedNodes || UFlowGraphEditorSettings::Get()->bHighlightOutputWiresOfSelectedNodes)) { - const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(GraphObj); - if (FlowAssetEditor.IsValid()) + const TSharedPtr FlowGraphEditor = FFlowGraphUtils::GetFlowGraphEditor(GraphObj); + if (FlowGraphEditor.IsValid()) { - for (UFlowGraphNode* SelectedNode : FlowAssetEditor->GetSelectedFlowNodes()) + for (UFlowGraphNode* SelectedNode : FlowGraphEditor->GetSelectedFlowNodes()) { for (UEdGraphPin* Pin : SelectedNode->Pins) { - if ((Pin->Direction == EGPD_Input && UFlowGraphSettings::Get()->bHighlightInputWiresOfSelectedNodes) - || (Pin->Direction == EGPD_Output && UFlowGraphSettings::Get()->bHighlightOutputWiresOfSelectedNodes)) + if ((Pin->Direction == EGPD_Input && UFlowGraphEditorSettings::Get()->bHighlightInputWiresOfSelectedNodes) + || (Pin->Direction == EGPD_Output && UFlowGraphEditorSettings::Get()->bHighlightOutputWiresOfSelectedNodes)) { for (UEdGraphPin* LinkedPin : Pin->LinkedTo) { @@ -100,12 +102,18 @@ void FFlowGraphConnectionDrawingPolicy::BuildPaths() } } -void FFlowGraphConnectionDrawingPolicy::Draw(TMap, FArrangedWidget>& InPinGeometries, FArrangedChildren& ArrangedNodes) +void FFlowGraphConnectionDrawingPolicy::DrawConnection(int32 LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) { - BuildPaths(); - - // Draw everything - FConnectionDrawingPolicy::Draw(InPinGeometries, ArrangedNodes); + switch (UFlowGraphSettings::Get()->ConnectionDrawType) + { + case EFlowConnectionDrawType::Default: + FConnectionDrawingPolicy::DrawConnection(LayerId, Start, End, Params); + break; + case EFlowConnectionDrawType::Circuit: + DrawCircuitSpline(LayerId, Start, End, Params); + break; + default: ; + } } // Give specific editor modes a chance to highlight this connection or darken non-interesting connections @@ -123,6 +131,11 @@ void FFlowGraphConnectionDrawingPolicy::DetermineWiringStyle(UEdGraphPin* Output { Params.WireColor = FLinearColor::Red; } + else if (Cast(OutputPin->GetOwningNode())->GetSignalMode() == EFlowSignalMode::Disabled) + { + Params.WireColor *= 0.5f; + Params.WireThickness = 0.5f; + } else { Params.WireColor = Schema->GetPinTypeColor(OutputPin->PinType); @@ -161,4 +174,201 @@ void FFlowGraphConnectionDrawingPolicy::DetermineWiringStyle(UEdGraphPin* Output Params.WireThickness = InactiveWireThickness; } } + + // If reroute node path goes backwards, we need to flip the direction to make it look nice + // (all of the logic for this is basically same as in FKismetConnectionDrawingPolicy) + { + UEdGraphNode* OutputNode = OutputPin->GetOwningNode(); + UEdGraphNode* InputNode = (InputPin != nullptr) ? InputPin->GetOwningNode() : nullptr; + if (auto* OutputRerouteNode = Cast(OutputNode)) + { + if (ShouldChangeTangentForReroute(OutputRerouteNode)) + { + Params.StartDirection = EGPD_Input; + } + } + + if (auto* InputRerouteNode = Cast(InputNode)) + { + if (ShouldChangeTangentForReroute(InputRerouteNode)) + { + Params.EndDirection = EGPD_Output; + } + } + } +} + +void FFlowGraphConnectionDrawingPolicy::Draw(TMap, FArrangedWidget>& InPinGeometries, FArrangedChildren& ArrangedNodes) +{ + BuildPaths(); + + FConnectionDrawingPolicy::Draw(InPinGeometries, ArrangedNodes); +} + +void FFlowGraphConnectionDrawingPolicy::DrawCircuitSpline(const int32& LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) const +{ + const FVector2D StartingPoint = FVector2D(Start.X + UFlowGraphSettings::Get()->CircuitConnectionSpacing.X, Start.Y); + const FVector2D EndPoint = FVector2D(End.X - UFlowGraphSettings::Get()->CircuitConnectionSpacing.Y, End.Y); + const FVector2D ControlPoint = GetControlPoint(StartingPoint, EndPoint); + + const FVector2D StartDirection = (Params.StartDirection == EGPD_Output) ? FVector2D(1.0f, 0.0f) : FVector2D(-1.0f, 0.0f); + const FVector2D EndDirection = (Params.EndDirection == EGPD_Input) ? FVector2D(1.0f, 0.0f) : FVector2D(-1.0f, 0.0f); + + DrawCircuitConnection(LayerId, Start, StartDirection, StartingPoint, EndDirection, Params); + DrawCircuitConnection(LayerId, StartingPoint, StartDirection, ControlPoint, EndDirection, Params); + DrawCircuitConnection(LayerId, ControlPoint, StartDirection, EndPoint, EndDirection, Params); + DrawCircuitConnection(LayerId, EndPoint, StartDirection, End, EndDirection, Params); +} + +void FFlowGraphConnectionDrawingPolicy::DrawCircuitConnection(const int32& LayerId, const FVector2D& Start, const FVector2D& StartDirection, const FVector2D& End, const FVector2D& EndDirection, const FConnectionParams& Params) const +{ + FSlateDrawElement::MakeDrawSpaceSpline(DrawElementsList, LayerId, Start, StartDirection, End, EndDirection, Params.WireThickness, ESlateDrawEffect::None, Params.WireColor); + + if (Params.bDrawBubbles) + { + // This table maps distance along curve to alpha + FInterpCurve SplineReparamTable; + const float SplineLength = MakeSplineReparamTable(Start, StartDirection, End, EndDirection, SplineReparamTable); + + // Draw bubbles on the spline + if (Params.bDrawBubbles) + { + const float BubbleSpacing = 64.f * ZoomFactor; + const float BubbleSpeed = 192.f * ZoomFactor; + const FVector2D BubbleSize = BubbleImage->ImageSize * ZoomFactor * 0.2f * Params.WireThickness; + + const float Time = (FPlatformTime::Seconds() - GStartTime); + const float BubbleOffset = FMath::Fmod(Time * BubbleSpeed, BubbleSpacing); + const int32 NumBubbles = FMath::CeilToInt(SplineLength / BubbleSpacing); + for (int32 i = 0; i < NumBubbles; ++i) + { + const float Distance = (static_cast(i) * BubbleSpacing) + BubbleOffset; + if (Distance < SplineLength) + { + const float Alpha = SplineReparamTable.Eval(Distance, 0.f); + FVector2D BubblePos = FMath::CubicInterp(Start, StartDirection, End, EndDirection, Alpha); + BubblePos -= (BubbleSize * 0.5f); + + FSlateDrawElement::MakeBox(DrawElementsList, LayerId, FPaintGeometry(BubblePos, BubbleSize, ZoomFactor), BubbleImage, ESlateDrawEffect::None, Params.WireColor); + } + } + } + } +} + +FVector2D FFlowGraphConnectionDrawingPolicy::GetControlPoint(const FVector2D& Source, const FVector2D& Target) +{ + const FVector2D Delta = Target - Source; + const float Tangent = FMath::Tan(UFlowGraphSettings::Get()->CircuitConnectionAngle * (PI / 180.f)); + + const float DeltaX = FMath::Abs(Delta.X); + const float DeltaY = FMath::Abs(Delta.Y); + + const float SlopeWidth = DeltaY / Tangent; + if (DeltaX > SlopeWidth) + { + return Delta.X > 0.f ? FVector2D(Target.X - SlopeWidth, Source.Y) : FVector2D(Source.X - SlopeWidth, Target.Y); + } + + const float SlopeHeight = DeltaX * Tangent; + if (DeltaY > SlopeHeight) + { + if (Delta.Y > 0.f) + { + return Delta.X < 0.f ? FVector2D(Source.X, Target.Y - SlopeHeight) : FVector2D(Target.X, Source.Y + SlopeHeight); + } + + if (Delta.X < 0.f) + { + return FVector2D(Source.X, Target.Y + SlopeHeight); + } + } + + return FVector2D(Target.X, Source.Y - SlopeHeight); } + +bool FFlowGraphConnectionDrawingPolicy::ShouldChangeTangentForReroute(UFlowGraphNode_Reroute* Reroute) +{ + if (const bool* pResult = RerouteToReversedDirectionMap.Find(Reroute)) + { + return *pResult; + } + else + { + bool bPinReversed = false; + + FVector2D AverageLeftPin; + FVector2D AverageRightPin; + FVector2D CenterPin; + const bool bCenterValid = Reroute->OutputPins.Num() == 0 ? false : FindPinCenter(Reroute->OutputPins[0], /*out*/ CenterPin); + const bool bLeftValid = GetAverageConnectedPosition(Reroute, EGPD_Input, /*out*/ AverageLeftPin); + const bool bRightValid = GetAverageConnectedPosition(Reroute, EGPD_Output, /*out*/ AverageRightPin); + + if (bLeftValid && bRightValid) + { + bPinReversed = AverageRightPin.X < AverageLeftPin.X; + } + else if (bCenterValid) + { + if (bLeftValid) + { + bPinReversed = CenterPin.X < AverageLeftPin.X; + } + else if (bRightValid) + { + bPinReversed = AverageRightPin.X < CenterPin.X; + } + } + + RerouteToReversedDirectionMap.Add(Reroute, bPinReversed); + + return bPinReversed; + } +} + +bool FFlowGraphConnectionDrawingPolicy::FindPinCenter(const UEdGraphPin* Pin, FVector2D& OutCenter) const +{ + if (const TSharedPtr* PinWidget = PinToPinWidgetMap.Find(Pin)) + { + if (const FArrangedWidget* PinEntry = PinGeometries->Find((*PinWidget).ToSharedRef())) + { + OutCenter = FGeometryHelper::CenterOf(PinEntry->Geometry); + return true; + } + } + + return false; +} + +bool FFlowGraphConnectionDrawingPolicy::GetAverageConnectedPosition(UFlowGraphNode_Reroute* Reroute, EEdGraphPinDirection Direction, FVector2D& OutPos) const +{ + FVector2D Result = FVector2D::ZeroVector; + int32 ResultCount = 0; + + if(Reroute->InputPins.Num() == 0 || Reroute->OutputPins.Num() == 0) + { + return false; + } + + UEdGraphPin* Pin = (Direction == EGPD_Input) ? Reroute->InputPins[0] : Reroute->OutputPins[0]; + for (const UEdGraphPin* LinkedPin : Pin->LinkedTo) + { + FVector2D CenterPoint; + if (FindPinCenter(LinkedPin, /*out*/ CenterPoint)) + { + Result += CenterPoint; + ResultCount++; + } + } + + if (ResultCount > 0) + { + OutPos = Result * (1.0f / ResultCount); + return true; + } + else + { + return false; + } +} + diff --git a/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp new file mode 100644 index 000000000..2ffc2022f --- /dev/null +++ b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp @@ -0,0 +1,1031 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/FlowGraphEditor.h" + +#include "FlowEditorCommands.h" + +#include "Asset/FlowAssetEditor.h" +#include "Asset/FlowDebuggerSubsystem.h" +#include "Graph/FlowGraphEditorSettings.h" +#include "Graph/FlowGraphSchema_Actions.h" +#include "Graph/Nodes/FlowGraphNode.h" + +#include "Nodes/Route/FlowNode_SubGraph.h" + +#include "Framework/Commands/GenericCommands.h" +#include "HAL/PlatformApplicationMisc.h" +#include "LevelEditor.h" +#include "Modules/ModuleManager.h" +#include "ScopedTransaction.h" +#include "Widgets/Docking/SDockTab.h" + +#define LOCTEXT_NAMESPACE "FlowGraphEditor" + +void SFlowGraphEditor::Construct(const FArguments& InArgs, const TSharedPtr InAssetEditor) +{ + FlowAssetEditor = InAssetEditor; + FlowAsset = FlowAssetEditor.Pin()->GetFlowAsset(); + + DetailsView = InArgs._DetailsView; + + BindGraphCommands(); + + SGraphEditor::FArguments Arguments; + Arguments._AdditionalCommands = CommandList; + Arguments._Appearance = GetGraphAppearanceInfo(); + Arguments._GraphToEdit = FlowAsset->GetGraph(); + Arguments._GraphEvents = InArgs._GraphEvents; + Arguments._AutoExpandActionMenu = true; + Arguments._GraphEvents.OnSelectionChanged = FOnSelectionChanged::CreateSP(this, &SFlowGraphEditor::OnSelectedNodesChanged); + Arguments._GraphEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &SFlowGraphEditor::OnNodeDoubleClicked); + Arguments._GraphEvents.OnTextCommitted = FOnNodeTextCommitted::CreateSP(this, &SFlowGraphEditor::OnNodeTitleCommitted); + Arguments._GraphEvents.OnSpawnNodeByShortcut = FOnSpawnNodeByShortcut::CreateStatic(&SFlowGraphEditor::OnSpawnGraphNodeByShortcut, static_cast(FlowAsset->GetGraph())); + + SGraphEditor::Construct(Arguments); +} + +void SFlowGraphEditor::BindGraphCommands() +{ + FGraphEditorCommands::Register(); + FFlowGraphCommands::Register(); + FFlowSpawnNodeCommands::Register(); + + const FGenericCommands& GenericCommands = FGenericCommands::Get(); + const FGraphEditorCommandsImpl& GraphEditorCommands = FGraphEditorCommands::Get(); + const FFlowGraphCommands& FlowGraphCommands = FFlowGraphCommands::Get(); + + CommandList = MakeShareable(new FUICommandList); + + // Graph commands + CommandList->MapAction(GraphEditorCommands.CreateComment, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnCreateComment), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::CanEdit)); + + CommandList->MapAction(GraphEditorCommands.StraightenConnections, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnStraightenConnections)); + + // Generic Node commands + CommandList->MapAction(GenericCommands.Undo, + FExecuteAction::CreateStatic(&SFlowGraphEditor::UndoGraphAction), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::CanEdit)); + + CommandList->MapAction(GenericCommands.Redo, + FExecuteAction::CreateStatic(&SFlowGraphEditor::RedoGraphAction), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::CanEdit)); + + CommandList->MapAction(GenericCommands.SelectAll, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SelectAllNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSelectAllNodes)); + + CommandList->MapAction(GenericCommands.Delete, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::DeleteSelectedNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDeleteNodes)); + + CommandList->MapAction(GenericCommands.Copy, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::CopySelectedNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanCopyNodes)); + + CommandList->MapAction(GenericCommands.Cut, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::CutSelectedNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanCutNodes)); + + CommandList->MapAction(GenericCommands.Paste, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::PasteNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanPasteNodes)); + + CommandList->MapAction(GenericCommands.Duplicate, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::DuplicateNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDuplicateNodes)); + + // Pin commands + CommandList->MapAction(FlowGraphCommands.RefreshContextPins, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::RefreshContextPins), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanRefreshContextPins)); + + CommandList->MapAction(FlowGraphCommands.AddInput, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::AddInput), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddInput)); + + CommandList->MapAction(FlowGraphCommands.AddOutput, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::AddOutput), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddOutput)); + + CommandList->MapAction(FlowGraphCommands.RemovePin, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::RemovePin), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanRemovePin)); + + // Breakpoint commands + CommandList->MapAction(GraphEditorCommands.AddBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAddBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanAddBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.RemoveBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnRemoveBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanRemoveBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanRemoveBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.EnableBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnEnableBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanEnableBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanEnableBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.DisableBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnDisableBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDisableBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanDisableBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.ToggleBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnToggleBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanToggleBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanToggleBreakpoint) + ); + + // Pin Breakpoint commands + CommandList->MapAction(FlowGraphCommands.AddPinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAddPinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddPinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanAddPinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.RemovePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnRemovePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanRemovePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanRemovePinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.EnablePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnEnablePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanEnablePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanEnablePinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.DisablePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnDisablePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDisablePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanDisablePinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.TogglePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnTogglePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanTogglePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanTogglePinBreakpoint) + ); + + // Execution Override commands + CommandList->MapAction(FlowGraphCommands.EnableNode, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SetSignalMode, EFlowSignalMode::Enabled), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Enabled), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Enabled) + ); + + CommandList->MapAction(FlowGraphCommands.DisableNode, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SetSignalMode, EFlowSignalMode::Disabled), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Disabled), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Disabled) + ); + + CommandList->MapAction(FlowGraphCommands.SetPassThrough, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SetSignalMode, EFlowSignalMode::PassThrough), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::PassThrough), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::PassThrough) + ); + + CommandList->MapAction(FlowGraphCommands.ForcePinActivation, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnForcePinActivation), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::IsPIE), + FIsActionChecked(), + FIsActionButtonVisible::CreateStatic(&SFlowGraphEditor::IsPIE) + ); + + // Jump commands + CommandList->MapAction(FlowGraphCommands.FocusViewport, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::FocusViewport), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanFocusViewport)); + + CommandList->MapAction(FlowGraphCommands.JumpToNodeDefinition, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::JumpToNodeDefinition), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanJumpToNodeDefinition)); +} + +FGraphAppearanceInfo SFlowGraphEditor::GetGraphAppearanceInfo() const +{ + FGraphAppearanceInfo AppearanceInfo; + AppearanceInfo.CornerText = GetCornerText(); + + if (UFlowDebuggerSubsystem::IsPlaySessionPaused()) + { + AppearanceInfo.PIENotifyText = LOCTEXT("PausedLabel", "PAUSED"); + } + + return AppearanceInfo; +} + +FText SFlowGraphEditor::GetCornerText() const +{ + return LOCTEXT("AppearanceCornerText_FlowAsset", "FLOW"); +} + +void SFlowGraphEditor::UndoGraphAction() +{ + GEditor->UndoTransaction(); +} + +void SFlowGraphEditor::RedoGraphAction() +{ + GEditor->RedoTransaction(); +} + +FReply SFlowGraphEditor::OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2D& InPosition, UEdGraph* InGraph) +{ + UEdGraph* Graph = InGraph; + + if (FFlowSpawnNodeCommands::IsRegistered()) + { + const TSharedPtr Action = FFlowSpawnNodeCommands::Get().GetActionByChord(InChord); + if (Action.IsValid()) + { + TArray DummyPins; + Action->PerformAction(Graph, DummyPins, InPosition); + return FReply::Handled(); + } + } + + return FReply::Unhandled(); +} + +void SFlowGraphEditor::OnCreateComment() const +{ + FFlowGraphSchemaAction_NewComment CommentAction; + CommentAction.PerformAction(FlowAsset->GetGraph(), nullptr, GetPasteLocation()); +} + +bool SFlowGraphEditor::CanEdit() +{ + return GEditor->PlayWorld == nullptr; +} + +bool SFlowGraphEditor::IsPIE() +{ + return GEditor->PlayWorld != nullptr; +} + +bool SFlowGraphEditor::IsTabFocused() const +{ + return FlowAssetEditor.Pin()->IsTabFocused(FFlowAssetEditor::GraphTab); +} + +void SFlowGraphEditor::SelectSingleNode(UEdGraphNode* Node) +{ + ClearSelectionSet(); + SetNodeSelection(Node, true); +} + +void SFlowGraphEditor::OnSelectedNodesChanged(const TSet& Nodes) +{ + TArray SelectedObjects; + + if (Nodes.Num() > 0) + { + FlowAssetEditor.Pin()->SetUISelectionState(FFlowAssetEditor::GraphTab); + + for (TSet::TConstIterator SetIt(Nodes); SetIt; ++SetIt) + { + if (const UFlowGraphNode* GraphNode = Cast(*SetIt)) + { + SelectedObjects.Add(Cast(GraphNode->GetFlowNode())); + } + else + { + SelectedObjects.Add(*SetIt); + } + } + } + else + { + FlowAssetEditor.Pin()->SetUISelectionState(NAME_None); + SelectedObjects.Add(FlowAsset.Get()); + } + + if (DetailsView.IsValid()) + { + DetailsView->SetObjects(SelectedObjects); + } + + OnSelectionChangedEvent.ExecuteIfBound(Nodes); +} + +TSet SFlowGraphEditor::GetSelectedFlowNodes() const +{ + TSet Result; + + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + { + if (UFlowGraphNode* SelectedNode = Cast(*NodeIt)) + { + Result.Emplace(SelectedNode); + } + } + + return Result; +} + +void SFlowGraphEditor::DeleteSelectedNodes() +{ + const FScopedTransaction Transaction(LOCTEXT("DeleteSelectedNode", "Delete Selected Node")); + GetCurrentGraph()->Modify(); + FlowAsset->Modify(); + + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + FlowAssetEditor.Pin()->SetUISelectionState(NAME_None); + + for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + { + UEdGraphNode* Node = CastChecked(*NodeIt); + if (Node->CanUserDeleteNode()) + { + if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + if (FlowGraphNode->GetFlowNode()) + { + const FGuid NodeGuid = FlowGraphNode->GetFlowNode()->GetGuid(); + + GetCurrentGraph()->GetSchema()->BreakNodeLinks(*Node); + Node->DestroyNode(); + + FlowAsset->UnregisterNode(NodeGuid); + continue; + } + } + + GetCurrentGraph()->GetSchema()->BreakNodeLinks(*Node); + Node->DestroyNode(); + } + } +} + +void SFlowGraphEditor::DeleteSelectedDuplicableNodes() +{ + // Cache off the old selection + const FGraphPanelSelectionSet OldSelectedNodes = GetSelectedNodes(); + + // Clear the selection and only select the nodes that can be duplicated + FGraphPanelSelectionSet RemainingNodes; + ClearSelectionSet(); + + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(OldSelectedNodes); SelectedIt; ++SelectedIt) + { + if (UEdGraphNode* Node = Cast(*SelectedIt)) + { + if (Node->CanDuplicateNode()) + { + SetNodeSelection(Node, true); + } + else + { + RemainingNodes.Add(Node); + } + } + } + + // Delete the duplicable nodes + DeleteSelectedNodes(); + + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(RemainingNodes); SelectedIt; ++SelectedIt) + { + if (UEdGraphNode* Node = Cast(*SelectedIt)) + { + SetNodeSelection(Node, true); + } + } +} + +bool SFlowGraphEditor::CanDeleteNodes() const +{ + if (CanEdit() && IsTabFocused()) + { + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + { + if (const UEdGraphNode* Node = Cast(*NodeIt)) + { + if (!Node->CanUserDeleteNode()) + { + return false; + } + } + } + + return SelectedNodes.Num() > 0; + } + + return false; +} + +void SFlowGraphEditor::CutSelectedNodes() +{ + CopySelectedNodes(); + + // Cut should only delete nodes that can be duplicated + DeleteSelectedDuplicableNodes(); +} + +bool SFlowGraphEditor::CanCutNodes() const +{ + return CanCopyNodes() && CanDeleteNodes(); +} + +void SFlowGraphEditor::CopySelectedNodes() const +{ + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + { + if (UFlowGraphNode* Node = Cast(*SelectedIt)) + { + Node->PrepareForCopying(); + } + } + + // Export the selected nodes and place the text on the clipboard + FString ExportedText; + FEdGraphUtilities::ExportNodesToText(SelectedNodes, /*out*/ ExportedText); + FPlatformApplicationMisc::ClipboardCopy(*ExportedText); + + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + { + if (UFlowGraphNode* Node = Cast(*SelectedIt)) + { + Node->PostCopyNode(); + } + } +} + +bool SFlowGraphEditor::CanCopyNodes() const +{ + if (CanEdit() && IsTabFocused()) + { + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + { + const UEdGraphNode* Node = Cast(*SelectedIt); + if (Node && Node->CanDuplicateNode()) + { + return true; + } + } + } + + return false; +} + +void SFlowGraphEditor::PasteNodes() +{ + PasteNodesHere(GetPasteLocation()); +} + +void SFlowGraphEditor::PasteNodesHere(const FVector2D& Location) +{ + FlowAssetEditor.Pin()->SetUISelectionState(NAME_None); + + // Undo/Redo support + const FScopedTransaction Transaction(LOCTEXT("PasteNode", "Paste Node")); + FlowAsset->GetGraph()->Modify(); + FlowAsset->Modify(); + + // Clear the selection set (newly pasted stuff will be selected) + ClearSelectionSet(); + + // Grab the text to paste from the clipboard. + FString TextToImport; + FPlatformApplicationMisc::ClipboardPaste(TextToImport); + + // Import the nodes + TSet PastedNodes; + FEdGraphUtilities::ImportNodesFromText(FlowAsset->GetGraph(), TextToImport, /*out*/ PastedNodes); + + //Average position of nodes so we can move them while still maintaining relative distances to each other + FVector2D AvgNodePosition(0.0f, 0.0f); + + for (TSet::TIterator It(PastedNodes); It; ++It) + { + const UEdGraphNode* Node = *It; + AvgNodePosition.X += Node->NodePosX; + AvgNodePosition.Y += Node->NodePosY; + } + + if (PastedNodes.Num() > 0) + { + const float InvNumNodes = 1.0f / static_cast(PastedNodes.Num()); + AvgNodePosition.X *= InvNumNodes; + AvgNodePosition.Y *= InvNumNodes; + } + + for (TSet::TIterator It(PastedNodes); It; ++It) + { + UEdGraphNode* Node = *It; + + // Give new node a different Guid from the old one + Node->CreateNewGuid(); + + if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + FlowAsset->RegisterNode(Node->NodeGuid, FlowGraphNode->GetFlowNode()); + } + + // Select the newly pasted stuff + SetNodeSelection(Node, true); + + Node->NodePosX = (Node->NodePosX - AvgNodePosition.X) + Location.X; + Node->NodePosY = (Node->NodePosY - AvgNodePosition.Y) + Location.Y; + + Node->SnapToGrid(SNodePanel::GetSnapGridSize()); + } + + // Force new pasted FlowNodes to have same connections as graph nodes + FlowAsset->HarvestNodeConnections(); + + // Update UI + NotifyGraphChanged(); + + FlowAsset->PostEditChange(); + FlowAsset->MarkPackageDirty(); +} + +bool SFlowGraphEditor::CanPasteNodes() const +{ + if (CanEdit() && IsTabFocused()) + { + FString ClipboardContent; + FPlatformApplicationMisc::ClipboardPaste(ClipboardContent); + + return FEdGraphUtilities::CanImportNodesFromText(FlowAsset->GetGraph(), ClipboardContent); + } + + return false; +} + +void SFlowGraphEditor::DuplicateNodes() +{ + CopySelectedNodes(); + PasteNodes(); +} + +bool SFlowGraphEditor::CanDuplicateNodes() const +{ + return CanCopyNodes(); +} + +void SFlowGraphEditor::OnNodeDoubleClicked(class UEdGraphNode* Node) const +{ + UFlowNode* FlowNode = Cast(Node)->GetFlowNode(); + + if (FlowNode) + { + if (UFlowGraphEditorSettings::Get()->NodeDoubleClickTarget == EFlowNodeDoubleClickTarget::NodeDefinition) + { + Node->JumpToDefinition(); + } + else + { + const FString AssetPath = FlowNode->GetAssetPath(); + if (!AssetPath.IsEmpty()) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetPath); + } + else if (UObject* AssetToEdit = FlowNode->GetAssetToEdit()) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetToEdit); + + if (IsPIE()) + { + if (UFlowNode_SubGraph* SubGraphNode = Cast(FlowNode)) + { + const TWeakObjectPtr SubFlowInstance = SubGraphNode->GetFlowAsset()->GetFlowInstance(SubGraphNode); + if (SubFlowInstance.IsValid()) + { + SubGraphNode->GetFlowAsset()->GetTemplateAsset()->SetInspectedInstance(SubFlowInstance->GetDisplayName()); + } + } + } + } + } + } +} + +void SFlowGraphEditor::OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged) +{ + if (NodeBeingChanged) + { + const FScopedTransaction Transaction(LOCTEXT("RenameNode", "Rename Node")); + NodeBeingChanged->Modify(); + NodeBeingChanged->OnRenameNode(NewText.ToString()); + } +} + +void SFlowGraphEditor::RefreshContextPins() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->RefreshContextPins(true); + } +} + +bool SFlowGraphEditor::CanRefreshContextPins() const +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->SupportsContextPins(); + } + } + + return false; +} + +void SFlowGraphEditor::AddInput() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->AddUserInput(); + } +} + +bool SFlowGraphEditor::CanAddInput() const +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->CanUserAddInput(); + } + } + + return false; +} + +void SFlowGraphEditor::AddOutput() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->AddUserOutput(); + } +} + +bool SFlowGraphEditor::CanAddOutput() const +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->CanUserAddOutput(); + } + } + + return false; +} + +void SFlowGraphEditor::RemovePin() +{ + if (UEdGraphPin* SelectedPin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* SelectedNode = Cast(SelectedPin->GetOwningNode())) + { + SelectedNode->RemoveInstancePin(SelectedPin); + } + } +} + +bool SFlowGraphEditor::CanRemovePin() +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + if (Pin->Direction == EGPD_Input) + { + return GraphNode->CanUserRemoveInput(Pin); + } + else + { + return GraphNode->CanUserRemoveOutput(Pin); + } + } + } + } + + return false; +} + +void SFlowGraphEditor::OnAddBreakpoint() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->NodeBreakpoint.AllowTrait(); + } +} + +void SFlowGraphEditor::OnAddPinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + GraphNode->PinBreakpoints.Add(Pin, FFlowPinTrait(true)); + } + } +} + +bool SFlowGraphEditor::CanAddBreakpoint() const +{ + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return !SelectedNode->NodeBreakpoint.IsAllowed(); + } + + return false; +} + +bool SFlowGraphEditor::CanAddPinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + return !GraphNode->PinBreakpoints.Contains(Pin) || !GraphNode->PinBreakpoints[Pin].IsAllowed(); + } + } + + return false; +} + +void SFlowGraphEditor::OnRemoveBreakpoint() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->NodeBreakpoint.DisallowTrait(); + } +} + +void SFlowGraphEditor::OnRemovePinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + GraphNode->PinBreakpoints.Remove(Pin); + } + } +} + +bool SFlowGraphEditor::CanRemoveBreakpoint() const +{ + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->NodeBreakpoint.IsAllowed(); + } + + return false; +} + +bool SFlowGraphEditor::CanRemovePinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + return GraphNode->PinBreakpoints.Contains(Pin); + } + } + + return false; +} + +void SFlowGraphEditor::OnEnableBreakpoint() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->NodeBreakpoint.EnableTrait(); + } +} + +void SFlowGraphEditor::OnEnablePinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + GraphNode->PinBreakpoints[Pin].EnableTrait(); + } + } +} + +bool SFlowGraphEditor::CanEnableBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + return GraphNode->PinBreakpoints.Contains(Pin); + } + } + + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->NodeBreakpoint.CanEnable(); + } + + return false; +} + +bool SFlowGraphEditor::CanEnablePinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + return GraphNode->PinBreakpoints.Contains(Pin) && GraphNode->PinBreakpoints[Pin].CanEnable(); + } + } + + return false; +} + +void SFlowGraphEditor::OnDisableBreakpoint() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->NodeBreakpoint.DisableTrait(); + } +} + +void SFlowGraphEditor::OnDisablePinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + GraphNode->PinBreakpoints[Pin].DisableTrait(); + } + } +} + +bool SFlowGraphEditor::CanDisableBreakpoint() const +{ + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->NodeBreakpoint.IsEnabled(); + } + + return false; +} + +bool SFlowGraphEditor::CanDisablePinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + return GraphNode->PinBreakpoints.Contains(Pin) && GraphNode->PinBreakpoints[Pin].IsEnabled(); + } + } + + return false; +} + +void SFlowGraphEditor::OnToggleBreakpoint() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->NodeBreakpoint.ToggleTrait(); + } +} + +void SFlowGraphEditor::OnTogglePinBreakpoint() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + GraphNode->PinBreakpoints.Add(Pin, FFlowPinTrait()); + GraphNode->PinBreakpoints[Pin].ToggleTrait(); + } + } +} + +bool SFlowGraphEditor::CanToggleBreakpoint() const +{ + return GetSelectedFlowNodes().Num() > 0; +} + +bool SFlowGraphEditor::CanTogglePinBreakpoint() +{ + return GetGraphPinForMenu() != nullptr; +} + +void SFlowGraphEditor::SetSignalMode(const EFlowSignalMode Mode) const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->SetSignalMode(Mode); + } + + FlowAsset->Modify(); +} + +bool SFlowGraphEditor::CanSetSignalMode(const EFlowSignalMode Mode) const +{ + if (IsPIE()) + { + return false; + } + + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->CanSetSignalMode(Mode); + } + + return false; +} + +void SFlowGraphEditor::OnForcePinActivation() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + GraphNode->ForcePinActivation(Pin); + } + } +} + +void SFlowGraphEditor::FocusViewport() const +{ + // Iterator used but should only contain one node + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + const UFlowNode* FlowNode = Cast(SelectedNode)->GetFlowNode(); + if (UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + { + if (AActor* ActorToFocus = NodeInstance->GetActorToFocus()) + { + GEditor->SelectNone(false, false, false); + GEditor->SelectActor(ActorToFocus, true, true, true); + GEditor->NoteSelectionChange(); + + GEditor->MoveViewportCamerasToActor(*ActorToFocus, false); + + const FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + const TSharedPtr LevelEditorTab = LevelEditorModule.GetLevelEditorInstanceTab().Pin(); + if (LevelEditorTab.IsValid()) + { + LevelEditorTab->DrawAttention(); + } + } + } + + return; + } +} + +bool SFlowGraphEditor::CanFocusViewport() const +{ + return GetSelectedFlowNodes().Num() == 1; +} + +void SFlowGraphEditor::JumpToNodeDefinition() const +{ + // Iterator used but should only contain one node + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->JumpToDefinition(); + return; + } +} + +bool SFlowGraphEditor::CanJumpToNodeDefinition() const +{ + return GetSelectedFlowNodes().Num() == 1; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Graph/FlowGraphEditorSettings.cpp b/Source/FlowEditor/Private/Graph/FlowGraphEditorSettings.cpp new file mode 100644 index 000000000..0ecffa9fc --- /dev/null +++ b/Source/FlowEditor/Private/Graph/FlowGraphEditorSettings.cpp @@ -0,0 +1,17 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/FlowGraphEditorSettings.h" + +UFlowGraphEditorSettings::UFlowGraphEditorSettings(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , NodeDoubleClickTarget(EFlowNodeDoubleClickTarget::PrimaryAsset) + , bShowNodeClass(false) + , bShowNodeDescriptionWhilePlaying(true) + , bEnforceFriendlyPinNames(false) + , bShowSubGraphPreview(true) + , bShowSubGraphPath(true) + , SubGraphPreviewSize(FVector2D(640.f, 360.f)) + , bHighlightInputWiresOfSelectedNodes(true) + , bHighlightOutputWiresOfSelectedNodes(false) +{ +} diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp index 7599caf26..5930e52e2 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp @@ -1,7 +1,10 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/FlowGraphSchema.h" -#include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphEditorSettings.h" #include "Graph/FlowGraphSchema_Actions.h" #include "Graph/FlowGraphSettings.h" #include "Graph/FlowGraphUtils.h" @@ -9,24 +12,24 @@ #include "FlowAsset.h" #include "Nodes/FlowNode.h" +#include "Nodes/FlowNodeBlueprint.h" +#include "Nodes/Route/FlowNode_CustomInput.h" #include "Nodes/Route/FlowNode_Start.h" +#include "Nodes/Route/FlowNode_Reroute.h" #include "AssetRegistryModule.h" -#include "Developer/ToolMenus/Public/ToolMenus.h" #include "EdGraph/EdGraph.h" -#include "Misc/HotReloadInterface.h" +#include "Editor.h" #include "ScopedTransaction.h" -#include "UObject/UObjectIterator.h" #define LOCTEXT_NAMESPACE "FlowGraphSchema" +bool UFlowGraphSchema::bInitialGatherPerformed = false; TArray UFlowGraphSchema::NativeFlowNodes; TMap UFlowGraphSchema::BlueprintFlowNodes; +TMap UFlowGraphSchema::GraphNodesByFlowNodes; -TSet UFlowGraphSchema::UnsortedCategories; -TArray> UFlowGraphSchema::FlowNodeCategories; - -TMap UFlowGraphSchema::AssignedGraphNodeClasses; +bool UFlowGraphSchema::bBlueprintCompilationPending; FFlowGraphSchemaRefresh UFlowGraphSchema::OnNodeListChanged; @@ -37,33 +40,32 @@ UFlowGraphSchema::UFlowGraphSchema(const FObjectInitializer& ObjectInitializer) void UFlowGraphSchema::SubscribeToAssetChanges() { - FAssetRegistryModule& AssetRegistry = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); - AssetRegistry.Get().OnFilesLoaded().AddStatic(&UFlowGraphSchema::GatherFlowNodes); + const FAssetRegistryModule& AssetRegistry = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); + AssetRegistry.Get().OnFilesLoaded().AddStatic(&UFlowGraphSchema::GatherNodes); AssetRegistry.Get().OnAssetAdded().AddStatic(&UFlowGraphSchema::OnAssetAdded); - AssetRegistry.Get().OnAssetRemoved().AddStatic(&UFlowGraphSchema::RemoveAsset); + AssetRegistry.Get().OnAssetRemoved().AddStatic(&UFlowGraphSchema::OnAssetRemoved); - IHotReloadInterface& HotReloadSupport = FModuleManager::LoadModuleChecked("HotReload"); - HotReloadSupport.OnHotReload().AddStatic(&UFlowGraphSchema::OnHotReload); + FCoreUObjectDelegates::ReloadCompleteDelegate.AddStatic(&UFlowGraphSchema::OnHotReload); if (GEditor) { - GEditor->OnBlueprintCompiled().AddStatic(&UFlowGraphSchema::GatherFlowNodes); - GEditor->OnClassPackageLoadedOrUnloaded().AddStatic(&UFlowGraphSchema::GatherFlowNodes); + GEditor->OnBlueprintPreCompile().AddStatic(&UFlowGraphSchema::OnBlueprintPreCompile); + GEditor->OnBlueprintCompiled().AddStatic(&UFlowGraphSchema::OnBlueprintCompiled); } } -void UFlowGraphSchema::GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const FString& CategoryName) +void UFlowGraphSchema::GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UClass* AssetClass, const FString& CategoryName) { - GetFlowNodeActions(ActionMenuBuilder, CategoryName); + GetFlowNodeActions(ActionMenuBuilder, AssetClass->GetDefaultObject(), CategoryName); GetCommentAction(ActionMenuBuilder); } void UFlowGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const { - GetFlowNodeActions(ContextMenuBuilder, FString()); + GetFlowNodeActions(ContextMenuBuilder, GetAssetClassDefaults(ContextMenuBuilder.CurrentGraph), FString()); GetCommentAction(ContextMenuBuilder, ContextMenuBuilder.CurrentGraph); - if (!ContextMenuBuilder.FromPin && FFlowGraphUtils::GetFlowAssetEditor(ContextMenuBuilder.CurrentGraph)->CanPasteNodes()) + if (!ContextMenuBuilder.FromPin && FFlowGraphUtils::GetFlowGraphEditor(ContextMenuBuilder.CurrentGraph)->CanPasteNodes()) { const TSharedPtr NewAction(new FFlowGraphSchemaAction_Paste(FText::GetEmpty(), LOCTEXT("PasteHereAction", "Paste here"), FText::GetEmpty(), 0)); ContextMenuBuilder.AddAction(NewAction); @@ -72,13 +74,42 @@ void UFlowGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextM void UFlowGraphSchema::CreateDefaultNodesForGraph(UEdGraph& Graph) const { + const UFlowAsset* AssetClassDefaults = GetAssetClassDefaults(&Graph); + static const FVector2D NodeOffsetIncrement = FVector2D(0, 128); + FVector2D NodeOffset = FVector2D::ZeroVector; + // Start node - UFlowGraphNode* NewGraphNode = FFlowGraphSchemaAction_NewNode::CreateNode(&Graph, nullptr, UFlowNode_Start::StaticClass(), FVector2D::ZeroVector); - SetNodeMetaData(NewGraphNode, FNodeMetadata::DefaultGraphNode); + CreateDefaultNode(Graph, AssetClassDefaults, UFlowNode_Start::StaticClass(), NodeOffset, AssetClassDefaults->bStartNodePlacedAsGhostNode); + + // Add default nodes for all of the CustomInputs + if (IsValid(AssetClassDefaults)) + { + for (const FName& CustomInputName : AssetClassDefaults->CustomInputs) + { + NodeOffset += NodeOffsetIncrement; + const UFlowGraphNode* NewFlowGraphNode = CreateDefaultNode(Graph, AssetClassDefaults, UFlowNode_CustomInput::StaticClass(), NodeOffset, true); + + UFlowNode_CustomInput* CustomInputNode = CastChecked(NewFlowGraphNode->GetFlowNode()); + CustomInputNode->SetEventName(CustomInputName); + } + } CastChecked(&Graph)->GetFlowAsset()->HarvestNodeConnections(); } +UFlowGraphNode* UFlowGraphSchema::CreateDefaultNode(UEdGraph& Graph, const UFlowAsset* AssetClassDefaults, const TSubclassOf& NodeClass, const FVector2D& Offset, const bool bPlacedAsGhostNode) +{ + UFlowGraphNode* NewGraphNode = FFlowGraphSchemaAction_NewNode::CreateNode(&Graph, nullptr, NodeClass, Offset); + SetNodeMetaData(NewGraphNode, FNodeMetadata::DefaultGraphNode); + + if (bPlacedAsGhostNode) + { + NewGraphNode->MakeAutomaticallyPlacedGhostNode(); + } + + return NewGraphNode; +} + const FPinConnectionResponse UFlowGraphSchema::CanCreateConnection(const UEdGraphPin* PinA, const UEdGraphPin* PinB) const { const UFlowGraphNode* OwningNodeA = Cast(PinA->GetOwningNodeUnchecked()); @@ -141,6 +172,40 @@ FLinearColor UFlowGraphSchema::GetPinTypeColor(const FEdGraphPinType& PinType) c return FLinearColor::White; } +FText UFlowGraphSchema::GetPinDisplayName(const UEdGraphPin* Pin) const +{ + FText ResultPinName; + check(Pin != nullptr); + if (Pin->PinFriendlyName.IsEmpty()) + { + // We don't want to display "None" for no name + if (Pin->PinName.IsNone()) + { + return FText::GetEmpty(); + } + if (GetDefault()->bEnforceFriendlyPinNames) // this option is only difference between this override and UEdGraphSchema::GetPinDisplayName + { + ResultPinName = FText::FromString(FName::NameToDisplayString(Pin->PinName.ToString(), true)); + } + else + { + ResultPinName = FText::FromName(Pin->PinName); + } + } + else + { + ResultPinName = Pin->PinFriendlyName; + + bool bShouldUseLocalizedNodeAndPinNames = false; + GConfig->GetBool(TEXT("Internationalization"), TEXT("ShouldUseLocalizedNodeAndPinNames"), bShouldUseLocalizedNodeAndPinNames, GEditorSettingsIni); + if (!bShouldUseLocalizedNodeAndPinNames) + { + ResultPinName = FText::FromString(ResultPinName.BuildSourceString()); + } + } + return ResultPinName; +} + void UFlowGraphSchema::BreakNodeLinks(UEdGraphNode& TargetNode) const { Super::BreakNodeLinks(TargetNode); @@ -167,7 +232,7 @@ void UFlowGraphSchema::BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNoti int32 UFlowGraphSchema::GetNodeSelectionCount(const UEdGraph* Graph) const { - return FFlowGraphUtils::GetFlowAssetEditor(Graph)->GetNumberOfSelectedNodes(); + return FFlowGraphUtils::GetFlowGraphEditor(Graph)->GetNumberOfSelectedNodes(); } TSharedPtr UFlowGraphSchema::GetCreateCommentAction() const @@ -175,47 +240,162 @@ TSharedPtr UFlowGraphSchema::GetCreateCommentAction() cons return TSharedPtr(static_cast(new FFlowGraphSchemaAction_NewComment)); } +void UFlowGraphSchema::OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2D& GraphPosition) const +{ + const FScopedTransaction Transaction(LOCTEXT("CreateFlowRerouteNodeOnWire", "Create Flow Reroute Node")); + + const FVector2D NodeSpacerSize(42.0f, 24.0f); + const FVector2D KnotTopLeft = GraphPosition - (NodeSpacerSize * 0.5f); + + UEdGraph* ParentGraph = PinA->GetOwningNode()->GetGraph(); + UFlowGraphNode* NewReroute = FFlowGraphSchemaAction_NewNode::CreateNode(ParentGraph, nullptr, UFlowNode_Reroute::StaticClass(), KnotTopLeft, false); + + PinA->BreakLinkTo(PinB); + PinA->MakeLinkTo((PinA->Direction == EGPD_Output) ? NewReroute->InputPins[0] : NewReroute->OutputPins[0]); + PinB->MakeLinkTo((PinB->Direction == EGPD_Output) ? NewReroute->InputPins[0] : NewReroute->OutputPins[0]); +} + TArray> UFlowGraphSchema::GetFlowNodeCategories() { - if (FlowNodeCategories.Num() == 0) + if (!bInitialGatherPerformed) { - GatherFlowNodes(); + GatherNodes(); } - return FlowNodeCategories; + TSet UnsortedCategories; + for (const UClass* FlowNodeClass : NativeFlowNodes) + { + if (const UFlowNode* DefaultObject = FlowNodeClass->GetDefaultObject()) + { + UnsortedCategories.Emplace(DefaultObject->GetNodeCategory()); + } + } + + for (const TPair& AssetData : BlueprintFlowNodes) + { + if (const UBlueprint* Blueprint = GetPlaceableNodeBlueprint(AssetData.Value)) + { + UnsortedCategories.Emplace(Blueprint->BlueprintCategory); + } + } + + TArray SortedCategories = UnsortedCategories.Array(); + SortedCategories.Sort(); + + // create list of categories + TArray> Result; + for (const FString& Category : SortedCategories) + { + if (!Category.IsEmpty()) + { + Result.Emplace(MakeShareable(new FString(Category))); + } + } + + return Result; } UClass* UFlowGraphSchema::GetAssignedGraphNodeClass(const UClass* FlowNodeClass) { - if (UClass* AssignedGraphNode = AssignedGraphNodeClasses.FindRef(FlowNodeClass)) + TArray FoundParentClasses; + UClass* ReturnClass = nullptr; + + // Collect all possible parents and their corresponding GraphNodeClasses + for (const TPair& GraphNodeByFlowNode : GraphNodesByFlowNodes) + { + if (FlowNodeClass == GraphNodeByFlowNode.Key) + { + return GraphNodeByFlowNode.Value; + } + + if (FlowNodeClass->IsChildOf(GraphNodeByFlowNode.Key)) + { + FoundParentClasses.Add(GraphNodeByFlowNode.Key); + } + } + + // Of only one parent found set the return to its GraphNodeClass + if (FoundParentClasses.Num() == 1) + { + ReturnClass = GraphNodesByFlowNodes.FindRef(FoundParentClasses[0]); + } + // If multiple parents found, find the closest one and set the return to its GraphNodeClass + else if (FoundParentClasses.Num() > 1) { - return AssignedGraphNode; + TPair ClosestParentMatch = {1000, nullptr}; + for (const auto& ParentClass : FoundParentClasses) + { + int32 StepsTillExactMatch = 0; + const UClass* LocalParentClass = FlowNodeClass; + + while (IsValid(LocalParentClass) && LocalParentClass != ParentClass && LocalParentClass != UFlowNode::StaticClass()) + { + StepsTillExactMatch++; + LocalParentClass = LocalParentClass->GetSuperClass(); + } + + if (StepsTillExactMatch != 0 && StepsTillExactMatch < ClosestParentMatch.Key) + { + ClosestParentMatch = {StepsTillExactMatch, ParentClass}; + } + } + + ReturnClass = GraphNodesByFlowNodes.FindRef(ClosestParentMatch.Value); } - return UFlowGraphNode::StaticClass(); + return IsValid(ReturnClass) ? ReturnClass : UFlowGraphNode::StaticClass(); } -void UFlowGraphSchema::GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const FString& CategoryName) +void UFlowGraphSchema::ApplyNodeFilter(const UFlowAsset* AssetClassDefaults, const UClass* FlowNodeClass, TArray& FilteredNodes) { - if (NativeFlowNodes.Num() == 0) + if (FlowNodeClass == nullptr) { - GatherFlowNodes(); + return; } - TArray FlowNodes; - FlowNodes.Reserve(NativeFlowNodes.Num() + BlueprintFlowNodes.Num()); + if (AssetClassDefaults == nullptr) + { + return; + } - for (UClass* FlowNodeClass : NativeFlowNodes) + if (!AssetClassDefaults->IsNodeClassAllowed(FlowNodeClass)) { - FlowNodes.Emplace(FlowNodeClass->GetDefaultObject()); + return; } - for (const TPair& AssetData : BlueprintFlowNodes) + + UFlowNode* NodeDefaults = FlowNodeClass->GetDefaultObject(); + FilteredNodes.Emplace(NodeDefaults); +} + +void UFlowGraphSchema::GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* AssetClassDefaults, const FString& CategoryName) +{ + if (!bInitialGatherPerformed) { - UBlueprint* Blueprint = GetNodeBlueprint(AssetData.Value); - FlowNodes.Emplace(Blueprint->GeneratedClass->GetDefaultObject()); + GatherNodes(); + } + + // Flow Asset type might limit which nodes are placeable + TArray FilteredNodes; + { + FilteredNodes.Reserve(NativeFlowNodes.Num() + BlueprintFlowNodes.Num()); + + for (const UClass* FlowNodeClass : NativeFlowNodes) + { + ApplyNodeFilter(AssetClassDefaults, FlowNodeClass, FilteredNodes); + } + + for (const TPair& AssetData : BlueprintFlowNodes) + { + if (const UBlueprint* Blueprint = GetPlaceableNodeBlueprint(AssetData.Value)) + { + ApplyNodeFilter(AssetClassDefaults, Blueprint->GeneratedClass, FilteredNodes); + } + } + + FilteredNodes.Shrink(); } - for (const UFlowNode* FlowNode : FlowNodes) + for (const UFlowNode* FlowNode : FilteredNodes) { if ((CategoryName.IsEmpty() || CategoryName.Equals(FlowNode->GetNodeCategory())) && !UFlowGraphSettings::Get()->NodesHiddenFromPalette.Contains(FlowNode->GetClass())) { @@ -229,7 +409,7 @@ void UFlowGraphSchema::GetCommentAction(FGraphActionMenuBuilder& ActionMenuBuild { if (!ActionMenuBuilder.FromPin) { - const bool bIsManyNodesSelected = CurrentGraph ? (FFlowGraphUtils::GetFlowAssetEditor(CurrentGraph)->GetNumberOfSelectedNodes() > 0) : false; + const bool bIsManyNodesSelected = CurrentGraph ? (FFlowGraphUtils::GetFlowGraphEditor(CurrentGraph)->GetNumberOfSelectedNodes() > 0) : false; const FText MenuDescription = bIsManyNodesSelected ? LOCTEXT("CreateCommentAction", "Create Comment from Selection") : LOCTEXT("AddCommentAction", "Add Comment..."); const FText ToolTip = LOCTEXT("CreateCommentToolTip", "Creates a comment."); @@ -240,67 +420,101 @@ void UFlowGraphSchema::GetCommentAction(FGraphActionMenuBuilder& ActionMenuBuild bool UFlowGraphSchema::IsFlowNodePlaceable(const UClass* Class) { - return !Class->HasAnyClassFlags(CLASS_Abstract) && !Class->HasAnyClassFlags(CLASS_NotPlaceable) && !Class->HasAnyClassFlags(CLASS_Deprecated); + if (Class->HasAnyClassFlags(CLASS_Abstract | CLASS_NotPlaceable | CLASS_Deprecated)) + { + return false; + } + + if (const UFlowNode* DefaultObject = Class->GetDefaultObject()) + { + return !DefaultObject->bNodeDeprecated; + } + + return true; } -void UFlowGraphSchema::GatherFlowNodes() +void UFlowGraphSchema::OnBlueprintPreCompile(UBlueprint* Blueprint) { - if (GEditor && GEditor->PlayWorld) + if (Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->IsChildOf(UFlowNode::StaticClass())) { - // prevent heavy asset crunching during PIE - return; + bBlueprintCompilationPending = true; + } +} + +void UFlowGraphSchema::OnBlueprintCompiled() +{ + if (bBlueprintCompilationPending) + { + GatherNodes(); } + bBlueprintCompilationPending = false; +} + +void UFlowGraphSchema::OnHotReload(EReloadCompleteReason ReloadCompleteReason) +{ + GatherNodes(); +} + +void UFlowGraphSchema::GatherNativeNodes() +{ // collect C++ nodes once per editor session - if (NativeFlowNodes.Num() == 0) + if (NativeFlowNodes.Num() > 0) { - for (TObjectIterator It; It; ++It) + return; + } + + TArray FlowNodes; + GetDerivedClasses(UFlowNode::StaticClass(), FlowNodes); + for (UClass* Class : FlowNodes) + { + if (Class->ClassGeneratedBy == nullptr && IsFlowNodePlaceable(Class)) { - if (It->IsChildOf(UFlowNode::StaticClass())) - { - if (It->ClassGeneratedBy == nullptr && IsFlowNodePlaceable(*It)) - { - NativeFlowNodes.Emplace(*It); + NativeFlowNodes.Emplace(Class); + } + } - const UFlowNode* DefaultObject = It->GetDefaultObject(); - UnsortedCategories.Emplace(DefaultObject->GetNodeCategory()); - } - } - else if (It->IsChildOf(UFlowGraphNode::StaticClass())) + TArray GraphNodes; + GetDerivedClasses(UFlowGraphNode::StaticClass(), GraphNodes); + for (UClass* GraphNodeClass : GraphNodes) + { + const UFlowGraphNode* GraphNodeCDO = GraphNodeClass->GetDefaultObject(); + for (UClass* AssignedClass : GraphNodeCDO->AssignedNodeClasses) + { + if (AssignedClass->IsChildOf(UFlowNode::StaticClass())) { - const UFlowGraphNode* DefaultObject = It->GetDefaultObject(); - for (UClass* AssignedClass : DefaultObject->AssignedNodeClasses) - { - if (AssignedClass->IsChildOf(UFlowNode::StaticClass())) - { - AssignedGraphNodeClasses.Emplace(AssignedClass, *It); - } - } + GraphNodesByFlowNodes.Emplace(AssignedClass, GraphNodeClass); } } } +} - // retrieve all blueprint nodes - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); +void UFlowGraphSchema::GatherNodes() +{ + // prevent asset crunching during PIE + if (GEditor && GEditor->PlayWorld) + { + return; + } + + bInitialGatherPerformed = true; + GatherNativeNodes(); + + // retrieve all blueprint nodes FARFilter Filter; - Filter.ClassNames.Add(UBlueprint::StaticClass()->GetFName()); - Filter.ClassNames.Add(UBlueprintGeneratedClass::StaticClass()->GetFName()); + Filter.ClassNames.Add(UFlowNodeBlueprint::StaticClass()->GetFName()); Filter.bRecursiveClasses = true; TArray FoundAssets; + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); AssetRegistryModule.Get().GetAssets(Filter, FoundAssets); for (const FAssetData& AssetData : FoundAssets) { AddAsset(AssetData, true); } - RefreshNodeList(); -} - -void UFlowGraphSchema::OnHotReload(bool bWasTriggeredAutomatically) -{ - GatherFlowNodes(); + OnNodeListChanged.Broadcast(); } void UFlowGraphSchema::OnAssetAdded(const FAssetData& AssetData) @@ -312,76 +526,59 @@ void UFlowGraphSchema::AddAsset(const FAssetData& AssetData, const bool bBatch) { if (!BlueprintFlowNodes.Contains(AssetData.PackageName)) { - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); if (AssetRegistryModule.Get().IsLoadingAssets()) { return; } - TArray AncestorClassNames; - AssetRegistryModule.Get().GetAncestorClassNames(AssetData.AssetClass, AncestorClassNames); - if (!AncestorClassNames.Contains(UBlueprintCore::StaticClass()->GetFName())) - { - return; - } - - FString NativeParentClassPath; - AssetData.GetTagValue(FBlueprintTags::NativeParentClassPath, NativeParentClassPath); - if (!NativeParentClassPath.IsEmpty()) + if (AssetData.GetClass()->IsChildOf(UFlowNodeBlueprint::StaticClass())) { - UObject* Outer = nullptr; - ResolveName(Outer, NativeParentClassPath, false, false); - UClass* NativeParentClass = FindObject(ANY_PACKAGE, *NativeParentClassPath); + BlueprintFlowNodes.Emplace(AssetData.PackageName, AssetData); - // accept only Flow Node blueprints - if (NativeParentClass && NativeParentClass->IsChildOf(UFlowNode::StaticClass())) + if (!bBatch) { - UBlueprint* Blueprint = GetNodeBlueprint(AssetData); - if (Blueprint && IsFlowNodePlaceable(Blueprint->GeneratedClass)) - { - BlueprintFlowNodes.Emplace(AssetData.PackageName, AssetData); - UnsortedCategories.Emplace(Blueprint->BlueprintCategory); - - if (!bBatch) - { - RefreshNodeList(); - } - } + OnNodeListChanged.Broadcast(); } } } } -void UFlowGraphSchema::RemoveAsset(const FAssetData& AssetData) +void UFlowGraphSchema::OnAssetRemoved(const FAssetData& AssetData) { if (BlueprintFlowNodes.Contains(AssetData.PackageName)) { BlueprintFlowNodes.Remove(AssetData.PackageName); BlueprintFlowNodes.Shrink(); - RefreshNodeList(); + OnNodeListChanged.Broadcast(); } } -void UFlowGraphSchema::RefreshNodeList() +UBlueprint* UFlowGraphSchema::GetPlaceableNodeBlueprint(const FAssetData& AssetData) { - // sort categories - TArray SortedCategories = UnsortedCategories.Array(); - SortedCategories.Sort(); - - // create list of categories - FlowNodeCategories.Empty(); - for (const FString& Category : SortedCategories) + UBlueprint* Blueprint = Cast(AssetData.GetAsset()); + if (Blueprint && IsFlowNodePlaceable(Blueprint->GeneratedClass)) { - FlowNodeCategories.Emplace(MakeShareable(new FString(Category))); + return Blueprint; } - OnNodeListChanged.Broadcast(); + return nullptr; } -UBlueprint* UFlowGraphSchema::GetNodeBlueprint(const FAssetData& AssetData) +const UFlowAsset* UFlowGraphSchema::GetAssetClassDefaults(const UEdGraph* Graph) { - return Cast(StaticLoadObject(AssetData.GetClass(), /*Outer =*/nullptr, *AssetData.ObjectPath.ToString(), nullptr, LOAD_NoWarn | LOAD_DisableCompileOnLoad)); + const UClass* AssetClass = UFlowAsset::StaticClass(); + + if (Graph) + { + if (const UFlowAsset* FlowAsset = Graph->GetTypedOuter()) + { + AssetClass = FlowAsset->GetClass(); + } + } + + return AssetClass->GetDefaultObject(); } #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp index ce8152c6c..fcc34b9b9 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp @@ -1,7 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/FlowGraphSchema_Actions.h" -#include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" #include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphUtils.h" #include "Graph/Nodes/FlowGraphNode.h" @@ -9,11 +11,9 @@ #include "FlowAsset.h" #include "Nodes/FlowNode.h" -#include "Developer/ToolMenus/Public/ToolMenus.h" #include "EdGraph/EdGraph.h" #include "EdGraphNode_Comment.h" #include "Editor.h" -#include "Layout/SlateRect.h" #include "ScopedTransaction.h" #define LOCTEXT_NAMESPACE "FlowGraphSchema_Actions" @@ -37,7 +37,7 @@ UEdGraphNode* FFlowGraphSchemaAction_NewNode::PerformAction(class UEdGraph* Pare return nullptr; } -UFlowGraphNode* FFlowGraphSchemaAction_NewNode::CreateNode(UEdGraph* ParentGraph, UEdGraphPin* FromPin, UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode /*= true*/) +UFlowGraphNode* FFlowGraphSchemaAction_NewNode::CreateNode(UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode /*= true*/) { check(NodeClass); @@ -52,42 +52,155 @@ UFlowGraphNode* FFlowGraphSchemaAction_NewNode::CreateNode(UEdGraph* ParentGraph UFlowAsset* FlowAsset = CastChecked(ParentGraph)->GetFlowAsset(); FlowAsset->Modify(); + // create new Flow Graph node const UClass* GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(NodeClass); UFlowGraphNode* NewGraphNode = NewObject(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional); + + // register to the graph NewGraphNode->CreateNewGuid(); + ParentGraph->AddNode(NewGraphNode, false, bSelectNewNode); + + // link editor and runtime nodes together + UFlowNode* FlowNode = FlowAsset->CreateNode(NodeClass, NewGraphNode); + NewGraphNode->SetNodeTemplate(FlowNode); + // create pins and connections + NewGraphNode->AllocateDefaultPins(); + NewGraphNode->AutowireNewNode(FromPin); + + // set position NewGraphNode->NodePosX = Location.X; NewGraphNode->NodePosY = Location.Y; - ParentGraph->AddNode(NewGraphNode, false, bSelectNewNode); - - UFlowNode* NewNode = FlowAsset->CreateNode(NodeClass, NewGraphNode); - NewGraphNode->SetFlowNode(NewNode); + // call notifies NewGraphNode->PostPlacedNewNode(); + ParentGraph->NotifyGraphChanged(); + + FlowAsset->PostEditChange(); + + // select in editor UI + if (bSelectNewNode) + { + const TSharedPtr FlowGraphEditor = FFlowGraphUtils::GetFlowGraphEditor(ParentGraph); + if (FlowGraphEditor.IsValid()) + { + FlowGraphEditor->SelectSingleNode(NewGraphNode); + } + } + + return NewGraphNode; +} + +UFlowGraphNode* FFlowGraphSchemaAction_NewNode::RecreateNode(UEdGraph* ParentGraph, UEdGraphNode* OldInstance, UFlowNode* FlowNode) +{ + check(FlowNode); + + ParentGraph->Modify(); + + UFlowAsset* FlowAsset = CastChecked(ParentGraph)->GetFlowAsset(); + FlowAsset->Modify(); + + // create new Flow Graph node + const UClass* GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(FlowNode->GetClass()); + UFlowGraphNode* NewGraphNode = NewObject(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional); + + // register to the graph + NewGraphNode->NodeGuid = FlowNode->GetGuid(); + ParentGraph->AddNode(NewGraphNode, false, false); + + // link editor and runtime nodes together + FlowNode->SetGraphNode(NewGraphNode); + NewGraphNode->SetNodeTemplate(FlowNode); + + // move links from the old node NewGraphNode->AllocateDefaultPins(); + if (OldInstance) + { + for (UEdGraphPin* OldPin : OldInstance->Pins) + { + if (OldPin->LinkedTo.Num() == 0) + { + continue; + } + + for (UEdGraphPin* NewPin : NewGraphNode->Pins) + { + if (NewPin->Direction == OldPin->Direction && NewPin->PinName == OldPin->PinName) + { + TArray Connections = OldPin->LinkedTo; + for (UEdGraphPin* ConnectedPin : Connections) + { + ConnectedPin->BreakLinkTo(OldPin); + ConnectedPin->MakeLinkTo(NewPin); + } + } + } + } + } - NewGraphNode->AutowireNewNode(FromPin); - + // keep old position + NewGraphNode->NodePosX = OldInstance ? OldInstance->NodePosX : 0; + NewGraphNode->NodePosY = OldInstance ? OldInstance->NodePosY : 0; + + // remove leftover + if (OldInstance) + { + OldInstance->DestroyNode(); + } + + // call notifies + NewGraphNode->PostPlacedNewNode(); ParentGraph->NotifyGraphChanged(); - const TSharedPtr FlowEditor = FFlowGraphUtils::GetFlowAssetEditor(ParentGraph); - if (FlowEditor.IsValid()) + return NewGraphNode; +} + +UFlowGraphNode* FFlowGraphSchemaAction_NewNode::ImportNode(UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FGuid& NodeGuid, const FVector2D Location) +{ + check(NodeClass); + + ParentGraph->Modify(); + if (FromPin) { - FlowEditor->SelectSingleNode(NewGraphNode); + FromPin->Modify(); } - FlowAsset->PostEditChange(); - FlowAsset->MarkPackageDirty(); + UFlowAsset* FlowAsset = CastChecked(ParentGraph)->GetFlowAsset(); + FlowAsset->Modify(); + + // create new Flow Graph node + const UClass* GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(NodeClass); + UFlowGraphNode* NewGraphNode = NewObject(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional); + + // register to the graph + NewGraphNode->NodeGuid = NodeGuid; + ParentGraph->AddNode(NewGraphNode, false, false); + + // link editor and runtime nodes together + UFlowNode* FlowNode = FlowAsset->CreateNode(NodeClass, NewGraphNode); + NewGraphNode->SetNodeTemplate(FlowNode); + + // create pins and connections + NewGraphNode->AllocateDefaultPins(); + NewGraphNode->AutowireNewNode(FromPin); + + // set position + NewGraphNode->NodePosX = Location.X; + NewGraphNode->NodePosY = Location.Y; + + // call notifies + NewGraphNode->PostPlacedNewNode(); + ParentGraph->NotifyGraphChanged(); return NewGraphNode; } -UEdGraphNode* FFlowGraphSchemaAction_Paste::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode/* = true*/) +UEdGraphNode* FFlowGraphSchemaAction_Paste::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, const bool bSelectNewNode/* = true*/) { // prevent adding new nodes while playing if (GEditor->PlayWorld == nullptr) { - FFlowGraphUtils::GetFlowAssetEditor(ParentGraph)->PasteNodesHere(Location); + FFlowGraphUtils::GetFlowGraphEditor(ParentGraph)->PasteNodesHere(Location); } return nullptr; @@ -96,7 +209,7 @@ UEdGraphNode* FFlowGraphSchemaAction_Paste::PerformAction(class UEdGraph* Parent ///////////////////////////////////////////////////// // Comment Node -UEdGraphNode* FFlowGraphSchemaAction_NewComment::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode/* = true*/) +UEdGraphNode* FFlowGraphSchemaAction_NewComment::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, const bool bSelectNewNode/* = true*/) { // prevent adding new nodes while playing if (GEditor->PlayWorld != nullptr) @@ -107,12 +220,16 @@ UEdGraphNode* FFlowGraphSchemaAction_NewComment::PerformAction(class UEdGraph* P UEdGraphNode_Comment* CommentTemplate = NewObject(); FVector2D SpawnLocation = Location; - FSlateRect Bounds; - if (FFlowGraphUtils::GetFlowAssetEditor(ParentGraph)->GetBoundsForSelectedNodes(Bounds, 50.0f)) + const TSharedPtr FlowGraphEditor = FFlowGraphUtils::GetFlowGraphEditor(ParentGraph); + if (FlowGraphEditor.IsValid()) { - CommentTemplate->SetBounds(Bounds); - SpawnLocation.X = CommentTemplate->NodePosX; - SpawnLocation.Y = CommentTemplate->NodePosY; + FSlateRect Bounds; + if (FlowGraphEditor->GetBoundsForSelectedNodes(Bounds, 50.0f)) + { + CommentTemplate->SetBounds(Bounds); + SpawnLocation.X = CommentTemplate->NodePosX; + SpawnLocation.Y = CommentTemplate->NodePosY; + } } return FEdGraphSchemaAction_NewNode::SpawnNodeFromTemplate(ParentGraph, CommentTemplate, SpawnLocation); diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp index 623695d38..7c54658fb 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp @@ -1,12 +1,27 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/FlowGraphSettings.h" +#include "FlowAsset.h" + +#define LOCTEXT_NAMESPACE "FlowGraphSettings" + UFlowGraphSettings::UFlowGraphSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , bExposeFlowAssetCreation(true) + , bExposeFlowNodeCreation(true) + , bShowAssetToolbarAboveLevelEditor(true) + , FlowAssetCategoryName(LOCTEXT("FlowAssetCategory", "Flow")) + , DefaultFlowAssetClass(UFlowAsset::StaticClass()) + , WorldAssetClass(UFlowAsset::StaticClass()) , bShowDefaultPinNames(false) , ExecPinColorModifier(0.75f, 0.75f, 0.75f, 1.0f) , NodeDescriptionBackground(FLinearColor(0.0625f, 0.0625f, 0.0625f, 1.0f)) , NodeStatusBackground(FLinearColor(0.12f, 0.12f, 0.12f, 1.0f)) , NodePreloadedBackground(FLinearColor(0.12f, 0.12f, 0.12f, 1.0f)) + , ConnectionDrawType(EFlowConnectionDrawType::Default) + , CircuitConnectionAngle(45.f) + , CircuitConnectionSpacing(FVector2D(30.f)) , InactiveWireColor(FLinearColor(0.364f, 0.364f, 0.364f, 1.0f)) , InactiveWireThickness(1.5f) , RecentWireDuration(3.0f) @@ -14,8 +29,6 @@ UFlowGraphSettings::UFlowGraphSettings(const FObjectInitializer& ObjectInitializ , RecentWireThickness(6.0f) , RecordedWireColor(FLinearColor(0.432f, 0.258f, 0.096f, 1.0f)) , RecordedWireThickness(3.5f) - , bHighlightInputWiresOfSelectedNodes(true) - , bHighlightOutputWiresOfSelectedNodes(false) , SelectedWireColor(FLinearColor(0.984f, 0.482f, 0.010f, 1.0f)) , SelectedWireThickness(1.5f) { @@ -26,3 +39,5 @@ UFlowGraphSettings::UFlowGraphSettings(const FObjectInitializer& ObjectInitializ NodeTitleColors.Emplace(EFlowNodeStyle::Logic, FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)); NodeTitleColors.Emplace(EFlowNodeStyle::SubGraph, FLinearColor(1.0f, 0.128f, 0.0f, 1.0f)); } + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp b/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp index c84930f31..468ba353f 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/FlowGraphUtils.h" #include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" @@ -6,12 +8,12 @@ #include "Toolkits/ToolkitManager.h" -TSharedPtr FFlowGraphUtils::GetFlowAssetEditor(const UObject* ObjectToFocusOn) +TSharedPtr FFlowGraphUtils::GetFlowAssetEditor(const UEdGraph* Graph) { - check(ObjectToFocusOn); + check(Graph); TSharedPtr FlowAssetEditor; - if (UFlowAsset* FlowAsset = Cast(ObjectToFocusOn)->GetFlowAsset()) + if (const UFlowAsset* FlowAsset = Cast(Graph)->GetFlowAsset()) { const TSharedPtr FoundAssetEditor = FToolkitManager::Get().FindEditorForAsset(FlowAsset); if (FoundAssetEditor.IsValid()) @@ -21,3 +23,16 @@ TSharedPtr FFlowGraphUtils::GetFlowAssetEditor(const UObject* } return FlowAssetEditor; } + +TSharedPtr FFlowGraphUtils::GetFlowGraphEditor(const UEdGraph* Graph) +{ + TSharedPtr FlowGraphEditor; + + const TSharedPtr FlowEditor = GetFlowAssetEditor(Graph); + if (FlowEditor.IsValid()) + { + FlowGraphEditor = FlowEditor->GetFlowGraph(); + } + + return FlowGraphEditor; +} diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index 5d9ee7556..a6862c809 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -1,8 +1,11 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Nodes/FlowGraphNode.h" -#include "Asset/FlowDebugger.h" +#include "Asset/FlowDebuggerSubsystem.h" #include "FlowEditorCommands.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditorSettings.h" #include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphSettings.h" #include "Graph/Widgets/SFlowGraphNode.h" @@ -10,7 +13,6 @@ #include "FlowAsset.h" #include "Nodes/FlowNode.h" -#include "AssetRegistryModule.h" #include "Developer/ToolMenus/Public/ToolMenus.h" #include "EdGraph/EdGraphSchema.h" #include "EdGraphSchema_K2.h" @@ -18,107 +20,41 @@ #include "Editor/EditorEngine.h" #include "Framework/Commands/GenericCommands.h" #include "GraphEditorActions.h" +#include "HAL/FileManager.h" #include "Kismet2/KismetEditorUtilities.h" -#include "Modules/ModuleManager.h" #include "ScopedTransaction.h" #include "SourceCodeNavigation.h" #include "Textures/SlateIcon.h" #include "ToolMenuSection.h" -#include "UnrealEd.h" #define LOCTEXT_NAMESPACE "FlowGraphNode" -////////////////////////////////////////////////////////////////////////// -// Flow Breakpoint - -void FFlowBreakpoint::AddBreakpoint() -{ - if (!bHasBreakpoint) - { - bHasBreakpoint = true; - bBreakpointEnabled = true; - } -} - -void FFlowBreakpoint::RemoveBreakpoint() -{ - if (bHasBreakpoint) - { - bHasBreakpoint = false; - bBreakpointEnabled = false; - } -} - -bool FFlowBreakpoint::HasBreakpoint() const -{ - return bHasBreakpoint; -} - -void FFlowBreakpoint::EnableBreakpoint() -{ - if (bHasBreakpoint && !bBreakpointEnabled) - { - bBreakpointEnabled = true; - } -} - -bool FFlowBreakpoint::CanEnableBreakpoint() const -{ - return bHasBreakpoint && !bBreakpointEnabled; -} - -void FFlowBreakpoint::DisableBreakpoint() -{ - if (bHasBreakpoint && bBreakpointEnabled) - { - bBreakpointEnabled = false; - } -} - -bool FFlowBreakpoint::IsBreakpointEnabled() const -{ - return bHasBreakpoint && bBreakpointEnabled; -} - -void FFlowBreakpoint::ToggleBreakpoint() -{ - if (bHasBreakpoint) - { - bHasBreakpoint = false; - bBreakpointEnabled = false; - } - else - { - bHasBreakpoint = true; - bBreakpointEnabled = true; - } -} - -bool UFlowGraphNode::bFlowAssetsLoaded = false; - -////////////////////////////////////////////////////////////////////////// -// Flow Graph Node - UFlowGraphNode::UFlowGraphNode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , FlowNode(nullptr) + , bBlueprintCompilationPending(false) , bNeedsFullReconstruction(false) { OrphanedPinSaveMode = ESaveOrphanPinMode::SaveAll; } -void UFlowGraphNode::SetFlowNode(UFlowNode* InFlowNode) +void UFlowGraphNode::SetNodeTemplate(UFlowNode* InFlowNode) { FlowNode = InFlowNode; } +const UFlowNode* UFlowGraphNode::GetNodeTemplate() const +{ + return FlowNode; +} + UFlowNode* UFlowGraphNode::GetFlowNode() const { if (FlowNode) { - if (UFlowAsset* InspectedInstance = FlowNode->GetFlowAsset()->GetInspectedInstance()) + if (const UFlowAsset* InspectedInstance = FlowNode->GetFlowAsset()->GetInspectedInstance()) { - return InspectedInstance->GetNodeInstance(FlowNode->GetGuid()); + return InspectedInstance->GetNode(FlowNode->GetGuid()); } return FlowNode; @@ -137,24 +73,6 @@ void UFlowGraphNode::PostLoad() SubscribeToExternalChanges(); } - // todo: verify if we still need this workaround - // without this reconstructing UFlowNode_SubGraph pins wouldn't work well - if (bFlowAssetsLoaded == false) - { - TArray FlowAssets; - - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); - AssetRegistryModule.Get().GetAssetsByClass(UFlowAsset::StaticClass()->GetFName(), FlowAssets, true); - - for (FAssetData const& Asset : FlowAssets) - { - const FString AssetPath = Asset.ObjectPath.ToString(); - StaticLoadObject(Asset.GetClass(), nullptr, *AssetPath); - } - - bFlowAssetsLoaded = true; - } - ReconstructNode(); } @@ -225,12 +143,30 @@ void UFlowGraphNode::SubscribeToExternalChanges() // blueprint nodes if (FlowNode->GetClass()->ClassGeneratedBy && GEditor) { - GEditor->OnBlueprintCompiled().AddUObject(this, &UFlowGraphNode::OnExternalChange); - GEditor->OnClassPackageLoadedOrUnloaded().AddUObject(this, &UFlowGraphNode::OnExternalChange); + GEditor->OnBlueprintPreCompile().AddUObject(this, &UFlowGraphNode::OnBlueprintPreCompile); + GEditor->OnBlueprintCompiled().AddUObject(this, &UFlowGraphNode::OnBlueprintCompiled); } } } +void UFlowGraphNode::OnBlueprintPreCompile(UBlueprint* Blueprint) +{ + if (Blueprint && Blueprint == FlowNode->GetClass()->ClassGeneratedBy) + { + bBlueprintCompilationPending = true; + } +} + +void UFlowGraphNode::OnBlueprintCompiled() +{ + if (bBlueprintCompilationPending) + { + OnExternalChange(); + } + + bBlueprintCompilationPending = false; +} + void UFlowGraphNode::OnExternalChange() { bNeedsFullReconstruction = true; @@ -239,6 +175,11 @@ void UFlowGraphNode::OnExternalChange() GetGraph()->NotifyGraphChanged(); } +void UFlowGraphNode::OnGraphRefresh() +{ + RefreshContextPins(true); +} + bool UFlowGraphNode::CanCreateUnderSpecifiedSchema(const UEdGraphSchema* Schema) const { return Schema->IsA(UFlowGraphSchema::StaticClass()); @@ -375,7 +316,7 @@ void UFlowGraphNode::RewireOldPinsToNewPins(TArray& InOldPins) // NOTE: we iterate backwards through the list because ReconstructSinglePin() // destroys pins as we go along (clearing out parent pointers, etc.); // we need the parent pin chain intact for DoPinsMatchForReconstruction(); - // we want to destroy old pins from the split children (leafs) up, so + // we want to destroy old pins from the split children (leaves) up, so // we do this since split child pins are ordered later in the list // (after their parents) for (int32 OldPinIndex = InOldPins.Num() - 1; OldPinIndex >= 0; --OldPinIndex) @@ -440,7 +381,7 @@ void UFlowGraphNode::ReconstructSinglePin(UEdGraphPin* NewPin, UEdGraphPin* OldP NewPin->MovePersistentDataFromOldPin(*OldPin); // Update the in breakpoints as the old pin will be going the way of the dodo - for (TPair& PinBreakpoint : PinBreakpoints) + for (TPair& PinBreakpoint : PinBreakpoints) { if (PinBreakpoint.Key.Get() == OldPin) { @@ -459,7 +400,7 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra if (Context->Pin) { { - FToolMenuSection& Section = Menu->AddSection("FlowGraphSchemaPinActions", LOCTEXT("PinActionsMenuHeader", "Pin Actions")); + FToolMenuSection& Section = Menu->AddSection("FlowGraphPinActions", LOCTEXT("PinActionsMenuHeader", "Pin Actions")); if (Context->Pin->LinkedTo.Num() > 0) { Section.AddMenuEntry(GraphCommands.BreakPinLinks); @@ -476,18 +417,23 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra } { - FToolMenuSection& Section = Menu->AddSection("FlowGraphNodePinBreakpoints", LOCTEXT("PinBreakpointsMenuHeader", "Pin Breakpoints")); + FToolMenuSection& Section = Menu->AddSection("FlowGraphPinBreakpoints", LOCTEXT("PinBreakpointsMenuHeader", "Pin Breakpoints")); Section.AddMenuEntry(FlowGraphCommands.AddPinBreakpoint); Section.AddMenuEntry(FlowGraphCommands.RemovePinBreakpoint); Section.AddMenuEntry(FlowGraphCommands.EnablePinBreakpoint); Section.AddMenuEntry(FlowGraphCommands.DisablePinBreakpoint); Section.AddMenuEntry(FlowGraphCommands.TogglePinBreakpoint); } + + { + FToolMenuSection& Section = Menu->AddSection("FlowGraphPinExecutionOverride", LOCTEXT("PinExecutionOverrideMenuHeader", "Execution Override")); + Section.AddMenuEntry(FlowGraphCommands.ForcePinActivation); + } } else if (Context->Node) { { - FToolMenuSection& Section = Menu->AddSection("FlowGraphSchemaNodeActions", LOCTEXT("NodeActionsMenuHeader", "Node Actions")); + FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeActions", LOCTEXT("NodeActionsMenuHeader", "Node Actions")); Section.AddMenuEntry(GenericCommands.Delete); Section.AddMenuEntry(GenericCommands.Cut); Section.AddMenuEntry(GenericCommands.Copy); @@ -519,6 +465,22 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra Section.AddMenuEntry(GraphCommands.ToggleBreakpoint); } + { + FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeExecutionOverride", LOCTEXT("NodeExecutionOverrideMenuHeader", "Execution Override")); + if (CanSetSignalMode(EFlowSignalMode::Enabled)) + { + Section.AddMenuEntry(FlowGraphCommands.EnableNode); + } + if (CanSetSignalMode(EFlowSignalMode::Disabled)) + { + Section.AddMenuEntry(FlowGraphCommands.DisableNode); + } + if (CanSetSignalMode(EFlowSignalMode::PassThrough)) + { + Section.AddMenuEntry(FlowGraphCommands.SetPassThrough); + } + } + { FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeJumps", LOCTEXT("NodeJumpsMenuHeader", "Jumps")); if (CanFocusViewport()) @@ -533,6 +495,16 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra } } +bool UFlowGraphNode::CanUserDeleteNode() const +{ + return FlowNode ? FlowNode->bCanDelete : Super::CanUserDeleteNode(); +} + +bool UFlowGraphNode::CanDuplicateNode() const +{ + return FlowNode ? FlowNode->bCanDuplicate : Super::CanDuplicateNode(); +} + TSharedPtr UFlowGraphNode::CreateVisualWidget() { return SNew(SFlowGraphNode, this); @@ -542,6 +514,26 @@ FText UFlowGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const { if (FlowNode) { + if (UFlowGraphEditorSettings::Get()->bShowNodeClass) + { + FString CleanAssetName; + if (FlowNode->GetClass()->ClassGeneratedBy) + { + FlowNode->GetClass()->GetPathName(nullptr, CleanAssetName); + const int32 SubStringIdx = CleanAssetName.Find(".", ESearchCase::IgnoreCase, ESearchDir::FromEnd); + CleanAssetName.LeftInline(SubStringIdx); + } + else + { + CleanAssetName = FlowNode->GetClass()->GetName(); + } + + FFormatNamedArguments Args; + Args.Add(TEXT("NodeTitle"), FlowNode->GetNodeTitle()); + Args.Add(TEXT("AssetName"), FText::FromString(CleanAssetName)); + return FText::Format(INVTEXT("{NodeTitle}\n{AssetName}"), Args); + } + return FlowNode->GetNodeTitle(); } @@ -553,12 +545,17 @@ FLinearColor UFlowGraphNode::GetNodeTitleColor() const if (FlowNode) { FLinearColor DynamicColor; - if (FlowNode->GetNodeTitleColor(DynamicColor)) + if (FlowNode->GetDynamicTitleColor(DynamicColor)) { return DynamicColor; } - if (const FLinearColor* StyleColor = UFlowGraphSettings::Get()->NodeTitleColors.Find(FlowNode->GetNodeStyle())) + UFlowGraphSettings* GraphSettings = UFlowGraphSettings::Get(); + if (const FLinearColor* NodeSpecificColor = GraphSettings->NodeSpecificColors.Find(FlowNode->GetClass())) + { + return *NodeSpecificColor; + } + if (const FLinearColor* StyleColor = GraphSettings->NodeTitleColors.Find(FlowNode->GetNodeStyle())) { return *StyleColor; } @@ -588,7 +585,12 @@ FText UFlowGraphNode::GetTooltipText() const FString UFlowGraphNode::GetNodeDescription() const { - return FlowNode ? FlowNode->GetNodeDescription() : FString(); + if (FlowNode && (GEditor->PlayWorld == nullptr || UFlowGraphEditorSettings::Get()->bShowNodeDescriptionWhilePlaying)) + { + return FlowNode->GetNodeDescription(); + } + + return FString(); } UFlowNode* UFlowGraphNode::GetInspectedNodeInstance() const @@ -600,7 +602,7 @@ EFlowNodeState UFlowGraphNode::GetActivationState() const { if (FlowNode) { - if (UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) { return NodeInstance->GetActivationState(); } @@ -613,7 +615,7 @@ FString UFlowGraphNode::GetStatusString() const { if (FlowNode) { - if (UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) { return NodeInstance->GetStatusString(); } @@ -622,11 +624,28 @@ FString UFlowGraphNode::GetStatusString() const return FString(); } +FLinearColor UFlowGraphNode::GetStatusBackgroundColor() const +{ + if (FlowNode) + { + if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + { + FLinearColor ObtainedColor; + if (NodeInstance->GetStatusBackgroundColor(ObtainedColor)) + { + return ObtainedColor; + } + } + } + + return UFlowGraphSettings::Get()->NodeStatusBackground; +} + bool UFlowGraphNode::IsContentPreloaded() const { if (FlowNode) { - if (UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) { return NodeInstance->bPreloaded; } @@ -686,6 +705,13 @@ void UFlowGraphNode::CreateInputPin(const FFlowPin& FlowPin, const int32 Index / const FEdGraphPinType PinType = FEdGraphPinType(UEdGraphSchema_K2::PC_Exec, FName(NAME_None), nullptr, EPinContainerType::None, false, FEdGraphTerminalType()); UEdGraphPin* NewPin = CreatePin(EGPD_Input, PinType, FlowPin.PinName, Index); check(NewPin); + + if (!FlowPin.PinFriendlyName.IsEmpty()) + { + NewPin->bAllowFriendlyName = true; + NewPin->PinFriendlyName = FlowPin.PinFriendlyName; + } + NewPin->PinToolTip = FlowPin.PinToolTip; InputPins.Emplace(NewPin); @@ -701,6 +727,13 @@ void UFlowGraphNode::CreateOutputPin(const FFlowPin& FlowPin, const int32 Index const FEdGraphPinType PinType = FEdGraphPinType(UEdGraphSchema_K2::PC_Exec, FName(NAME_None), nullptr, EPinContainerType::None, false, FEdGraphTerminalType()); UEdGraphPin* NewPin = CreatePin(EGPD_Output, PinType, FlowPin.PinName, Index); check(NewPin); + + if (!FlowPin.PinFriendlyName.IsEmpty()) + { + NewPin->bAllowFriendlyName = true; + NewPin->PinFriendlyName = FlowPin.PinFriendlyName; + } + NewPin->PinToolTip = FlowPin.PinToolTip; OutputPins.Emplace(NewPin); @@ -713,7 +746,7 @@ void UFlowGraphNode::RemoveOrphanedPin(UEdGraphPin* Pin) PinBreakpoints.Remove(Pin); - Pin->MarkPendingKill(); + Pin->MarkAsGarbage(); Pins.Remove(Pin); ReconstructNode(); @@ -737,38 +770,56 @@ bool UFlowGraphNode::CanUserAddOutput() const bool UFlowGraphNode::CanUserRemoveInput(const UEdGraphPin* Pin) const { - return FlowNode && FlowNode->InputPins.Num() > FlowNode->GetClass()->GetDefaultObject()->InputPins.Num(); + return FlowNode && !FlowNode->GetClass()->GetDefaultObject()->InputPins.Contains(Pin->PinName); } bool UFlowGraphNode::CanUserRemoveOutput(const UEdGraphPin* Pin) const { - return FlowNode && FlowNode->OutputPins.Num() > FlowNode->GetClass()->GetDefaultObject()->OutputPins.Num(); + return FlowNode && !FlowNode->GetClass()->GetDefaultObject()->OutputPins.Contains(Pin->PinName); } void UFlowGraphNode::AddUserInput() { - AddInstancePin(EGPD_Input, *FString::FromInt(InputPins.Num())); + AddInstancePin(EGPD_Input, FlowNode->CountNumberedInputs()); } void UFlowGraphNode::AddUserOutput() { - AddInstancePin(EGPD_Output, *FString::FromInt(OutputPins.Num())); + AddInstancePin(EGPD_Output, FlowNode->CountNumberedOutputs()); } -void UFlowGraphNode::AddInstancePin(const EEdGraphPinDirection Direction, const FName& PinName) +void UFlowGraphNode::AddInstancePin(const EEdGraphPinDirection Direction, const uint8 NumberedPinsAmount) { const FScopedTransaction Transaction(LOCTEXT("AddInstancePin", "Add Instance Pin")); Modify(); + const FFlowPin PinName = FFlowPin(FString::FromInt(NumberedPinsAmount)); + if (Direction == EGPD_Input) { - FlowNode->InputPins.Emplace(PinName); - CreateInputPin(FlowNode->InputPins.Last()); + if (FlowNode->InputPins.IsValidIndex(NumberedPinsAmount)) + { + FlowNode->InputPins.Insert(PinName, NumberedPinsAmount); + } + else + { + FlowNode->InputPins.Add(PinName); + } + + CreateInputPin(PinName, NumberedPinsAmount); } else { - FlowNode->OutputPins.Emplace(PinName); - CreateOutputPin(FlowNode->OutputPins.Last()); + if (FlowNode->OutputPins.IsValidIndex(NumberedPinsAmount)) + { + FlowNode->OutputPins.Insert(PinName, NumberedPinsAmount); + } + else + { + FlowNode->OutputPins.Add(PinName); + } + + CreateOutputPin(PinName, FlowNode->InputPins.Num() + NumberedPinsAmount); } GetGraph()->NotifyGraphChanged(); @@ -786,9 +837,9 @@ void UFlowGraphNode::RemoveInstancePin(UEdGraphPin* Pin) if (InputPins.Contains(Pin)) { InputPins.Remove(Pin); - FlowNode->RemoveUserInput(); + FlowNode->RemoveUserInput(Pin->PinName); - Pin->MarkPendingKill(); + Pin->MarkAsGarbage(); Pins.Remove(Pin); } } @@ -797,9 +848,9 @@ void UFlowGraphNode::RemoveInstancePin(UEdGraphPin* Pin) if (OutputPins.Contains(Pin)) { OutputPins.Remove(Pin); - FlowNode->RemoveUserOutput(); + FlowNode->RemoveUserOutput(Pin->PinName); - Pin->MarkPendingKill(); + Pin->MarkAsGarbage(); Pins.Remove(Pin); } } @@ -841,13 +892,13 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO // add information on pin activations if (GEditor->PlayWorld) { - if (UFlowNode* InspectedNodeInstance = GetInspectedNodeInstance()) + if (const UFlowNode* InspectedNodeInstance = GetInspectedNodeInstance()) { if (!HoverTextOut.IsEmpty()) { HoverTextOut.Append(LINE_TERMINATOR).Append(LINE_TERMINATOR); } - + const TArray& PinRecords = InspectedNodeInstance->GetPinRecords(Pin.PinName, Pin.Direction); if (PinRecords.Num() == 0) { @@ -860,6 +911,19 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO { HoverTextOut.Append(LINE_TERMINATOR); HoverTextOut.Appendf(TEXT("%d) %s"), i + 1, *PinRecords[i].HumanReadableTime); + + switch (PinRecords[i].ActivationType) + { + case EFlowPinActivationType::Default: + break; + case EFlowPinActivationType::Forced: + HoverTextOut.Append(FPinRecord::ForcedActivation); + break; + case EFlowPinActivationType::PassThrough: + HoverTextOut.Append(FPinRecord::PassThroughActivation); + break; + default: ; + } } } } @@ -870,7 +934,7 @@ void UFlowGraphNode::OnInputTriggered(const int32 Index) { if (InputPins.IsValidIndex(Index) && PinBreakpoints.Contains(InputPins[Index])) { - PinBreakpoints[InputPins[Index]].bBreakpointHit = true; + PinBreakpoints[InputPins[Index]].MarkAsHit(); TryPausingSession(true); } @@ -881,7 +945,7 @@ void UFlowGraphNode::OnOutputTriggered(const int32 Index) { if (OutputPins.IsValidIndex(Index) && PinBreakpoints.Contains(OutputPins[Index])) { - PinBreakpoints[OutputPins[Index]].bBreakpointHit = true; + PinBreakpoints[OutputPins[Index]].MarkAsHit(); TryPausingSession(true); } @@ -891,9 +955,9 @@ void UFlowGraphNode::OnOutputTriggered(const int32 Index) void UFlowGraphNode::TryPausingSession(bool bPauseSession) { // Node breakpoints waits on any pin triggered - if (NodeBreakpoint.IsBreakpointEnabled()) + if (NodeBreakpoint.IsEnabled()) { - NodeBreakpoint.bBreakpointHit = true; + NodeBreakpoint.MarkAsHit(); bPauseSession = true; } @@ -902,7 +966,7 @@ void UFlowGraphNode::TryPausingSession(bool bPauseSession) FEditorDelegates::ResumePIE.AddUObject(this, &UFlowGraphNode::OnResumePIE); FEditorDelegates::EndPIE.AddUObject(this, &UFlowGraphNode::OnEndPIE); - FFlowDebugger::PausePlaySession(); + UFlowDebuggerSubsystem::PausePlaySession(); } } @@ -921,11 +985,53 @@ void UFlowGraphNode::ResetBreakpoints() FEditorDelegates::ResumePIE.RemoveAll(this); FEditorDelegates::EndPIE.RemoveAll(this); - NodeBreakpoint.bBreakpointHit = false; - for (TPair& PinBreakpoint : PinBreakpoints) + NodeBreakpoint.ResetHit(); + for (TPair& PinBreakpoint : PinBreakpoints) + { + PinBreakpoint.Value.ResetHit(); + } +} + +void UFlowGraphNode::ForcePinActivation(const FEdGraphPinReference PinReference) const +{ + UFlowNode* InspectedNodeInstance = GetInspectedNodeInstance(); + if (InspectedNodeInstance == nullptr) { - PinBreakpoint.Value.bBreakpointHit = false; + return; } + + if (const UEdGraphPin* FoundPin = PinReference.Get()) + { + switch (FoundPin->Direction) + { + case EGPD_Input: + InspectedNodeInstance->TriggerInput(FoundPin->PinName, EFlowPinActivationType::Forced); + break; + case EGPD_Output: + InspectedNodeInstance->TriggerOutput(FoundPin->PinName, false, EFlowPinActivationType::Forced); + break; + default: ; + } + } +} + +void UFlowGraphNode::SetSignalMode(const EFlowSignalMode Mode) +{ + if (FlowNode) + { + FlowNode->SignalMode = Mode; + OnSignalModeChanged.ExecuteIfBound(); + } +} + +EFlowSignalMode UFlowGraphNode::GetSignalMode() const +{ + return FlowNode ? FlowNode->SignalMode : EFlowSignalMode::Disabled; +} + +bool UFlowGraphNode::CanSetSignalMode(const EFlowSignalMode Mode) const +{ + return FlowNode ? (FlowNode->AllowedSignalModes.Contains(Mode) && FlowNode->SignalMode != Mode) : false; } #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp index 6b093c1c8..5ba3e06f2 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Nodes/FlowGraphNode_ExecutionSequence.h" #include "Nodes/Route/FlowNode_ExecutionMultiGate.h" #include "Nodes/Route/FlowNode_ExecutionSequence.h" diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp index f18d02e06..1092260e4 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Nodes/FlowGraphNode_Finish.h" #include "Graph/Widgets/SFlowGraphNode_Finish.h" diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp index 3bb0d6918..cac81f6d4 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Nodes/FlowGraphNode_Reroute.h" #include "SGraphNodeKnot.h" diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp index a8b1d2b56..19fde8ba8 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Nodes/FlowGraphNode_Start.h" #include "Graph/Widgets/SFlowGraphNode_Start.h" diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_SubGraph.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_SubGraph.cpp new file mode 100644 index 000000000..2e22bc80b --- /dev/null +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_SubGraph.cpp @@ -0,0 +1,17 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/Nodes/FlowGraphNode_SubGraph.h" +#include "Graph/Widgets/SFlowGraphNode_SubGraph.h" + +#include "Nodes/Route/FlowNode_SubGraph.h" + +UFlowGraphNode_SubGraph::UFlowGraphNode_SubGraph(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + AssignedNodeClasses = {UFlowNode_SubGraph::StaticClass()}; +} + +TSharedPtr UFlowGraphNode_SubGraph::CreateVisualWidget() +{ + return SNew(SFlowGraphNode_SubGraph, this); +} diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp index 21ad855fe..5982d52c3 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Widgets/SFlowGraphNode.h" #include "FlowEditorStyle.h" #include "Graph/FlowGraphSettings.h" @@ -14,9 +16,9 @@ #include "SCommentBubble.h" #include "SGraphNode.h" #include "SGraphPin.h" +#include "SlateOptMacros.h" #include "SLevelOfDetailBranchNode.h" #include "SNodePanel.h" -#include "SlateOptMacros.h" #include "Styling/SlateColor.h" #include "TutorialMetaData.h" #include "Widgets/Images/SImage.h" @@ -24,6 +26,7 @@ #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/SToolTip.h" +#include "Widgets/Text/SInlineEditableTextBlock.h" #define LOCTEXT_NAMESPACE "SFlowGraphNode" @@ -41,7 +44,9 @@ void SFlowGraphPinExec::Construct(const FArguments& InArgs, UEdGraphPin* InPin) void SFlowGraphNode::Construct(const FArguments& InArgs, UFlowGraphNode* InNode) { GraphNode = InNode; + FlowGraphNode = InNode; + FlowGraphNode->OnSignalModeChanged.BindRaw(this, &SFlowGraphNode::UpdateGraphNode); SetCursor(EMouseCursor::CardinalCross); UpdateGraphNode(); @@ -49,7 +54,7 @@ void SFlowGraphNode::Construct(const FArguments& InArgs, UFlowGraphNode* InNode) void SFlowGraphNode::GetNodeInfoPopups(FNodeInfoContext* Context, TArray& Popups) const { - const FString Description = FlowGraphNode->GetNodeDescription(); + const FString& Description = FlowGraphNode->GetNodeDescription(); if (!Description.IsEmpty()) { const FGraphInformationPopupInfo DescriptionPopup = FGraphInformationPopupInfo(nullptr, UFlowGraphSettings::Get()->NodeDescriptionBackground, Description); @@ -61,7 +66,7 @@ void SFlowGraphNode::GetNodeInfoPopups(FNodeInfoContext* Context, TArrayGetStatusString(); if (!Status.IsEmpty()) { - const FGraphInformationPopupInfo DescriptionPopup = FGraphInformationPopupInfo(nullptr, UFlowGraphSettings::Get()->NodeStatusBackground, Status); + const FGraphInformationPopupInfo DescriptionPopup = FGraphInformationPopupInfo(nullptr, FlowGraphNode->GetStatusBackgroundColor(), Status); Popups.Add(DescriptionPopup); } else if (FlowGraphNode->IsContentPreloaded()) @@ -95,18 +100,18 @@ const FSlateBrush* SFlowGraphNode::GetShadowBrush(bool bSelected) const void SFlowGraphNode::GetOverlayBrushes(bool bSelected, const FVector2D WidgetSize, TArray& Brushes) const { // Node breakpoint - if (FlowGraphNode->NodeBreakpoint.bHasBreakpoint) + if (FlowGraphNode->NodeBreakpoint.IsAllowed()) { FOverlayBrushInfo NodeBrush; - if (FlowGraphNode->NodeBreakpoint.bBreakpointHit) + if (FlowGraphNode->NodeBreakpoint.IsHit()) { NodeBrush.Brush = FFlowEditorStyle::Get()->GetBrush(TEXT("FlowGraph.BreakpointHit")); NodeBrush.OverlayOffset.X = WidgetSize.X - 12.0f; } else { - NodeBrush.Brush = FFlowEditorStyle::Get()->GetBrush(FlowGraphNode->NodeBreakpoint.bBreakpointEnabled ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); + NodeBrush.Brush = FFlowEditorStyle::Get()->GetBrush(FlowGraphNode->NodeBreakpoint.IsEnabled() ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); NodeBrush.OverlayOffset.X = WidgetSize.X; } @@ -116,7 +121,7 @@ void SFlowGraphNode::GetOverlayBrushes(bool bSelected, const FVector2D WidgetSiz } // Pin breakpoints - for (const TPair& PinBreakpoint : FlowGraphNode->PinBreakpoints) + for (const TPair& PinBreakpoint : FlowGraphNode->PinBreakpoints) { if (PinBreakpoint.Key.Get()->Direction == EGPD_Input) { @@ -129,13 +134,13 @@ void SFlowGraphNode::GetOverlayBrushes(bool bSelected, const FVector2D WidgetSiz } } -void SFlowGraphNode::GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const FFlowBreakpoint& Breakpoint, TArray& Brushes) const +void SFlowGraphNode::GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const FFlowPinTrait& Breakpoint, TArray& Brushes) const { - if (Breakpoint.bHasBreakpoint) + if (Breakpoint.IsAllowed()) { FOverlayBrushInfo PinBrush; - if (Breakpoint.bBreakpointHit) + if (Breakpoint.IsHit()) { PinBrush.Brush = FFlowEditorStyle::Get()->GetBrush(TEXT("FlowGraph.PinBreakpointHit")); PinBrush.OverlayOffset.X = bLeftSide ? 0.0f : (WidgetWidth - 36.0f); @@ -143,7 +148,7 @@ void SFlowGraphNode::GetPinBrush(const bool bLeftSide, const float WidgetWidth, } else { - PinBrush.Brush = FFlowEditorStyle::Get()->GetBrush(Breakpoint.bBreakpointEnabled ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); + PinBrush.Brush = FFlowEditorStyle::Get()->GetBrush(Breakpoint.IsEnabled() ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); PinBrush.OverlayOffset.X = bLeftSide ? -24.0f : WidgetWidth; PinBrush.OverlayOffset.Y = 16.0f + PinIndex * 28.0f; } @@ -189,46 +194,46 @@ void SFlowGraphNode::UpdateGraphNode() const TSharedRef DefaultTitleAreaWidget = SNew(SOverlay) + SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() .HAlign(HAlign_Fill) - .VAlign(VAlign_Center) [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .HAlign(HAlign_Fill) - [ - SNew(SBorder) - .BorderImage(FFlowEditorStyle::GetBrush("Flow.Node.Title")) - // The extra margin on the right is for making the color spill stretch well past the node title - .Padding(FMargin(10, 5, 30, 3)) - .BorderBackgroundColor(this, &SGraphNode::GetNodeTitleColor) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .VAlign(VAlign_Top) - .Padding(FMargin(0.f, 0.f, 4.f, 0.f)) - .AutoWidth() - [ - SNew(SImage) - .Image(IconBrush) - .ColorAndOpacity(this, &SGraphNode::GetNodeTitleIconColor) - ] - + SHorizontalBox::Slot() - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - [ - CreateTitleWidget(NodeTitle) - ] - + SVerticalBox::Slot() - .AutoHeight() - [ - NodeTitle.ToSharedRef() - ] - ] - ] - ] - ]; + SNew(SBorder) + .BorderImage(FFlowEditorStyle::GetBrush("Flow.Node.Title")) + // The extra margin on the right is for making the color spill stretch well past the node title + .Padding(FMargin(10, 5, 30, 3)) + .BorderBackgroundColor(this, &SGraphNode::GetNodeTitleColor) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .VAlign(VAlign_Top) + .Padding(FMargin(0.f, 0.f, 4.f, 0.f)) + .AutoWidth() + [ + SNew(SImage) + .Image(IconBrush) + .ColorAndOpacity(this, &SFlowGraphNode::GetNodeTitleIconColor) + ] + + SHorizontalBox::Slot() + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + CreateTitleWidget(NodeTitle) + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + NodeTitle.ToSharedRef() + ] + ] + ] + ] + ]; SetDefaultTitleAreaWidget(DefaultTitleAreaWidget); @@ -247,20 +252,13 @@ void SFlowGraphNode::UpdateGraphNode() DefaultTitleAreaWidget ]; - - if (!SWidget::GetToolTip().IsValid()) - { - const TSharedRef DefaultToolTip = IDocumentation::Get()->CreateToolTip(TAttribute(this, &SGraphNode::GetNodeTooltip), nullptr, GraphNode->GetDocumentationLink(), GraphNode->GetDocumentationExcerptName()); - SetToolTip(DefaultToolTip); - } - // Setup a meta tag for this node FGraphNodeMetaData TagMeta(TEXT("FlowGraphNode")); PopulateMetaTag(&TagMeta); this->ContentScale.Bind(this, &SGraphNode::GetContentScale); - TSharedPtr InnerVerticalBox = SNew(SVerticalBox) + const TSharedPtr InnerVerticalBox = SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) @@ -303,22 +301,22 @@ void SFlowGraphNode::UpdateGraphNode() [ SAssignNew(MainVerticalBox, SVerticalBox) + SVerticalBox::Slot() - .AutoHeight() - [ - SNew(SOverlay) - .AddMetaData(TagMeta) - + SOverlay::Slot() - .Padding(Settings->GetNonPinNodeBodyPadding()) - [ - SNew(SImage) - .Image(GetNodeBodyBrush()) - .ColorAndOpacity(this, &SGraphNode::GetNodeBodyColor) - ] - + SOverlay::Slot() - [ - InnerVerticalBox.ToSharedRef() - ] - ] + .AutoHeight() + [ + SNew(SOverlay) + .AddMetaData(TagMeta) + + SOverlay::Slot() + .Padding(Settings->GetNonPinNodeBodyPadding()) + [ + SNew(SImage) + .Image(GetNodeBodyBrush()) + .ColorAndOpacity(this, &SFlowGraphNode::GetNodeBodyColor) + ] + + SOverlay::Slot() + [ + InnerVerticalBox.ToSharedRef() + ] + ] ]; if (GraphNode && GraphNode->SupportsCommentBubble()) @@ -357,6 +355,67 @@ void SFlowGraphNode::UpdateGraphNode() CreateAdvancedViewArrow(InnerVerticalBox); } +void SFlowGraphNode::UpdateErrorInfo() +{ + if (const UFlowNode* FlowNode = FlowGraphNode->GetFlowNode()) + { + if (FlowNode->ValidationLog.Messages.Num() > 0) + { + EMessageSeverity::Type MaxSeverity = EMessageSeverity::Info; + for (const TSharedRef& Message : FlowNode->ValidationLog.Messages) + { + if (Message->GetSeverity() < MaxSeverity) + { + MaxSeverity = Message->GetSeverity(); + } + } + + switch(MaxSeverity) + { + case EMessageSeverity::Error: + ErrorMsg = FString(TEXT("ERROR!")); + ErrorColor = FEditorStyle::GetColor("ErrorReporting.BackgroundColor"); + break; + case EMessageSeverity::PerformanceWarning: + case EMessageSeverity::Warning: + ErrorMsg = FString(TEXT("WARNING!")); + ErrorColor = FEditorStyle::GetColor("ErrorReporting.WarningBackgroundColor"); + break; + case EMessageSeverity::Info: + ErrorMsg = FString(TEXT("NOTE")); + ErrorColor = FEditorStyle::GetColor("InfoReporting.BackgroundColor"); + break; + default: ; + } + + return; + } + + if (FlowNode->GetClass()->HasAnyClassFlags(CLASS_Deprecated) || FlowNode->bNodeDeprecated) + { + ErrorMsg = FlowNode->ReplacedBy ? FString::Printf(TEXT(" REPLACED BY: %s "), *FlowNode->ReplacedBy->GetName()) : FString(TEXT(" DEPRECATED! ")); + ErrorColor = FEditorStyle::GetColor("ErrorReporting.WarningBackgroundColor"); + return; + } + } + + SGraphNode::UpdateErrorInfo(); +} + +TSharedRef SFlowGraphNode::CreateTitleWidget(TSharedPtr NodeTitle) +{ + SAssignNew(InlineEditableText, SInlineEditableTextBlock) + .Style(FAppStyle::Get(), "Graph.Node.NodeTitleInlineEditableText") + .Text(NodeTitle.Get(), &SNodeTitle::GetHeadTitle) + .OnVerifyTextChanged(this, &SFlowGraphNode::OnVerifyNameTextChanged) + .OnTextCommitted(this, &SFlowGraphNode::OnNameTextCommited) + .IsReadOnly(this, &SFlowGraphNode::IsNameReadOnly) + .IsSelected(this, &SFlowGraphNode::IsSelectedExclusively); + InlineEditableText->SetColorAndOpacity(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &SFlowGraphNode::GetNodeTitleTextColor))); + + return InlineEditableText.ToSharedRef(); +} + TSharedRef SFlowGraphNode::CreateNodeContentArea() { return SNew(SBorder) @@ -385,9 +444,83 @@ const FSlateBrush* SFlowGraphNode::GetNodeBodyBrush() const return FFlowEditorStyle::GetBrush("Flow.Node.Body"); } +FSlateColor SFlowGraphNode::GetNodeTitleColor() const +{ + FLinearColor ReturnTitleColor = GraphNode->IsDeprecated() ? FLinearColor::Red : GetNodeObj()->GetNodeTitleColor(); + + if (FlowGraphNode->GetSignalMode() == EFlowSignalMode::Enabled) + { + ReturnTitleColor.A = FadeCurve.GetLerp(); + } + else + { + ReturnTitleColor *= FLinearColor(0.5f, 0.5f, 0.5f, 0.4f); + } + + return ReturnTitleColor; +} + +FSlateColor SFlowGraphNode::GetNodeBodyColor() const +{ + FLinearColor ReturnBodyColor = GraphNode->GetNodeBodyTintColor(); + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled) + { + ReturnBodyColor *= FLinearColor(1.0f, 1.0f, 1.0f, 0.5f); + } + return ReturnBodyColor; +} + +FSlateColor SFlowGraphNode::GetNodeTitleIconColor() const +{ + FLinearColor ReturnIconColor = IconColor; + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled) + { + ReturnIconColor *= FLinearColor(1.0f, 1.0f, 1.0f, 0.3f); + } + return ReturnIconColor; +} + +FLinearColor SFlowGraphNode::GetNodeTitleTextColor() const +{ + FLinearColor ReturnTextColor = FLinearColor::White; + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled) + { + ReturnTextColor *= FLinearColor(1.0f, 1.0f, 1.0f, 0.3f); + } + return ReturnTextColor; +} + +TSharedPtr SFlowGraphNode::GetEnabledStateWidget() const +{ + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled && !GraphNode->IsAutomaticallyPlacedGhostNode()) + { + const bool bPassThrough = FlowGraphNode->GetSignalMode() == EFlowSignalMode::PassThrough; + const FText StatusMessage = bPassThrough ? LOCTEXT("PassThrough", "Pass Through") : LOCTEXT("DisabledNode", "Disabled"); + const FText StatusMessageTooltip = bPassThrough ? + LOCTEXT("PassThroughTooltip", "This node won't execute internal logic, but it will trigger all connected outputs") : + LOCTEXT("DisabledNodeTooltip", "This node is disabled and will not be executed"); + + return SNew(SBorder) + .BorderImage(FEditorStyle::GetBrush(bPassThrough ? "Graph.Node.DevelopmentBanner" : "Graph.Node.DisabledBanner")) + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(STextBlock) + .Text(StatusMessage) + .ToolTipText(StatusMessageTooltip) + .Justification(ETextJustify::Center) + .ColorAndOpacity(FLinearColor::White) + .ShadowOffset(FVector2D::UnitVector) + .Visibility(EVisibility::Visible) + ]; + } + + return TSharedPtr(); +} + void SFlowGraphNode::CreateStandardPinWidget(UEdGraphPin* Pin) { - TSharedPtr NewPin = SNew(SFlowGraphPinExec, Pin); + const TSharedPtr NewPin = SNew(SFlowGraphPinExec, Pin); if (!UFlowGraphSettings::Get()->bShowDefaultPinNames && FlowGraphNode->GetFlowNode()) { @@ -412,25 +545,35 @@ void SFlowGraphNode::CreateStandardPinWidget(UEdGraphPin* Pin) END_SLATE_FUNCTION_BUILD_OPTIMIZATION +TSharedPtr SFlowGraphNode::GetComplexTooltip() +{ + return IDocumentation::Get()->CreateToolTip(TAttribute(this, &SGraphNode::GetNodeTooltip), nullptr, GraphNode->GetDocumentationLink(), GraphNode->GetDocumentationExcerptName()); +} + void SFlowGraphNode::CreateInputSideAddButton(TSharedPtr OutputBox) { if (FlowGraphNode->CanUserAddInput()) { - const TSharedRef AddPinButton = AddPinButtonContent( - LOCTEXT("FlowNodeAddPinButton", "Add pin"), - LOCTEXT("FlowNodeAddPinButton_Tooltip", "Adds an input pin") - ); - - FMargin AddPinPadding = Settings->GetInputPinPadding(); - AddPinPadding.Top += 6.0f; + TSharedPtr AddPinWidget; + SAssignNew(AddPinWidget, SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + . VAlign(VAlign_Center) + . Padding( 0,0,7,0 ) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush(TEXT("Icons.PlusCircle"))) + ] + +SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Text(LOCTEXT("FlowNodeAddPinButton", "Add pin")) + .ColorAndOpacity(FLinearColor::White) + ]; - OutputBox->AddSlot() - .AutoHeight() - .VAlign(VAlign_Center) - .Padding(AddPinPadding) - [ - AddPinButton - ]; + AddPinButton(OutputBox, AddPinWidget.ToSharedRef(), EGPD_Input); } } @@ -438,33 +581,80 @@ void SFlowGraphNode::CreateOutputSideAddButton(TSharedPtr OutputBo { if (FlowGraphNode->CanUserAddOutput()) { - const TSharedRef AddPinButton = AddPinButtonContent( - LOCTEXT("FlowNodeAddPinButton", "Add pin"), - LOCTEXT("FlowNodeAddPinButton_Tooltip", "Adds an output pin") - ); - - FMargin AddPinPadding = Settings->GetOutputPinPadding(); - AddPinPadding.Top += 6.0f; + TSharedPtr AddPinWidget; + SAssignNew(AddPinWidget, SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Text(LOCTEXT("FlowNodeAddPinButton", "Add pin")) + .ColorAndOpacity(FLinearColor::White) + ] + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(7,0,0,0) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush(TEXT("Icons.PlusCircle"))) + ]; - OutputBox->AddSlot() - .AutoHeight() - .VAlign(VAlign_Center) - .Padding(AddPinPadding) - [ - AddPinButton - ]; - } + AddPinButton(OutputBox, AddPinWidget.ToSharedRef(), EGPD_Output); + } } -FReply SFlowGraphNode::OnAddPin() +void SFlowGraphNode::AddPinButton(TSharedPtr OutputBox, const TSharedRef ButtonContent, const EEdGraphPinDirection Direction, const FString DocumentationExcerpt, const TSharedPtr CustomTooltip) { - if (FlowGraphNode->CanUserAddInput()) + const FText PinTooltipText = (Direction == EEdGraphPinDirection::EGPD_Input) ? LOCTEXT("FlowNodeAddPinButton_InputTooltip", "Adds an input pin") : LOCTEXT("FlowNodeAddPinButton_OutputTooltip", "Adds an output pin"); + TSharedPtr Tooltip; + + if (CustomTooltip.IsValid()) { - FlowGraphNode->AddUserInput(); + Tooltip = CustomTooltip; } - else if (FlowGraphNode->CanUserAddOutput()) + else if (!DocumentationExcerpt.IsEmpty()) + { + Tooltip = IDocumentation::Get()->CreateToolTip(PinTooltipText, nullptr, GraphNode->GetDocumentationLink(), DocumentationExcerpt); + } + + const TSharedRef AddPinButton = SNew(SButton) + .ContentPadding(0.0f) + .ButtonStyle(FEditorStyle::Get(), "NoBorder") + .OnClicked(this, &SFlowGraphNode::OnAddFlowPin, Direction) + .IsEnabled(this, &SFlowGraphNode::IsNodeEditable) + .ToolTipText(PinTooltipText) + .ToolTip(Tooltip) + .Visibility(this, &SFlowGraphNode::IsAddPinButtonVisible) + [ + ButtonContent + ]; + + AddPinButton->SetCursor(EMouseCursor::Hand); + + FMargin AddPinPadding = (Direction == EEdGraphPinDirection::EGPD_Input) ? Settings->GetInputPinPadding() : Settings->GetOutputPinPadding(); + AddPinPadding.Top += 6.0f; + + OutputBox->AddSlot() + .AutoHeight() + .VAlign(VAlign_Center) + .Padding(AddPinPadding) + [ + AddPinButton + ]; +} + +FReply SFlowGraphNode::OnAddFlowPin(const EEdGraphPinDirection Direction) +{ + switch (Direction) { - FlowGraphNode->AddUserOutput(); + case EGPD_Input: + FlowGraphNode->AddUserInput(); + break; + case EGPD_Output: + FlowGraphNode->AddUserOutput(); + break; + default: ; } return FReply::Handled(); diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Finish.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Finish.cpp index ad77c560a..b5df50053 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Finish.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Finish.cpp @@ -1 +1,3 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Widgets/SFlowGraphNode_Finish.h" diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Start.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Start.cpp index ac9e25785..7fe9b3062 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Start.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_Start.cpp @@ -1 +1,3 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Widgets/SFlowGraphNode_Start.h" diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_SubGraph.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_SubGraph.cpp new file mode 100644 index 000000000..18216afc7 --- /dev/null +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_SubGraph.cpp @@ -0,0 +1,61 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/Widgets/SFlowGraphNode_SubGraph.h" +#include "Graph/FlowGraphEditorSettings.h" + +#include "FlowAsset.h" +#include "Nodes/Route/FlowNode_SubGraph.h" + +#include "SGraphPreviewer.h" +#include "Widgets/SToolTip.h" + +#define LOCTEXT_NAMESPACE "SFlowGraphNode_SubGraph" + +TSharedPtr SFlowGraphNode_SubGraph::GetComplexTooltip() +{ + if (UFlowGraphEditorSettings::Get()->bShowSubGraphPreview && FlowGraphNode) + { + if (UFlowNode* FlowNode = FlowGraphNode->GetFlowNode()) + { + const UFlowAsset* AssetToEdit = Cast(FlowNode->GetAssetToEdit()); + if (AssetToEdit && AssetToEdit->GetGraph()) + { + TSharedPtr TitleBarWidget = SNullWidget::NullWidget; + if (UFlowGraphEditorSettings::Get()->bShowSubGraphPath) + { + FString CleanAssetName = AssetToEdit->GetPathName(nullptr); + const int32 SubStringIdx = CleanAssetName.Find(".", ESearchCase::IgnoreCase, ESearchDir::FromEnd); + CleanAssetName.LeftInline(SubStringIdx); + + TitleBarWidget = SNew(SBox) + .Padding(10.f) + [ + SNew(STextBlock) + .Text(FText::FromString(CleanAssetName)) + ]; + } + + return SNew(SToolTip) + [ + SNew(SBox) + .WidthOverride(UFlowGraphEditorSettings::Get()->SubGraphPreviewSize.X) + .HeightOverride(UFlowGraphEditorSettings::Get()->SubGraphPreviewSize.Y) + [ + SNew(SOverlay) + +SOverlay::Slot() + [ + SNew(SGraphPreviewer, AssetToEdit->GetGraph()) + .CornerOverlayText(LOCTEXT("FlowNodePreviewGraphOverlayText", "GRAPH PREVIEW")) + .ShowGraphStateOverlay(false) + .TitleBar(TitleBarWidget) + ] + ] + ]; + } + } + } + + return SFlowGraphNode::GetComplexTooltip(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp index 77a96f83b..3332500d2 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp @@ -1,9 +1,12 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Graph/Widgets/SFlowPalette.h" #include "Asset/FlowAssetEditor.h" #include "FlowEditorCommands.h" #include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphSchema_Actions.h" +#include "FlowAsset.h" #include "Nodes/FlowNode.h" #include "EditorStyleSet.h" @@ -47,10 +50,10 @@ void SFlowPaletteItem::Construct(const FArguments& InArgs, FCreateWidgetForActio const FSlateBrush* IconBrush = FEditorStyle::GetBrush(TEXT("NoBrush")); const FSlateColor IconColor = FSlateColor::UseForeground(); const FText IconToolTip = GraphAction->GetTooltipDescription(); - const bool bIsReadOnly = false; + constexpr bool bIsReadOnly = false; const TSharedRef IconWidget = CreateIconWidget(IconToolTip, IconBrush, IconColor); - const TSharedRef NameSlotWidget = CreateTextSlotWidget(NameFont, InCreateData, bIsReadOnly); + const TSharedRef NameSlotWidget = CreateTextSlotWidget(InCreateData, bIsReadOnly); const TSharedRef HotkeyDisplayWidget = CreateHotkeyDisplayWidget(NameFont, HotkeyChord); // Create the actual widget @@ -98,7 +101,7 @@ FText SFlowPaletteItem::GetItemTooltip() const void SFlowPalette::Construct(const FArguments& InArgs, TWeakPtr InFlowAssetEditor) { - FlowAssetEditorPtr = InFlowAssetEditor; + FlowAssetEditor = InFlowAssetEditor; UpdateCategoryNames(); UFlowGraphSchema::OnNodeListChanged.AddSP(this, &SFlowPalette::Refresh); @@ -180,8 +183,11 @@ TSharedRef SFlowPalette::OnCreateWidgetForAction(FCreateWidgetForAction void SFlowPalette::CollectAllActions(FGraphActionListBuilderBase& OutAllActions) { + ensureAlways(FlowAssetEditor.Pin() && FlowAssetEditor.Pin()->GetFlowAsset()); + const UClass* AssetClass = FlowAssetEditor.Pin()->GetFlowAsset()->GetClass(); + FGraphActionMenuBuilder ActionMenuBuilder; - UFlowGraphSchema::GetPaletteActions(ActionMenuBuilder, GetFilterCategoryName()); + UFlowGraphSchema::GetPaletteActions(ActionMenuBuilder, AssetClass, GetFilterCategoryName()); OutAllActions.Append(ActionMenuBuilder); } @@ -200,15 +206,11 @@ void SFlowPalette::CategorySelectionChanged(TSharedPtr NewSelection, ES RefreshActionsList(true); } -void SFlowPalette::OnActionSelected(const TArray>& InActions, ESelectInfo::Type InSelectionType) +void SFlowPalette::OnActionSelected(const TArray>& InActions, ESelectInfo::Type InSelectionType) const { if (InSelectionType == ESelectInfo::OnMouseClick || InSelectionType == ESelectInfo::OnKeyPress || InSelectionType == ESelectInfo::OnNavigation || InActions.Num() == 0) { - TSharedPtr FlowAssetEditor = FlowAssetEditorPtr.Pin(); - if (FlowAssetEditor) - { - FlowAssetEditor->SetUISelectionState(FFlowAssetEditor::PaletteTab); - } + FlowAssetEditor.Pin()->SetUISelectionState(FFlowAssetEditor::PaletteTab); } } diff --git a/Source/FlowEditor/Private/MovieScene/FlowSection.cpp b/Source/FlowEditor/Private/MovieScene/FlowSection.cpp index 7204d51ec..d88c773be 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowSection.cpp +++ b/Source/FlowEditor/Private/MovieScene/FlowSection.cpp @@ -1,11 +1,12 @@ -#include "FlowSection.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "MovieScene/FlowSection.h" #include "MovieScene/MovieSceneFlowRepeaterSection.h" #include "MovieScene/MovieSceneFlowTriggerSection.h" #include "CommonMovieSceneTools.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" -#include "MovieSceneEventUtils.h" #include "MovieSceneTrack.h" #include "Rendering/DrawElements.h" #include "Sections/MovieSceneEventSection.h" @@ -16,19 +17,19 @@ bool FFlowSectionBase::IsSectionSelected() const { - TSharedPtr SequencerPtr = Sequencer.Pin(); + const TSharedPtr SequencerPtr = Sequencer.Pin(); TArray SelectedTracks; SequencerPtr->GetSelectedTracks(SelectedTracks); - UMovieSceneSection* Section = WeakSection.Get(); + const UMovieSceneSection* Section = WeakSection.Get(); UMovieSceneTrack* Track = Section ? CastChecked(Section->GetOuter()) : nullptr; return Track && SelectedTracks.Contains(Track); } void FFlowSectionBase::PaintEventName(FSequencerSectionPainter& Painter, int32 LayerId, const FString& InEventString, float PixelPos, bool bIsEventValid) const { - static const float BoxOffsetPx = 10.f; + static constexpr float BoxOffsetPx = 10.f; static const TCHAR* WarningString = TEXT("\xf071"); const FSlateFontInfo FontAwesomeFont = FEditorStyle::Get().GetFontStyle("FontAwesome.10"); @@ -97,7 +98,7 @@ void FFlowSectionBase::PaintEventName(FSequencerSectionPainter& Painter, int32 L int32 FFlowSection::OnPaintSection(FSequencerSectionPainter& Painter) const { const int32 LayerId = Painter.PaintSectionBackground(); - UMovieSceneEventSection* EventSection = Cast(WeakSection.Get()); + const UMovieSceneEventSection* EventSection = Cast(WeakSection.Get()); if (!EventSection || !IsSectionSelected()) { return LayerId; @@ -158,7 +159,7 @@ int32 FFlowRepeaterSection::OnPaintSection(FSequencerSectionPainter& Painter) co { const int32 LayerId = Painter.PaintSectionBackground(); - UMovieSceneFlowRepeaterSection* EventRepeaterSection = Cast(WeakSection.Get()); + const UMovieSceneFlowRepeaterSection* EventRepeaterSection = Cast(WeakSection.Get()); if (!EventRepeaterSection) { return LayerId; diff --git a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp b/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp index 60d10678b..6bb5a9541 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp +++ b/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp @@ -1,18 +1,19 @@ -#include "FlowTrackEditor.h" -#include "FlowSection.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "MovieScene/FlowTrackEditor.h" +#include "MovieScene/FlowSection.h" #include "MovieScene/MovieSceneFlowRepeaterSection.h" #include "MovieScene/MovieSceneFlowTrack.h" #include "MovieScene/MovieSceneFlowTriggerSection.h" #include "Framework/MultiBox/MultiBoxBuilder.h" -#include "UObject/Package.h" #include "ISequencerSection.h" -#include "DetailLayoutBuilder.h" -#include "DetailCategoryBuilder.h" +#include "Launch/Resources/Version.h" +#include "LevelSequence.h" +#include "MovieSceneSequenceEditor.h" #include "Sections/MovieSceneEventSection.h" #include "SequencerUtilities.h" -#include "MovieSceneSequenceEditor.h" #define LOCTEXT_NAMESPACE "FFlowTrackEditor" @@ -65,7 +66,7 @@ void FFlowTrackEditor::AddFlowSubMenu(FMenuBuilder& MenuBuilder) void FFlowTrackEditor::BuildAddTrackMenu(FMenuBuilder& MenuBuilder) { UMovieSceneSequence* RootMovieSceneSequence = GetSequencer()->GetRootMovieSceneSequence(); - FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(RootMovieSceneSequence); + const FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(RootMovieSceneSequence); if (SequenceEditor && SequenceEditor->SupportsEvents(RootMovieSceneSequence)) { @@ -136,8 +137,7 @@ bool FFlowTrackEditor::SupportsType(TSubclassOf Type) const bool FFlowTrackEditor::SupportsSequence(UMovieSceneSequence* InSequence) const { - static UClass* LevelSequenceClass = FindObject(ANY_PACKAGE, TEXT("LevelSequence"), true); - return InSequence && LevelSequenceClass && InSequence->GetClass()->IsChildOf(LevelSequenceClass); + return InSequence && InSequence->GetClass()->IsChildOf(ULevelSequence::StaticClass()); } const FSlateBrush* FFlowTrackEditor::GetIconBrush() const @@ -145,7 +145,7 @@ const FSlateBrush* FFlowTrackEditor::GetIconBrush() const return FEditorStyle::GetBrush("Sequencer.Tracks.Event"); } -void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) +void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) const { UMovieScene* FocusedMovieScene = GetFocusedMovieScene(); @@ -163,8 +163,8 @@ void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) FocusedMovieScene->Modify(); TArray NewTracks; - UMovieSceneFlowTrack* NewMasterTrack = FocusedMovieScene->AddMasterTrack(); + NewTracks.Add(NewMasterTrack); if (GetSequencer().IsValid()) { @@ -180,12 +180,12 @@ void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) } } -void FFlowTrackEditor::CreateNewSection(UMovieSceneTrack* Track, int32 RowIndex, UClass* SectionType, bool bSelect) const +void FFlowTrackEditor::CreateNewSection(UMovieSceneTrack* Track, const int32 RowIndex, UClass* SectionType, const bool bSelect) const { - TSharedPtr SequencerPtr = GetSequencer(); + const TSharedPtr SequencerPtr = GetSequencer(); if (SequencerPtr.IsValid()) { - UMovieScene* FocusedMovieScene = GetFocusedMovieScene(); + const UMovieScene* FocusedMovieScene = GetFocusedMovieScene(); const FQualifiedFrameTime CurrentTime = SequencerPtr->GetLocalTime(); FScopedTransaction Transaction(LOCTEXT("CreateNewFlowSectionTransactionText", "Add Flow Section")); @@ -219,7 +219,7 @@ void FFlowTrackEditor::CreateNewSection(UMovieSceneTrack* Track, int32 RowIndex, } else { - const float DefaultLengthInSeconds = 5.f; + constexpr float DefaultLengthInSeconds = 5.f; NewSectionRange = TRange(CurrentTime.Time.FrameNumber, CurrentTime.Time.FrameNumber + (DefaultLengthInSeconds * SequencerPtr->GetFocusedTickResolution()).FloorToFrame()); } diff --git a/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp b/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp index b12dcdc9d..a7b1b6afe 100644 --- a/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp +++ b/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp @@ -1,6 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/AssetTypeActions_FlowNodeBlueprint.h" #include "Nodes/FlowNodeBlueprintFactory.h" #include "FlowEditorModule.h" +#include "Graph/FlowGraphSettings.h" #include "Nodes/FlowNodeBlueprint.h" @@ -13,7 +16,7 @@ FText FAssetTypeActions_FlowNodeBlueprint::GetName() const uint32 FAssetTypeActions_FlowNodeBlueprint::GetCategories() { - return FFlowEditorModule::FlowAssetCategory; + return UFlowGraphSettings::Get()->bExposeFlowNodeCreation ? FFlowEditorModule::FlowAssetCategory : 0; } UClass* FAssetTypeActions_FlowNodeBlueprint::GetSupportedClass() const diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.cpp b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.cpp deleted file mode 100644 index 5919e37aa..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "FlowNode_CustomInputDetails.h" -#include "FlowAsset.h" -#include "Nodes/Route/FlowNode_CustomInput.h" - -#include "DetailCategoryBuilder.h" -#include "DetailWidgetRow.h" -#include "PropertyEditing.h" -#include "UnrealEd.h" -#include "Widgets/Input/SComboBox.h" -#include "Widgets/Text/STextBlock.h" - -#define LOCTEXT_NAMESPACE "FlowNode_CustomInputDetails" - -void FFlowNode_CustomInputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) -{ - DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); - GetEventNames(); - - IDetailCategoryBuilder& Category = DetailLayout.EditCategory("CustomInput", LOCTEXT("CustomInputCategory", "Custom Event")); - Category.AddCustomRow(LOCTEXT("CustomRowName", "Event Name")) - .NameContent() - [ - SNew(STextBlock) - .Text(LOCTEXT("EventName", "Event Name")) - ] - .ValueContent() - .HAlign(HAlign_Fill) - [ - SNew(SComboBox>) - .OptionsSource(&EventNames) - .OnGenerateWidget(this, &FFlowNode_CustomInputDetails::GenerateEventWidget) - .OnSelectionChanged(this, &FFlowNode_CustomInputDetails::PinSelectionChanged) - [ - SNew(STextBlock) - .Text(this, &FFlowNode_CustomInputDetails::GetSelectedEventText) - ] - ]; -} - -void FFlowNode_CustomInputDetails::GetEventNames() -{ - EventNames.Empty(); - EventNames.Emplace(MakeShareable(new FName(NAME_None))); - - if (ObjectsBeingEdited[0].IsValid() && ObjectsBeingEdited[0].Get()->GetOuter()) - { - const UFlowAsset* FlowAsset = Cast(ObjectsBeingEdited[0].Get()->GetOuter()); - TArray SortedNames = FlowAsset->GetCustomInputs(); - - for (const TPair& Node : FlowAsset->GetNodes()) - { - if (Node.Value->GetClass()->IsChildOf(UFlowNode_CustomInput::StaticClass())) - { - SortedNames.Remove(Cast(Node.Value)->EventName); - } - } - - SortedNames.Sort([](const FName& A, const FName& B) - { - return A.LexicalLess(B); - }); - - for (const FName& EventName : SortedNames) - { - if (!EventName.IsNone()) - { - EventNames.Emplace(MakeShareable(new FName(EventName))); - } - } - } -} - -TSharedRef FFlowNode_CustomInputDetails::GenerateEventWidget(const TSharedPtr Item) const -{ - return SNew(STextBlock).Text(FText::FromName(*Item.Get())); -} - -FText FFlowNode_CustomInputDetails::GetSelectedEventText() const -{ - FText PropertyValue; - - ensure(ObjectsBeingEdited[0].IsValid()); - if (const UFlowNode_CustomInput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - PropertyValue = FText::FromName(Node->EventName); - } - - return PropertyValue; -} - -void FFlowNode_CustomInputDetails::PinSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectInfo) const -{ - ensure(ObjectsBeingEdited[0].IsValid()); - if (UFlowNode_CustomInput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - Node->EventName = *Item.Get(); - } -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.h b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.h deleted file mode 100644 index 3f3cbd352..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "IDetailCustomization.h" -#include "Templates/SharedPointer.h" -#include "Widgets/SWidget.h" - -class FFlowNode_CustomInputDetails final : public IDetailCustomization -{ -public: - static TSharedRef MakeInstance() - { - return MakeShareable(new FFlowNode_CustomInputDetails()); - } - - // IDetailCustomization - virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; - // -- - -private: - void GetEventNames(); - TSharedRef GenerateEventWidget(TSharedPtr Item) const; - FText GetSelectedEventText() const; - void PinSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) const; - - TArray> ObjectsBeingEdited; - TArray> EventNames; -}; diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.cpp b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.cpp deleted file mode 100644 index d3795dc14..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "FlowNode_CustomOutputDetails.h" -#include "FlowAsset.h" -#include "Nodes/Route/FlowNode_CustomOutput.h" - -#include "DetailCategoryBuilder.h" -#include "DetailWidgetRow.h" -#include "PropertyEditing.h" -#include "UnrealEd.h" -#include "Widgets/Input/SComboBox.h" -#include "Widgets/Text/STextBlock.h" - -#define LOCTEXT_NAMESPACE "FlowNode_CustomOutputDetails" - -void FFlowNode_CustomOutputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) -{ - DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); - GetEventNames(); - - IDetailCategoryBuilder& Category = DetailLayout.EditCategory("CustomOutput", LOCTEXT("CustomEventsCategory", "Custom Output")); - Category.AddCustomRow(LOCTEXT("CustomRowName", "Event Name")) - .NameContent() - [ - SNew(STextBlock) - .Text(LOCTEXT("EventName", "Event Name")) - ] - .ValueContent() - .HAlign(HAlign_Fill) - [ - SNew(SComboBox>) - .OptionsSource(&EventNames) - .OnGenerateWidget(this, &FFlowNode_CustomOutputDetails::GenerateEventWidget) - .OnSelectionChanged(this, &FFlowNode_CustomOutputDetails::PinSelectionChanged) - [ - SNew(STextBlock) - .Text(this, &FFlowNode_CustomOutputDetails::GetSelectedEventText) - ] - ]; -} - -void FFlowNode_CustomOutputDetails::GetEventNames() -{ - EventNames.Empty(); - EventNames.Emplace(MakeShareable(new FName(NAME_None))); - - if (ObjectsBeingEdited[0].IsValid() && ObjectsBeingEdited[0].Get()->GetOuter()) - { - const UFlowAsset* FlowAsset = Cast(ObjectsBeingEdited[0].Get()->GetOuter()); - TArray SortedNames = FlowAsset->GetCustomOutputs(); - - SortedNames.Sort([](const FName& A, const FName& B) - { - return A.LexicalLess(B); - }); - - for (const FName& EventName : SortedNames) - { - if (!EventName.IsNone()) - { - EventNames.Emplace(MakeShareable(new FName(EventName))); - } - } - } -} - -TSharedRef FFlowNode_CustomOutputDetails::GenerateEventWidget(const TSharedPtr Item) const -{ - return SNew(STextBlock).Text(FText::FromName(*Item.Get())); -} - -FText FFlowNode_CustomOutputDetails::GetSelectedEventText() const -{ - FText PropertyValue; - - ensure(ObjectsBeingEdited[0].IsValid()); - if (const UFlowNode_CustomOutput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - PropertyValue = FText::FromName(Node->EventName); - } - - return PropertyValue; -} - -void FFlowNode_CustomOutputDetails::PinSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectInfo) const -{ - ensure(ObjectsBeingEdited[0].IsValid()); - if (UFlowNode_CustomOutput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - Node->EventName = *Item.Get(); - } -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.h b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.h deleted file mode 100644 index 982f795b6..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "IDetailCustomization.h" -#include "Templates/SharedPointer.h" -#include "Widgets/SWidget.h" - -class FFlowNode_CustomOutputDetails final : public IDetailCustomization -{ -public: - static TSharedRef MakeInstance() - { - return MakeShareable(new FFlowNode_CustomOutputDetails()); - } - - // IDetailCustomization - virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; - // -- - -private: - void GetEventNames(); - TSharedRef GenerateEventWidget(TSharedPtr Item) const; - FText GetSelectedEventText() const; - void PinSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) const; - - TArray> ObjectsBeingEdited; - TArray> EventNames; -}; diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.cpp b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.cpp deleted file mode 100644 index c95540ee3..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "FlowNode_PlayLevelSequenceDetails.h" -#include "Nodes/World/FlowNode_PlayLevelSequence.h" - -#include "DetailCategoryBuilder.h" -#include "DetailLayoutBuilder.h" - -void FFlowNode_PlayLevelSequenceDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) -{ - IDetailCategoryBuilder& SequenceCategory = DetailBuilder.EditCategory("Sequence"); - SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, Sequence)); -} diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowPinCustomization.cpp b/Source/FlowEditor/Private/Nodes/Customizations/FlowPinCustomization.cpp deleted file mode 100644 index 1f7e44742..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowPinCustomization.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "FlowPinCustomization.h" -#include "Nodes/FlowPin.h" - -#include "PropertyEditing.h" -#include "Widgets/Input/SEditableTextBox.h" - -#define LOCTEXT_NAMESPACE "FlowPinCustomization" - -void FFlowPinCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) -{ -} - -void FFlowPinCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) -{ - TArray Objects; - StructPropertyHandle->GetOuterObjects(Objects); - const FFlowPin* FlowPin = reinterpret_cast(StructPropertyHandle->GetValueBaseAddress(reinterpret_cast(Objects[0]))); - - uint32 PropertyChildNum = -1; - if (StructPropertyHandle->GetNumChildren(PropertyChildNum) == FPropertyAccess::Success) - { - for (uint32 i = 0; i < PropertyChildNum; ++i) - { - TSharedPtr ChildHandle = StructPropertyHandle->GetChildHandle(i); - - if (ChildHandle.IsValid() && ChildHandle->GetProperty()) - { - const FName PropertyName = ChildHandle->GetProperty()->GetFName(); - if (PropertyName == GET_MEMBER_NAME_CHECKED(FFlowPin, PinName)) - { - StructBuilder.AddCustomRow(LOCTEXT("PinNameValue", "PinNameValue")) - .NameContent() - [ - StructPropertyHandle->CreatePropertyNameWidget() - ] - .ValueContent() - .MinDesiredWidth(125.0f) - .MaxDesiredWidth(125.0f) - [ - SNew(SEditableTextBox) - .Text(FText::FromName(FlowPin->PinName)) - .OnTextCommitted_Static(&FFlowPinCustomization::OnPinNameCommitted, ChildHandle.ToSharedRef()) - .OnVerifyTextChanged_Static(&FFlowPinCustomization::VerifyNewPinName) - ]; - } - else - { - StructBuilder.AddProperty(ChildHandle.ToSharedRef()); - } - } - } - } -} - -void FFlowPinCustomization::OnPinNameCommitted(const FText& InText, ETextCommit::Type InCommitType, TSharedRef PropertyHandle) -{ - TArray Objects; - PropertyHandle->GetParentHandle()->GetOuterObjects(Objects); - if (FFlowPin* FlowText = reinterpret_cast(PropertyHandle->GetValueBaseAddress(reinterpret_cast(Objects[0])))) - { - FlowText->PinName = *InText.ToString(); - } -} - -bool FFlowPinCustomization::VerifyNewPinName(const FText& InNewText, FText& OutErrorMessage) -{ - const FName NewString = *InNewText.ToString(); - - if (NewString.IsNone()) - { - OutErrorMessage = LOCTEXT("VerifyTextFailed", "Pin Name can't be None!"); - return false; - } - - return true; -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowPinCustomization.h b/Source/FlowEditor/Private/Nodes/Customizations/FlowPinCustomization.h deleted file mode 100644 index 883fd7ef2..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowPinCustomization.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "IPropertyTypeCustomization.h" -#include "Templates/SharedPointer.h" - -class FFlowPinCustomization final : public IPropertyTypeCustomization -{ -public: - static TSharedRef MakeInstance() - { - return MakeShareable(new FFlowPinCustomization()); - } - - virtual void CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; - virtual void CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; - -private: - static void OnPinNameCommitted(const FText& InText, ETextCommit::Type InCommitType, TSharedRef PropertyHandle); - static bool VerifyNewPinName(const FText& InNewText, FText& OutErrorMessage); -}; diff --git a/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp b/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp index 402526c0a..40dd60d08 100644 --- a/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp +++ b/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #include "Nodes/FlowNodeBlueprintFactory.h" #include "Nodes/FlowNode.h" @@ -147,11 +149,11 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget Options.DisplayMode = EClassViewerDisplayMode::TreeView; Options.bIsBlueprintBaseOnly = true; - TSharedPtr Filter = MakeShareable(new FFlowNodeBlueprintParentFilter); + const TSharedPtr Filter = MakeShareable(new FFlowNodeBlueprintParentFilter); // All child child classes of UFlowNode are valid Filter->AllowedChildrenOfClasses.Add(UFlowNode::StaticClass()); - Options.ClassFilter = Filter; + Options.ClassFilters = {Filter.ToSharedRef()}; ParentClassContainer->ClearChildren(); ParentClassContainer->AddSlot() @@ -240,7 +242,7 @@ UFlowNodeBlueprintFactory::UFlowNodeBlueprintFactory(const FObjectInitializer& O bool UFlowNodeBlueprintFactory::ConfigureProperties() { - TSharedRef Dialog = SNew(SFlowNodeBlueprintCreateDialog); + const TSharedRef Dialog = SNew(SFlowNodeBlueprintCreateDialog); return Dialog->ConfigureProperties(this); } @@ -260,7 +262,7 @@ UObject* UFlowNodeBlueprintFactory::FactoryCreateNew(UClass* Class, UObject* InP if (NewBP && NewBP->UbergraphPages.Num() > 0) { - UBlueprintEditorSettings* Settings = GetMutableDefault(); + const UBlueprintEditorSettings* Settings = GetMutableDefault(); if(Settings && Settings->bSpawnDefaultBlueprintNodes) { int32 NodePositionY = 0; diff --git a/Source/FlowEditor/Private/Pins/SFlowInputPinHandle.cpp b/Source/FlowEditor/Private/Pins/SFlowInputPinHandle.cpp new file mode 100644 index 000000000..c8e41ae44 --- /dev/null +++ b/Source/FlowEditor/Private/Pins/SFlowInputPinHandle.cpp @@ -0,0 +1,36 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Pins/SFlowInputPinHandle.h" +#include "Nodes/FlowNode.h" + +void SFlowInputPinHandle::RefreshNameList() +{ + PinNames.Empty(); + + if (Blueprint && Blueprint->GeneratedClass) + { + if (UFlowNode* FlowNode = Blueprint->GeneratedClass->GetDefaultObject()) + { + for (const FFlowPin& InputPin : FlowNode->InputPins) + { + PinNames.Add(MakeShareable(new FName(InputPin.PinName))); + } + } + } +} + +TSharedPtr FFlowInputPinHandleFactory::CreatePin(UEdGraphPin* InPin) const +{ + if (InPin->PinType.PinCategory == GetDefault()->PC_Struct && InPin->PinType.PinSubCategoryObject == FFlowInputPinHandle::StaticStruct() && InPin->LinkedTo.Num() == 0) + { + if (const UEdGraphNode* GraphNode = InPin->GetOuter()) + { + if (const UBlueprint* Blueprint = GraphNode->GetGraph()->GetTypedOuter()) + { + return SNew(SFlowInputPinHandle, InPin, Blueprint); + } + } + } + + return nullptr; +} diff --git a/Source/FlowEditor/Private/Pins/SFlowOutputPinHandle.cpp b/Source/FlowEditor/Private/Pins/SFlowOutputPinHandle.cpp new file mode 100644 index 000000000..5fca65021 --- /dev/null +++ b/Source/FlowEditor/Private/Pins/SFlowOutputPinHandle.cpp @@ -0,0 +1,36 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Pins/SFlowOutputPinHandle.h" +#include "Nodes/FlowNode.h" + +void SFlowOutputPinHandle::RefreshNameList() +{ + PinNames.Empty(); + + if (Blueprint && Blueprint->GeneratedClass) + { + if (UFlowNode* FlowNode = Blueprint->GeneratedClass->GetDefaultObject()) + { + for (const FFlowPin& OutputPin : FlowNode->OutputPins) + { + PinNames.Add(MakeShareable(new FName(OutputPin.PinName))); + } + } + } +} + +TSharedPtr FFlowOutputPinHandleFactory::CreatePin(UEdGraphPin* InPin) const +{ + if (InPin->PinType.PinCategory == GetDefault()->PC_Struct && InPin->PinType.PinSubCategoryObject == FFlowOutputPinHandle::StaticStruct() && InPin->LinkedTo.Num() == 0) + { + if (const UEdGraphNode* GraphNode = InPin->GetOuter()) + { + if (const UBlueprint* Blueprint = GraphNode->GetGraph()->GetTypedOuter()) + { + return SNew(SFlowOutputPinHandle, InPin, Blueprint); + } + } + } + + return nullptr; +} diff --git a/Source/FlowEditor/Private/Pins/SFlowPinHandle.cpp b/Source/FlowEditor/Private/Pins/SFlowPinHandle.cpp new file mode 100644 index 000000000..1a85544c9 --- /dev/null +++ b/Source/FlowEditor/Private/Pins/SFlowPinHandle.cpp @@ -0,0 +1,77 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Pins/SFlowPinHandle.h" + +#define LOCTEXT_NAMESPACE "SFlowPinHandle" + +SFlowPinHandle::SFlowPinHandle() + : Blueprint(nullptr) +{ +} + +SFlowPinHandle::~SFlowPinHandle() +{ + Blueprint = nullptr; +} + +void SFlowPinHandle::Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj, const UBlueprint* InBlueprint) +{ + Blueprint = InBlueprint; + RefreshNameList(); + SGraphPin::Construct(SGraphPin::FArguments(), InGraphPinObj); +} + +TSharedRef SFlowPinHandle::GetDefaultValueWidget() +{ + ParseDefaultValueData(); + + // Create widget + return SAssignNew(ComboBox, SNameComboBox) + .ContentPadding(FMargin(6.0f, 2.0f)) + .OptionsSource(&PinNames) + .InitiallySelectedItem(CurrentlySelectedName) + .OnSelectionChanged(this, &SFlowPinHandle::ComboBoxSelectionChanged) + .Visibility(this, &SGraphPin::GetDefaultValueVisibility); +} + +void SFlowPinHandle::ParseDefaultValueData() +{ + FString DefaultValue = GraphPinObj->GetDefaultAsString(); + if (DefaultValue.StartsWith(TEXT("(PinName=")) && DefaultValue.EndsWith(TEXT(")"))) + { + DefaultValue.Split(TEXT("PinName=\""), nullptr, &DefaultValue); + DefaultValue.Split(TEXT("\""), &DefaultValue, nullptr); + } + + // Preserve previous selection + if (PinNames.Num() > 0) + { + for (TSharedPtr PinName : PinNames) + { + if (*PinName.Get() == *DefaultValue) + { + CurrentlySelectedName = PinName; + break; + } + } + } +} + +void SFlowPinHandle::ComboBoxSelectionChanged(const TSharedPtr NameItem, ESelectInfo::Type SelectInfo) const +{ + const FName Name = NameItem.IsValid() ? *NameItem : NAME_None; + if (const UEdGraphSchema* Schema = (GraphPinObj ? GraphPinObj->GetSchema() : nullptr)) + { + const FString ValueString = TEXT("(PinName=\"") + Name.ToString() + TEXT("\")"); + + if (GraphPinObj->GetDefaultAsString() != ValueString) + { + const FScopedTransaction Transaction(LOCTEXT("ChangePinValue", "Change Pin Value")); + GraphPinObj->Modify(); + + Schema->TrySetDefaultValue(*GraphPinObj, ValueString); + } + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp b/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp new file mode 100644 index 000000000..30d9da6cc --- /dev/null +++ b/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp @@ -0,0 +1,262 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +// NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. +// If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule + +#include "UnrealExtensions/IFlowCuratedNamePropertyCustomization.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "EditorClassUtils.h" +#include "IDetailPropertyRow.h" +#include "IDetailChildrenBuilder.h" +#include "Internationalization/Text.h" +#include "PropertyHandle.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Text/STextBlock.h" + + +// IFlowCuratedNamePropertyCustomization Implementation + +TSharedPtr IFlowCuratedNamePropertyCustomization::NoneAsText = nullptr; + +void IFlowCuratedNamePropertyCustomization::Initialize() +{ + // Cache off "None" as a sharable FText, for use later + if (!NoneAsText.IsValid()) + { + NoneAsText = MakeShared(FText::FromName(NAME_None)); + } + + // Cache the Name property handle + check(StructPropertyHandle.IsValid()); + CachedNameHandle = GetCuratedNamePropertyHandle(); + check(CachedNameHandle->IsValidHandle()); + + // Initial setup the CachedTextSelected and CachedTextList + // (via SetCuratedNameWithSideEffects) + check(!CachedTextSelected.IsValid()); + check(CachedTextList.IsEmpty()); + + const FName CuratedName = GetCuratedName(); + const bool bChangedValue = TrySetCuratedNameWithSideEffects(CuratedName); + + check(bChangedValue); + check(CachedTextSelected.IsValid()); + check(CachedTextList.Num() == 1); +} + +void IFlowCuratedNamePropertyCustomization::CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils & StructCustomizationUtils) +{ + // Do one-time setup first + Initialize(); + + // Replace the default HeaderRow widget with one of our own + HeaderRow + .NameContent() + [ + SAssignNew(HeaderTextBlock, STextBlock) + .Text(BuildHeaderText()) + ] + .ValueContent() + .MaxDesiredWidth(250.0f) + [ + SAssignNew(TextListWidget, SComboBox>) + .OptionsSource(&CachedTextList) + .OnGenerateWidget(this, &IFlowCuratedNamePropertyCustomization::GenerateTextListWidget) + .OnComboBoxOpening(this, &IFlowCuratedNamePropertyCustomization::OnTextListComboBoxOpening) + .OnSelectionChanged(this, &IFlowCuratedNamePropertyCustomization::OnTextSelected) + [ + SNew(STextBlock) + .Text(this, &IFlowCuratedNamePropertyCustomization::GetCachedText) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText(this, &IFlowCuratedNamePropertyCustomization::GetCachedText) + ] + ]; + + // Hook-up the ResetToDefault overrides + FIsResetToDefaultVisible IsResetVisible = + FIsResetToDefaultVisible::CreateSP( + this, + &IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisible); + FResetToDefaultHandler ResetHandler = + FResetToDefaultHandler::CreateSP( + this, + &IFlowCuratedNamePropertyCustomization::CustomResetToDefault); + FResetToDefaultOverride ResetOverride = FResetToDefaultOverride::Create(IsResetVisible, ResetHandler); + + HeaderRow.OverrideResetToDefault(ResetOverride); +} + +bool IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisible(TSharedPtr Property) const +{ + const FName CuratedName = GetCuratedName(); + return !CuratedName.IsNone(); +} + +void IFlowCuratedNamePropertyCustomization::CustomResetToDefault(TSharedPtr Property) +{ + if (TrySetCuratedNameWithSideEffects(NAME_None)) + { + RepaintTextListWidget(); + } +} + +bool IFlowCuratedNamePropertyCustomization::TrySetCuratedNameWithSideEffects(const FName& NewName) +{ + const FName ExistingName = GetCuratedName(); + + if (ExistingName != NewName) + { + // Set the new name on the actual struct first + SetCuratedName(NewName); + } + + // Ensure the FText representations are up to date + + TSharedPtr NewText = FindCachedOrCreateText(NewName); + + const bool bIsChanged = (NewText != CachedTextSelected); + + CachedTextSelected = NewText; + + InsertAtHeadOfCachedTextList(CachedTextSelected); + + // Set the Name property to the new value + check(CachedNameHandle.IsValid()); + CachedNameHandle->SetValue(NewName); + + return bIsChanged; +} + +FText IFlowCuratedNamePropertyCustomization::GetCachedText() const +{ + check(CachedTextSelected.IsValid()); + + return *CachedTextSelected.Get(); +} + +TSharedRef IFlowCuratedNamePropertyCustomization::GenerateTextListWidget(TSharedPtr InItem) +{ + return + SNew(STextBlock) + .Text(*InItem) + .ColorAndOpacity(FSlateColor::UseForeground()) + .Font(IDetailLayoutBuilder::GetDetailFont()); +} + +void IFlowCuratedNamePropertyCustomization::OnTextListComboBoxOpening() +{ + check(CachedTextSelected.IsValid()); + + // Create a dictionary of Names to their shared FTexts + // (to preserve the shared FText objects, if they already exist) + TMap> MapNameToText; + + const FName CurrentName = GetCuratedName(); + MapNameToText.Add(CurrentName, CachedTextSelected); + + for (TSharedPtr& Text : CachedTextList) + { + (void) MapNameToText.FindOrAdd(FName(Text.Get()->ToString()), Text); + } + + TArray CuratedNameOptions = GetCuratedNameOptions(); + + // (+2 to reserve space for the Selected and None entry) + CachedTextList.Empty(CuratedNameOptions.Num() + 2); + + // Populate the current selection at the top of the list + if (CuratedNameOptions.Contains(CurrentName) || CurrentName.IsNone()) + { + CachedTextList.Add(CachedTextSelected); + } + + // Populate the other curated name options + for (const FName& NameOption : CuratedNameOptions) + { + if (!NameOption.IsNone() && NameOption != CurrentName) + { + AddToCachedTextList(FindCachedOrCreateText(NameOption)); + } + } + + // Ensure "None" is in the list (if CurrentName is not None) + if (!CurrentName.IsNone()) + { + check(!CachedTextList.Contains(NoneAsText)); + + CachedTextList.Add(NoneAsText); + } +} + +void IFlowCuratedNamePropertyCustomization::OnTextSelected(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo) +{ + // Called when the combo box has selected a new element + + // Process NewSelection and derive the matching Name + // (NewSelection can be null) + + FName NewName; + + if (NewSelection.IsValid()) + { + // Ensure NewSelection is in the CachedTextList + AddToCachedTextList(NewSelection); + + NewName = FName(NewSelection->ToString()); + } + else + { + NewName = NAME_None; + } + + if (TrySetCuratedNameWithSideEffects(NewName)) + { + RepaintTextListWidget(); + } +} + +TSharedPtr IFlowCuratedNamePropertyCustomization::FindCachedOrCreateText(const FName& NewName) +{ + if (NewName.IsNone()) + { + return NoneAsText; + } + + const FText NewText = FText::FromName(NewName); + + for (int32 Index = 0; Index < CachedTextList.Num(); ++Index) + { + const TSharedPtr& TextCur = CachedTextList[Index]; + + if (TextCur->EqualTo(NewText, ETextComparisonLevel::Default)) + { + return TextCur; + } + } + + TSharedPtr Result = MakeShareable(new FText(NewText)); + return Result; +} + +void IFlowCuratedNamePropertyCustomization::InsertAtHeadOfCachedTextList(TSharedPtr Text) +{ + CachedTextList.Remove(Text); + + CachedTextList.Insert(Text, 0); +} + +void IFlowCuratedNamePropertyCustomization::AddToCachedTextList(TSharedPtr Text) +{ + CachedTextList.AddUnique(Text); +} + +void IFlowCuratedNamePropertyCustomization::RepaintTextListWidget() +{ + if (TextListWidget.IsValid()) + { + // Prod UDE to refresh the widget to show the new change + TextListWidget->Invalidate(EInvalidateWidgetReason::Paint); + } +} diff --git a/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp b/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp new file mode 100644 index 000000000..033a27179 --- /dev/null +++ b/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp @@ -0,0 +1,82 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +// NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. +// If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule + +#include "UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h" + +#include "DetailWidgetRow.h" +#include "IDetailChildrenBuilder.h" +#include "IDetailPropertyRow.h" +#include "Widgets/Text/STextBlock.h" + + +// IFlowExtendedPropertyTypeCustomization Implementation + +void IFlowExtendedPropertyTypeCustomization::CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + StructPropertyHandle = InStructPropertyHandle; + + // Connect our property callback to any of the children properties changing + uint32 NumChildren; + StructPropertyHandle->GetNumChildren(NumChildren); + + for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) + { + const TSharedRef ChildHandle = StructPropertyHandle->GetChildHandle(ChildIndex).ToSharedRef(); + + ChildHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &IFlowExtendedPropertyTypeCustomization::OnAnyChildPropertyChanged)); + } + + CreateHeaderRowWidget(HeaderRow, StructCustomizationUtils); +} + +void IFlowExtendedPropertyTypeCustomization::CustomizeChildrenDefaultImpl(TSharedRef PropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // A 'default' implementation of CustomizeChildren + + uint32 NumChildren = 0; + PropertyHandle->GetNumChildren(NumChildren); + + for (uint32 ChildNum = 0; ChildNum < NumChildren; ++ChildNum) + { + StructBuilder.AddProperty(PropertyHandle->GetChildHandle(ChildNum).ToSharedRef()); + } +} + +void IFlowExtendedPropertyTypeCustomization::CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // Build the Slate widget for the header row + HeaderRow + .NameContent() + [ + SAssignNew(HeaderTextBlock, STextBlock) + .Text(BuildHeaderText()) + ]; +} + +void IFlowExtendedPropertyTypeCustomization::OnAnyChildPropertyChanged() +{ + RefreshHeader(); +} + +void IFlowExtendedPropertyTypeCustomization::RefreshHeader() const +{ + if (HeaderTextBlock.IsValid() && StructPropertyHandle.IsValid()) + { + HeaderTextBlock->SetText(BuildHeaderText()); + } +} + +FText IFlowExtendedPropertyTypeCustomization::BuildHeaderText() const +{ + if (StructPropertyHandle.IsValid()) + { + return StructPropertyHandle->GetPropertyDisplayName(); + } + else + { + return FText(); + } +} diff --git a/Source/FlowEditor/Private/LevelEditor/SLevelEditorFlow.cpp b/Source/FlowEditor/Private/Utils/SLevelEditorFlow.cpp similarity index 62% rename from Source/FlowEditor/Private/LevelEditor/SLevelEditorFlow.cpp rename to Source/FlowEditor/Private/Utils/SLevelEditorFlow.cpp index ca076ec83..b31eb942a 100644 --- a/Source/FlowEditor/Private/LevelEditor/SLevelEditorFlow.cpp +++ b/Source/FlowEditor/Private/Utils/SLevelEditorFlow.cpp @@ -1,12 +1,14 @@ -#include "LevelEditor/SLevelEditorFlow.h" +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Utils/SLevelEditorFlow.h" #include "FlowAsset.h" #include "FlowComponent.h" #include "FlowWorldSettings.h" +#include "Graph/FlowGraphSettings.h" + #include "Editor.h" -#include "Framework/MultiBox/MultiBoxDefs.h" #include "PropertyCustomizationHelpers.h" -#include "SLevelOfDetailBranchNode.h" #define LOCTEXT_NAMESPACE "SLevelEditorFlow" @@ -23,8 +25,7 @@ void SLevelEditorFlow::OnMapOpened(const FString& Filename, bool bAsTemplate) void SLevelEditorFlow::CreateFlowWidget() { - UFlowComponent* FlowComponent = FindFlowComponent(); - if (FlowComponent && FlowComponent->RootFlow) + if (UFlowComponent* FlowComponent = FindFlowComponent(); FlowComponent && FlowComponent->RootFlow) { FlowPath = FName(*FlowComponent->RootFlow->GetPathName()); } @@ -33,40 +34,17 @@ void SLevelEditorFlow::CreateFlowWidget() FlowPath = FName(); } - const TSharedRef FlowWidget = SNew(SHorizontalBox) + ChildSlot + [ + SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SObjectPropertyEntryBox) - .AllowedClass(UFlowAsset::StaticClass()) + .AllowedClass(UFlowGraphSettings::Get()->WorldAssetClass) .DisplayThumbnail(false) .OnObjectChanged(this, &SLevelEditorFlow::OnFlowChanged) .ObjectPath(this, &SLevelEditorFlow::GetFlowPath) - ]; - - ChildSlot - [ - SNew(SLevelOfDetailBranchNode) - .UseLowDetailSlot(FMultiBoxSettings::UseSmallToolBarIcons) - .LowDetail() - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - [ - FlowWidget - ] - ] - .HighDetail() - [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() - .VAlign(VAlign_Top) - .Padding(5.0f) - [ - FlowWidget - ] ] ]; } @@ -75,8 +53,7 @@ void SLevelEditorFlow::OnFlowChanged(const FAssetData& NewAsset) { FlowPath = NewAsset.ObjectPath; - UFlowComponent* FlowComponent = FindFlowComponent(); - if (FlowComponent) + if (UFlowComponent* FlowComponent = FindFlowComponent()) { if (UObject* NewObject = NewAsset.GetAsset()) { @@ -99,12 +76,11 @@ FString SLevelEditorFlow::GetFlowPath() const UFlowComponent* SLevelEditorFlow::FindFlowComponent() const { - if (UWorld* World = GEditor->GetEditorWorldContext().World()) + if (const UWorld* World = GEditor->GetEditorWorldContext().World()) { if (const AWorldSettings* WorldSettings = World->GetWorldSettings()) { - UActorComponent* FoundComponent = WorldSettings->GetComponentByClass(UFlowComponent::StaticClass()); - if (FoundComponent) + if (UActorComponent* FoundComponent = WorldSettings->GetComponentByClass(UFlowComponent::StaticClass())) { return Cast(FoundComponent); } diff --git a/Source/FlowEditor/Public/Asset/AssetTypeActions_FlowAsset.h b/Source/FlowEditor/Public/Asset/AssetTypeActions_FlowAsset.h index 6ae3b99cf..dfab704c8 100644 --- a/Source/FlowEditor/Public/Asset/AssetTypeActions_FlowAsset.h +++ b/Source/FlowEditor/Public/Asset/AssetTypeActions_FlowAsset.h @@ -1,9 +1,13 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "AssetTypeActions_Base.h" #include "Toolkits/IToolkitHost.h" -class FAssetTypeActions_FlowAsset : public FAssetTypeActions_Base +#include "FlowEditorDefines.h" + +class FLOWEDITOR_API FAssetTypeActions_FlowAsset : public FAssetTypeActions_Base { public: virtual FText GetName() const override; @@ -12,4 +16,12 @@ class FAssetTypeActions_FlowAsset : public FAssetTypeActions_Base virtual UClass* GetSupportedClass() const override; virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor = TSharedPtr()) override; + + /** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Visual-Diff + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9659 + */ +#if ENABLE_FLOW_DIFF + virtual void PerformAssetDiff(UObject* OldAsset, UObject* NewAsset, const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision) const override; +#endif }; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetEditor.h b/Source/FlowEditor/Public/Asset/FlowAssetEditor.h index 41d6bbdbd..93a8c80c0 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetEditor.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetEditor.h @@ -1,12 +1,17 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "EditorUndoClient.h" -#include "GraphEditor.h" #include "Misc/NotifyHook.h" #include "Toolkits/AssetEditorToolkit.h" #include "Toolkits/IToolkitHost.h" #include "UObject/GCObject.h" +#include "FlowEditorDefines.h" + +class FFlowMessageLog; +class SFlowGraphEditor; class SFlowPalette; class UFlowAsset; class UFlowGraphNode; @@ -19,27 +24,38 @@ struct FSlateBrush; struct FPropertyChangedEvent; struct Rect; -class FFlowAssetEditor : public FAssetEditorToolkit, - public FEditorUndoClient, - public FGCObject, - public FNotifyHook +class FLOWEDITOR_API FFlowAssetEditor : public FAssetEditorToolkit, public FEditorUndoClient, public FGCObject, public FNotifyHook { - /** The FlowAsset asset being inspected */ +public: + /** The tab ids for all the tabs used */ + static const FName DetailsTab; + static const FName GraphTab; + static const FName PaletteTab; + static const FName RuntimeLogTab; + static const FName SearchTab; + static const FName ValidationLogTab; + +protected: + /** The Flow Asset being edited */ UFlowAsset* FlowAsset; TSharedPtr AssetToolbar; - TSharedPtr Debugger; - TSharedPtr DebuggerToolbar; - TSharedPtr FocusedGraphEditor; + TSharedPtr GraphEditor; TSharedPtr DetailsView; TSharedPtr Palette; -public: - /** The tab ids for all the tabs used */ - static const FName DetailsTab; - static const FName GraphTab; - static const FName PaletteTab; +#if ENABLE_SEARCH_IN_ASSET_EDITOR + TSharedPtr SearchBrowser; +#endif + + /** Runtime message log, with the log listing that it reflects */ + TSharedPtr RuntimeLog; + TSharedPtr RuntimeLogListing; + + /** Asset Validation message log, with the log listing that it reflects */ + TSharedPtr ValidationLog; + TSharedPtr ValidationLogListing; private: /** The current UI selection state of this editor */ @@ -47,12 +63,18 @@ class FFlowAssetEditor : public FAssetEditorToolkit, public: FFlowAssetEditor(); - virtual ~FFlowAssetEditor(); + virtual ~FFlowAssetEditor() override; - UFlowAsset* GetFlowAsset() const { return FlowAsset; }; + UFlowAsset* GetFlowAsset() const { return FlowAsset; } + TSharedPtr GetFlowGraph() const { return GraphEditor; } // FGCObject virtual void AddReferencedObjects(FReferenceCollector& Collector) override; + + virtual FString GetReferencerName() const override + { + return TEXT("FFlowAssetEditor"); + } // -- // FEditorUndoClient @@ -76,143 +98,66 @@ class FFlowAssetEditor : public FAssetEditorToolkit, virtual void UnregisterTabSpawners(const TSharedRef& TabManager) override; // -- + // FAssetEditorToolkit + virtual void InitToolMenuContext(FToolMenuContext& MenuContext) override; + virtual void PostRegenerateMenusAndToolbars() override; + // -- + + bool IsTabFocused(const FTabId& TabId) const; + private: TSharedRef SpawnTab_Details(const FSpawnTabArgs& Args) const; - TSharedRef SpawnTab_GraphCanvas(const FSpawnTabArgs& Args) const; + TSharedRef SpawnTab_Graph(const FSpawnTabArgs& Args) const; TSharedRef SpawnTab_Palette(const FSpawnTabArgs& Args) const; + TSharedRef SpawnTab_RuntimeLog(const FSpawnTabArgs& Args) const; +#if ENABLE_SEARCH_IN_ASSET_EDITOR + TSharedRef SpawnTab_Search(const FSpawnTabArgs& Args) const; +#endif + TSharedRef SpawnTab_ValidationLog(const FSpawnTabArgs& Args) const; public: /** Edits the specified FlowAsset object */ void InitFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit); -private: - void AddFlowAssetToolbar(); - void BindAssetCommands(); - - void AddPlayWorldToolbar() const; - - void CreateFlowDebugger(); - void BindDebuggerCommands(); - protected: + virtual void CreateToolbar(); + virtual void BindToolbarCommands(); + virtual void RefreshAsset(); - virtual void GoToMasterInstance(); - virtual bool CanGoToMasterInstance(); - private: - void CreateWidgets(); - TSharedRef CreateGraphWidget(); + void ValidateAsset_Internal(); protected: - FGraphAppearanceInfo GetGraphAppearanceInfo() const; - virtual FText GetCornerText() const; + virtual void ValidateAsset(FFlowMessageLog& MessageLog); -private: - void BindGraphCommands(); - void UndoGraphAction(); - void RedoGraphAction(); +#if ENABLE_SEARCH_IN_ASSET_EDITOR + virtual void SearchInAsset(); +#endif - FReply OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2D& InPosition, UEdGraph* InGraph); + void EditAssetDefaults_Clicked() const; -public: - /** Gets the UI selection state of this editor */ - FName GetUISelectionState() const { return CurrentUISelection; } - void SetUISelectionState(const FName SelectionOwner); + virtual void GoToParentInstance(); + virtual bool CanGoToParentInstance(); - virtual void ClearSelectionStateFor(const FName SelectionOwner); + virtual void CreateWidgets(); + virtual void CreateGraphWidget(); -private: - void OnCreateComment() const; - void OnStraightenConnections() const; - -public: static bool CanEdit(); - static bool IsPIE(); - static EVisibility GetDebuggerVisibility(); - - TSet GetSelectedFlowNodes() const; - int32 GetNumberOfSelectedNodes() const; - bool GetBoundsForSelectedNodes(class FSlateRect& Rect, float Padding) const; - -private: - void OnSelectedNodesChanged(const TSet& Nodes); - -public: - void SelectSingleNode(UEdGraphNode* Node) const; - -private: - void SelectAllNodes() const; - bool CanSelectAllNodes() const; - - void DeleteSelectedNodes(); - void DeleteSelectedDuplicatableNodes(); - bool CanDeleteNodes() const; - - void CopySelectedNodes() const; - bool CanCopyNodes() const; - - void CutSelectedNodes(); - bool CanCutNodes() const; - - void PasteNodes(); public: - void PasteNodesHere(const FVector2D& Location); - bool CanPasteNodes() const; - -private: - void DuplicateNodes(); - bool CanDuplicateNodes() const; - - void OnNodeDoubleClicked(class UEdGraphNode* Node) const; - void OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged); - - void RefreshContextPins() const; - bool CanRefreshContextPins() const; - - void AddInput() const; - bool CanAddInput() const; - - void AddOutput() const; - bool CanAddOutput() const; - - void RemovePin() const; - bool CanRemovePin() const; - - void OnAddBreakpoint() const; - void OnAddPinBreakpoint() const; - - bool CanAddBreakpoint() const; - bool CanAddPinBreakpoint() const; - - void OnRemoveBreakpoint() const; - void OnRemovePinBreakpoint() const; - - bool CanRemoveBreakpoint() const; - bool CanRemovePinBreakpoint() const; - - void OnEnableBreakpoint() const; - void OnEnablePinBreakpoint() const; - - bool CanEnableBreakpoint() const; - bool CanEnablePinBreakpoint() const; - - void OnDisableBreakpoint() const; - void OnDisablePinBreakpoint() const; - - bool CanDisableBreakpoint() const; - bool CanDisablePinBreakpoint() const; - - void OnToggleBreakpoint() const; - void OnTogglePinBreakpoint() const; + void SetUISelectionState(const FName SelectionOwner); + virtual void ClearSelectionStateFor(const FName SelectionOwner); + FName GetUISelectionState() const; - bool CanToggleBreakpoint() const; - bool CanTogglePinBreakpoint() const; + virtual void OnSelectedNodesChanged(const TSet& Nodes) {} - void FocusViewport() const; - bool CanFocusViewport() const; +#if ENABLE_JUMP_TO_INNER_OBJECT + // FAssetEditorToolkit + virtual void JumpToInnerObject(UObject* InnerObject) override; + // -- +#endif - void JumpToNodeDefinition() const; - bool CanJumpToNodeDefinition() const; +protected: + void OnLogTokenClicked(const TSharedRef& Token) const; }; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetEditorContext.h b/Source/FlowEditor/Public/Asset/FlowAssetEditorContext.h new file mode 100644 index 000000000..f26d03ca4 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowAssetEditorContext.h @@ -0,0 +1,21 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "CoreMinimal.h" +#include "FlowAssetEditorContext.generated.h" + +class UFlowAsset; +class FFlowAssetEditor; + +UCLASS() +class FLOWEDITOR_API UFlowAssetEditorContext : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category="Tool Menus") + UFlowAsset* GetFlowAsset() const; + + TWeakPtr FlowAssetEditor; +}; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetFactory.h b/Source/FlowEditor/Public/Asset/FlowAssetFactory.h index 5dc8e3c3b..5e6fd011a 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetFactory.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetFactory.h @@ -1,12 +1,22 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Factories/Factory.h" #include "FlowAssetFactory.generated.h" -UCLASS(HideCategories = Object, MinimalAPI) -class UFlowAssetFactory : public UFactory +UCLASS(HideCategories = Object) +class FLOWEDITOR_API UFlowAssetFactory : public UFactory { GENERATED_UCLASS_BODY() + UPROPERTY(EditAnywhere, Category = Asset) + TSubclassOf AssetClass; + + virtual bool ConfigureProperties() override; virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + +protected: + // Parameterized guts of ConfigureProperties() + bool ConfigurePropertiesInternal(const FText& TitleText); }; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetIndexer.h b/Source/FlowEditor/Public/Asset/FlowAssetIndexer.h new file mode 100644 index 000000000..4b97b0fb4 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowAssetIndexer.h @@ -0,0 +1,28 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9070 + */ +#include "FlowEditorDefines.h" +#if ENABLE_FLOW_SEARCH +#include "CoreMinimal.h" +#include "IAssetIndexer.h" + +class UFlowAsset; +class FSearchSerializer; + +class FLOWEDITOR_API FFlowAssetIndexer : public IAssetIndexer +{ +public: + virtual FString GetName() const override { return TEXT("FlowAsset"); } + virtual int32 GetVersion() const override; + virtual void IndexAsset(const UObject* InAssetObject, FSearchSerializer& Serializer) const override; + +private: + // Variant of FBlueprintIndexer::IndexGraphs + void IndexGraph(const UFlowAsset* InFlowAsset, FSearchSerializer& Serializer) const; +}; +#endif diff --git a/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h b/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h index 2e611701a..d9bda86d9 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h @@ -1,22 +1,94 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once -#include "CoreMinimal.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Navigation/SBreadcrumbTrail.h" -class FExtender; -class FToolBarBuilder; -class SWidget; +#include "FlowAsset.h" class FFlowAssetEditor; +////////////////////////////////////////////////////////////////////////// +// Flow Asset Instance List + +class FLOWEDITOR_API SFlowAssetInstanceList : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SFlowAssetInstanceList) {} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TWeakObjectPtr InTemplateAsset); + virtual ~SFlowAssetInstanceList() override; + + static EVisibility GetDebuggerVisibility(); + +private: + void RefreshInstances(); + + TSharedRef OnGenerateWidget(TSharedPtr Item) const; + void OnSelectionChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectionType); + FText GetSelectedInstanceName() const; + + TWeakObjectPtr TemplateAsset; + TSharedPtr>> Dropdown; + + TArray> InstanceNames; + TSharedPtr SelectedInstance; + + static FText NoInstanceSelectedText; +}; + +////////////////////////////////////////////////////////////////////////// +// Flow Asset Breadcrumb + +/** + * The kind of breadcrumbs that Flow Debugger uses + */ +struct FLOWEDITOR_API FFlowBreadcrumb +{ + const FString AssetPathName; + const FName InstanceName; + + FFlowBreadcrumb() + : AssetPathName(FString()) + , InstanceName(NAME_None) + {} + + explicit FFlowBreadcrumb(const TWeakObjectPtr FlowAsset) + : AssetPathName(FlowAsset->GetTemplateAsset()->GetPathName()) + , InstanceName(FlowAsset->GetDisplayName()) + {} +}; + +class FLOWEDITOR_API SFlowAssetBreadcrumb : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SFlowAssetInstanceList) {} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TWeakObjectPtr InTemplateAsset); + +private: + void OnCrumbClicked(const FFlowBreadcrumb& Item) const; + + TWeakObjectPtr TemplateAsset; + TSharedPtr> BreadcrumbTrail; +}; + ////////////////////////////////////////////////////////////////////////// // Flow Asset Toolbar -class FFlowAssetToolbar final : public TSharedFromThis +class FLOWEDITOR_API FFlowAssetToolbar : public TSharedFromThis { public: - FFlowAssetToolbar(TSharedPtr InNodeEditor); + explicit FFlowAssetToolbar(const TSharedPtr InAssetEditor, UToolMenu* ToolbarMenu); - void AddToolbar(FToolBarBuilder& ToolbarBuilder); +private: + void BuildAssetToolbar(UToolMenu* ToolbarMenu) const; + TSharedRef MakeDiffMenu() const; + + void BuildDebuggerToolbar(UToolMenu* ToolbarMenu) const; private: TWeakPtr FlowAssetEditor; diff --git a/Source/FlowEditor/Public/Asset/FlowDebugger.h b/Source/FlowEditor/Public/Asset/FlowDebugger.h deleted file mode 100644 index 01a8336c1..000000000 --- a/Source/FlowEditor/Public/Asset/FlowDebugger.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" - -/** -** Minimalistic form of breakpoint debugger -** See BehaviorTreeDebugger for a more complex example - */ -class FFlowDebugger -{ -public: - FFlowDebugger(); - ~FFlowDebugger(); - - static void PausePlaySession(); - static bool IsPlaySessionPaused(); -}; diff --git a/Source/FlowEditor/Public/Asset/FlowDebuggerSubsystem.h b/Source/FlowEditor/Public/Asset/FlowDebuggerSubsystem.h new file mode 100644 index 000000000..dae10ebae --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowDebuggerSubsystem.h @@ -0,0 +1,37 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "EditorSubsystem.h" +#include "Logging/TokenizedMessage.h" +#include "FlowDebuggerSubsystem.generated.h" + +class UFlowAsset; +class FFlowMessageLog; + +/** +** Persistent subsystem supporting Flow Graph debugging + */ +UCLASS() +class FLOWEDITOR_API UFlowDebuggerSubsystem : public UEditorSubsystem +{ + GENERATED_BODY() + +public: + UFlowDebuggerSubsystem(); + +protected: + TMap, TSharedPtr> RuntimeLogs; + + void OnInstancedTemplateAdded(UFlowAsset* FlowAsset); + void OnInstancedTemplateRemoved(UFlowAsset* FlowAsset); + + void OnRuntimeMessageAdded(UFlowAsset* FlowAsset, const TSharedRef& Message) const; + + void OnBeginPIE(const bool bIsSimulating); + void OnEndPIE(const bool bIsSimulating); + +public: + static void PausePlaySession(); + static bool IsPlaySessionPaused(); +}; diff --git a/Source/FlowEditor/Public/Asset/FlowDebuggerToolbar.h b/Source/FlowEditor/Public/Asset/FlowDebuggerToolbar.h deleted file mode 100644 index 15037329c..000000000 --- a/Source/FlowEditor/Public/Asset/FlowDebuggerToolbar.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "Widgets/Input/SComboBox.h" -#include "Widgets/Navigation/SBreadcrumbTrail.h" - -#include "FlowAsset.h" - -class FExtender; -class FToolBarBuilder; -class SWidget; - -class FFlowAssetEditor; - -////////////////////////////////////////////////////////////////////////// -// Flow Instance List - -class SFlowInstanceList final : public SCompoundWidget -{ -public: - SLATE_BEGIN_ARGS(SFlowInstanceList) {} - SLATE_END_ARGS() - - void Construct(const FArguments& InArgs, TWeakPtr InFlowAssetEditor); - -private: - TSharedRef OnGenerateWidget(TSharedPtr Item) const; - void OnSelectionChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectionType); - FText GetSelectedInstanceName() const; - - TWeakPtr FlowAssetEditor; - TSharedPtr>> Dropdown; - - TArray> InstanceNames; - TSharedPtr SelectedInstance; - - static FText NoInstanceSelectedText; -}; - -////////////////////////////////////////////////////////////////////////// -// Flow Breadcrumb - -/** - * The kind of breadcrumbs that Flow Debugger uses - */ -struct FFlowBreadcrumb -{ - FString AssetPathName; - FName InstanceName; - - FFlowBreadcrumb() - : AssetPathName(FString()) - , InstanceName(NAME_None) - {} - - FFlowBreadcrumb(const UFlowAsset* FlowAsset) - : AssetPathName(FlowAsset->GetTemplateAsset()->GetPathName()) - , InstanceName(FlowAsset->GetDisplayName()) - {} -}; - -class SFlowBreadcrumb final : public SCompoundWidget -{ -public: - SLATE_BEGIN_ARGS(SFlowInstanceList) {} - SLATE_END_ARGS() - - void Construct(const FArguments& InArgs, TWeakPtr InFlowAssetEditor); - -private: - void OnCrumbClicked(const FFlowBreadcrumb& Item); - FText GetBreadcrumbText(const TWeakObjectPtr FlowInstance) const; - - TWeakPtr FlowAssetEditor; - TSharedPtr> BreadcrumbTrail; -}; - -////////////////////////////////////////////////////////////////////////// -// Flow Debugger Toolbar - -class FFlowDebuggerToolbar final : public TSharedFromThis -{ -public: - FFlowDebuggerToolbar(TSharedPtr InNodeEditor); - - void AddToolbar(FToolBarBuilder& ToolbarBuilder); - TSharedPtr GetFlowInstanceList() const { return FlowInstanceList; } - -private: - TWeakPtr FlowAssetEditor; - - TSharedPtr FlowInstanceList; - TSharedPtr FlowBreadcrumb; -}; diff --git a/Source/FlowEditor/Public/Asset/FlowDiffControl.h b/Source/FlowEditor/Public/Asset/FlowDiffControl.h new file mode 100644 index 000000000..f8fa7b6b7 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowDiffControl.h @@ -0,0 +1,72 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Visual-Diff + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9659 + */ + +#include "FlowEditorDefines.h" +#if ENABLE_FLOW_DIFF +#include "DiffResults.h" +#include "IAssetTypeActions.h" +#include "Kismet/Private/DiffControl.h" + +struct FDiffResultItem; +class UEdGraph; +struct FEdGraphEditAction; + +class UFlowAsset; +class SFlowDiff; + +///////////////////////////////////////////////////////////////////////////// +/// FFlowAssetDiffControl +class FLOWEDITOR_API FFlowAssetDiffControl : public TDetailsDiffControl +{ +public: + FFlowAssetDiffControl(const UFlowAsset* InOldFlowAsset, const UFlowAsset* InNewFlowAsset, FOnDiffEntryFocused InSelectionCallback); + + virtual void GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) override; +}; + +///////////////////////////////////////////////////////////////////////////// +/// FFlowGraphToDiff: engine's FGraphToDiff customized to Flow Graph +struct FLOWEDITOR_API FFlowGraphToDiff : public TSharedFromThis, IDiffControl +{ + FFlowGraphToDiff(class SFlowDiff* DiffWidget, UEdGraph* GraphOld, UEdGraph* GraphNew, const FRevisionInfo& RevisionOld, const FRevisionInfo& RevisionNew); + virtual ~FFlowGraphToDiff() override; + + /** Add widgets to the differences tree */ + virtual void GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) override; + + UEdGraph* GetGraphOld() const { return GraphOld; }; + UEdGraph* GetGraphNew() const { return GraphNew; }; + + /** Source for list view */ + TArray> DiffListSource; + TSharedPtr> FoundDiffs; + + /** Index of the first item in RealDifferences that was generated by this graph */ + int32 RealDifferencesStartIndex = INDEX_NONE; + +private: + FText GetToolTip() const; + TSharedRef GenerateCategoryWidget() const; + + /** Called when the Newer Graph is modified*/ + void OnGraphChanged(const FEdGraphEditAction& Action) const; + + void BuildDiffSourceArray(); + + class SFlowDiff* DiffWidget; + UEdGraph* GraphOld; + UEdGraph* GraphNew; + + /** Description of Old and new graph */ + FRevisionInfo RevisionOld; + FRevisionInfo RevisionNew; + + FDelegateHandle OnGraphChangedDelegateHandle; +}; +#endif \ No newline at end of file diff --git a/Source/FlowEditor/Public/Asset/FlowImportUtils.h b/Source/FlowEditor/Public/Asset/FlowImportUtils.h new file mode 100644 index 000000000..3ef1799b7 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowImportUtils.h @@ -0,0 +1,70 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "FlowAsset.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Nodes/FlowPin.h" +#include "FlowImportUtils.generated.h" + +// Helper structure allowing to recreate blueprint graph as Flow Graph +USTRUCT() +struct FLOWEDITOR_API FImportedGraphNode +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + UEdGraphNode* SourceGraphNode; + + TMultiMap Incoming; + TMultiMap Outgoing; + + FImportedGraphNode() + : SourceGraphNode(nullptr) + { + } +}; + +// Helper structure allowing to copy properties from blueprint function pin to the Flow Node property of different name +USTRUCT(BlueprintType) +struct FLOWEDITOR_API FBlueprintToFlowPinName +{ + GENERATED_USTRUCT_BODY() + + // Key represents Flow Node property name + // Value represents Input Pin name of blueprint function + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pins") + TMap NodePropertiesToFunctionPins; + + FBlueprintToFlowPinName() + { + } +}; + +/** + * Groundwork for converting blueprint graphs to Flow Graph. + * It's NOT meant to be universal, out-of-box solution as complexity of blueprint graphs conflicts with simplicity of Flow Graph. + * However, it might useful to provide this basic utility to anyone who would like to batch-convert their custom blueprint-based event system to Flow Graph. + * Pull requests are welcome if you able to improve this utility w/o with minimal amount of code. + */ +UCLASS(meta = (ScriptName = "FlowImportUtils")) +class FLOWEDITOR_API UFlowImportUtils : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + static TMap> FunctionsToFlowNodes; + static TMap, FBlueprintToFlowPinName> PinMappings; + + UFUNCTION(BlueprintCallable, Category = "FlowImportUtils") + static UFlowAsset* ImportBlueprintGraph(UObject* BlueprintAsset, const TSubclassOf FlowAssetClass, const FString FlowAssetName, + const TMap> InFunctionsToFlowNodes, const TMap, FBlueprintToFlowPinName> InPinMappings, const FName StartEventName = TEXT("BeginPlay")); + + static void ImportBlueprintGraph(UBlueprint* Blueprint, UFlowAsset* FlowAsset, const FName StartEventName = TEXT("BeginPlay")); + static void ImportBlueprintFunction(const UFlowAsset* FlowAsset, const FImportedGraphNode& NodeImport, const TMap& SourceNodes, TMap& TargetNodes); + + static void GetValidInputPins(const UEdGraphNode* GraphNode, TMap& Result); + static const UEdGraphPin* FindPinMatchingToProperty(UClass* FlowNodeClass, const FProperty* Property, const TMapPins); +}; diff --git a/Source/FlowEditor/Public/Asset/FlowMessageLogListing.h b/Source/FlowEditor/Public/Asset/FlowMessageLogListing.h new file mode 100644 index 000000000..b7e7aafaf --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowMessageLogListing.h @@ -0,0 +1,37 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "IMessageLogListing.h" + +#include "FlowAsset.h" + +UENUM() +enum class EFlowLogType : uint8 +{ + Runtime, + Validation +}; + +/** + * Scope wrapper for the message log. Ensures we don't leak logs that we dont need (i.e. those that have no messages) + * Replicated after FScopedBlueprintMessageLog + */ +class FLOWEDITOR_API FFlowMessageLogListing +{ +public: + FFlowMessageLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + ~FFlowMessageLogListing(); + +public: + TSharedRef Log; + FName LogName; + +private: + static TSharedRef RegisterLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + static FName GetListingName(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + +public: + static TSharedRef GetLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + static FString GetLogLabel(const EFlowLogType Type); +}; diff --git a/Source/FlowEditor/Public/Asset/SAssetRevisionMenu.h b/Source/FlowEditor/Public/Asset/SAssetRevisionMenu.h new file mode 100644 index 000000000..4bd540150 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/SAssetRevisionMenu.h @@ -0,0 +1,53 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "ISourceControlProvider.h" +#include "Widgets/SCompoundWidget.h" + +class FUpdateStatus; +struct FRevisionInfo; + +// Forced to make a variant of SBlueprintRevisionMenu, only to replace to UBlueprint* parameter +class FLOWEDITOR_API SAssetRevisionMenu : public SCompoundWidget +{ + DECLARE_DELEGATE_TwoParams(FOnRevisionSelected, FRevisionInfo const& RevisionInfo, const FString& InFilename) + +public: + SLATE_BEGIN_ARGS(SAssetRevisionMenu) + : _bIncludeLocalRevision(false) + { + } + + SLATE_ARGUMENT(bool, bIncludeLocalRevision) + SLATE_EVENT(FOnRevisionSelected, OnRevisionSelected) + SLATE_END_ARGS() + + virtual ~SAssetRevisionMenu() override; + + void Construct(const FArguments& InArgs, const FString& InFilename); + +private: + /** Delegate used to determine the visibility 'in progress' widgets */ + EVisibility GetInProgressVisibility() const; + /** Delegate used to determine the visibility of the cancel button */ + EVisibility GetCancelButtonVisibility() const; + + /** Delegate used to cancel a source control operation in progress */ + FReply OnCancelButtonClicked() const; + /** Callback for when the source control operation is complete */ + void OnSourceControlQueryComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult); + + /** */ + bool bIncludeLocalRevision = false; + /** */ + FOnRevisionSelected OnRevisionSelected; + /** The name of the file we want revision info for */ + FString Filename; + /** The box we are using to display our menu */ + TSharedPtr MenuBox; + /** The source control operation in progress */ + TSharedPtr SourceControlQueryOp; + /** The state of the SCC query */ + uint32 SourceControlQueryState = 0; +}; diff --git a/Source/FlowEditor/Public/Asset/SFlowDiff.h b/Source/FlowEditor/Public/Asset/SFlowDiff.h new file mode 100644 index 000000000..c054204ba --- /dev/null +++ b/Source/FlowEditor/Public/Asset/SFlowDiff.h @@ -0,0 +1,224 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Visual-Diff + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9659 + */ + +#include "FlowEditorDefines.h" +#if ENABLE_FLOW_DIFF +#include "SDetailsDiff.h" + +struct FFlowGraphToDiff; +class UFlowAsset; + +enum class EAssetEditorCloseReason : uint8; + +namespace FlowDiffUtils +{ + FLOWEDITOR_API void SelectNextRow(SListView>& ListView, const TArray>& ListViewSource); + FLOWEDITOR_API void SelectPrevRow(SListView>& ListView, const TArray>& ListViewSource); + FLOWEDITOR_API bool HasNextDifference(const SListView>& ListView, const TArray>& ListViewSource); + FLOWEDITOR_API bool HasPrevDifference(const SListView>& ListView, const TArray>& ListViewSource); +} + +/** Panel used to display the asset */ +struct FLOWEDITOR_API FFlowDiffPanel +{ + FFlowDiffPanel(); + + /** Generate a panel for NewGraph diffed against OldGraph */ + void GeneratePanel(UEdGraph* NewGraph, UEdGraph* OldGraph); + + /** Generate a panel that displays the Graph and reflects the items in the DiffResults */ + void GeneratePanel(UEdGraph* Graph, TSharedPtr> DiffResults, TAttribute FocusedDiffResult); + + /** Called when user hits keyboard shortcut to copy nodes */ + void CopySelectedNodes() const; + + /** Gets whatever nodes are selected in the Graph Editor */ + FGraphPanelSelectionSet GetSelectedNodes() const; + + /** Can user copy any of the selected nodes? */ + bool CanCopyNodes() const; + + /** Functions used to focus/find a particular change in a diff result */ + void FocusDiff(const UEdGraphPin& Pin) const; + void FocusDiff(const UEdGraphNode& Node) const; + + /** The Flow Asset that owns the graph we are showing */ + const UFlowAsset* FlowAsset; + + /** The box around the graph editor, used to change the content when new graphs are set */ + TSharedPtr GraphEditorBox; + + /** The details view associated with the graph editor */ + TSharedPtr DetailsView; + + /** The graph editor which does the work of displaying the graph */ + TWeakPtr GraphEditor; + + /** Revision information for this asset */ + FRevisionInfo RevisionInfo; + + /** True if we should show a name identifying which asset this panel is displaying */ + bool bShowAssetName; + + /** The widget that contains the revision info in graph mode */ + TSharedPtr OverlayGraphRevisionInfo; +private: + /** Command list for this diff panel */ + TSharedPtr GraphEditorCommands; +}; + +/* Visual Diff between two Flow Assets */ +class FLOWEDITOR_API SFlowDiff : public SCompoundWidget +{ +public: + DECLARE_DELEGATE_TwoParams(FOpenInDefaults, const class UFlowAsset*, const class UFlowAsset*); + + SLATE_BEGIN_ARGS(SFlowDiff) + { + } + + SLATE_ARGUMENT(const class UFlowAsset*, OldFlow) + SLATE_ARGUMENT(const class UFlowAsset*, NewFlow) + SLATE_ARGUMENT(struct FRevisionInfo, OldRevision) + SLATE_ARGUMENT(struct FRevisionInfo, NewRevision) + SLATE_ARGUMENT(bool, ShowAssetNames) + SLATE_ARGUMENT(TSharedPtr, ParentWindow) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + virtual ~SFlowDiff() override; + + /** Called when a new Graph is clicked on by user */ + void OnGraphChanged(const FFlowGraphToDiff* Diff); + + /** Called when user clicks on a new graph list item */ + void OnGraphSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectionType); + + /** Called when user clicks on an entry in the listview of differences */ + void OnDiffListSelectionChanged(TSharedPtr TheDiff); + + /** Helper function for generating an empty widget */ + static TSharedRef DefaultEmptyPanel(); + + /** Helper function to create a window that holds a diff widget */ + static TSharedPtr CreateDiffWindow(const FText WindowTitle, const UFlowAsset* OldFlow, const UFlowAsset* NewFlow, const struct FRevisionInfo& OldRevision, const struct FRevisionInfo& NewRevision); + +protected: + /** Called when user clicks button to go to next difference */ + void NextDiff() const; + + /** Called when user clicks button to go to prev difference */ + void PrevDiff() const; + + /** Called to determine whether we have a list of differences to cycle through */ + bool HasNextDiff() const; + bool HasPrevDiff() const; + + /** Find the FGraphToDiff that displays the graph with GraphPath relative path */ + FFlowGraphToDiff* FindGraphToDiffEntry(const FString& GraphPath) const; + + /** Bring these revisions of graph into focus on main display*/ + void FocusOnGraphRevisions(const FFlowGraphToDiff* Diff); + + /** User toggles the option to lock the views between the two assets */ + void OnToggleLockView(); + + /** User toggles the option to change the split view mode between vertical and horizontal */ + void OnToggleSplitViewMode(); + + /** Reset the graph editor, called when user switches graphs to display*/ + void ResetGraphEditors() const; + + /** Get the image to show for the toggle lock option*/ + FSlateIcon GetLockViewImage() const; + + /** Get the image to show for the toggle split view mode option*/ + FSlateIcon GetSplitViewModeImage() const; + + /** List of graphs to diff, are added to panel last */ + TSharedPtr GraphToDiff; + + /** Get Graph editor associated with this Graph */ + FFlowDiffPanel& GetDiffPanelForNode(const UEdGraphNode& Node); + + /** Event handler that updates the graph view when user selects a new graph */ + void HandleGraphChanged(const FString& GraphPath); + + /** Function used to generate the list of differences and the widgets needed to calculate that list */ + void GenerateDifferencesList(); + + /** Called when editor may need to be closed */ + void OnCloseAssetEditor(UObject* Asset, const EAssetEditorCloseReason CloseReason); + + struct FDiffControl + { + FDiffControl() + : Widget() + , DiffControl(nullptr) + { + } + + TSharedPtr Widget; + TSharedPtr DiffControl; + }; + + FDiffControl GenerateDetailsPanel(); + FDiffControl GenerateGraphPanel(); + + TSharedRef GenerateGraphWidgetForPanel(FFlowDiffPanel& OutDiffPanel) const; + TSharedRef GenerateRevisionInfoWidgetForPanel(TSharedPtr& OutGeneratedWidget, const FText& InRevisionText) const; + + /** Accessor and event handler for toggling between diff view modes (defaults, components, graph view, interface, macro): */ + void SetCurrentMode(FName NewMode); + FName GetCurrentMode() const { return CurrentMode; } + void OnModeChanged(const FName& InNewViewMode) const; + + void UpdateTopSectionVisibility(const FName& InNewViewMode) const; + + FName CurrentMode; + + /*The two panels used to show the old & new revision*/ + FFlowDiffPanel PanelOld, PanelNew; + + /** If the two views should be locked */ + bool bLockViews; + + /** If the view on Graph Mode should be divided vertically */ + bool bVerticalSplitGraphMode = true; + + /** Contents widget that we swap when mode changes (defaults, components, etc) */ + TSharedPtr ModeContents; + + TSharedPtr TopRevisionInfoWidget; + TSharedPtr DiffGraphSplitter; + TSharedPtr GraphToolBarWidget; + + friend struct FListItemGraphToDiff; + + /** We can't use the global tab manager because we need to instance the diff control, so we have our own tab manager: */ + TSharedPtr TabManager; + + /** Tree of differences collected across all panels: */ + TArray> PrimaryDifferencesList; + + /** List of all differences, cached so that we can iterate only the differences and not labels, etc: */ + TArray> RealDifferences; + + /** Tree view that displays the differences, cached for the buttons that iterate the differences: */ + TSharedPtr>> DifferencesTreeView; + + /** Stored references to widgets used to display various parts of a asset, from the mode name */ + TMap ModePanels; + + /** A pointer to the window holding this */ + TWeakPtr WeakParentWindow; + + FDelegateHandle AssetEditorCloseDelegate; +}; +#endif diff --git a/Source/FlowEditor/Private/Asset/FlowAssetDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetDetails.h similarity index 84% rename from Source/FlowEditor/Private/Asset/FlowAssetDetails.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowAssetDetails.h index ccc2d33d8..016151dd1 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetDetails.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetDetails.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "IDetailCustomization.h" @@ -17,7 +19,7 @@ class FFlowAssetDetails final : public IDetailCustomization } // IDetailCustomization - virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; // -- private: diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_ComponentObserverDetails.h similarity index 82% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowNode_ComponentObserverDetails.h index ca84f4745..c3450294f 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_ComponentObserverDetails.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "IDetailCustomization.h" diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomEventBaseDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomEventBaseDetails.h new file mode 100644 index 000000000..ce5f7d4f1 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomEventBaseDetails.h @@ -0,0 +1,38 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "IDetailCustomization.h" +#include "Templates/SharedPointer.h" +#include "Types/SlateEnums.h" +#include "Widgets/Input/SComboBox.h" + +class IDetailCategoryBuilder; +class UFlowAsset; + +class FFlowNode_CustomEventBaseDetails : public IDetailCustomization +{ +public: + // IDetailCustomization + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + // -- + +protected: + void CustomizeDetailsInternal(IDetailLayoutBuilder& DetailLayout, const FText& CustomRowNameText, const FText& EventNameText); + + virtual IDetailCategoryBuilder& CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const = 0; + virtual TArray BuildEventNames(const UFlowAsset& FlowAsset) const = 0; + + void OnComboBoxOpening(); + void RebuildEventNames(); + TSharedRef GenerateEventWidget(TSharedPtr Item) const; + FText GetSelectedEventText() const; + void PinSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo); + bool IsInEventNames(const FName& EventName) const; + + TArray> ObjectsBeingEdited; + TArray> EventNames; + TSharedPtr CachedEventNameSelected; + TSharedPtr>> EventTextListWidget; + bool bExcludeReferencedEvents = false; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomInputDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomInputDetails.h new file mode 100644 index 000000000..7fe0c4a6e --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomInputDetails.h @@ -0,0 +1,25 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "FlowNode_CustomEventBaseDetails.h" +#include "Templates/SharedPointer.h" + +class FFlowNode_CustomInputDetails final : public FFlowNode_CustomEventBaseDetails +{ +public: + FFlowNode_CustomInputDetails(); + + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowNode_CustomInputDetails()); + } + + // IDetailCustomization + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + // -- + +protected: + virtual IDetailCategoryBuilder& CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const override; + virtual TArray BuildEventNames(const UFlowAsset& FlowAsset) const override; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomOutputDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomOutputDetails.h new file mode 100644 index 000000000..6e90889f7 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomOutputDetails.h @@ -0,0 +1,25 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "FlowNode_CustomEventBaseDetails.h" +#include "Templates/SharedPointer.h" + +class FFlowNode_CustomOutputDetails final : public FFlowNode_CustomEventBaseDetails +{ +public: + FFlowNode_CustomOutputDetails(); + + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowNode_CustomOutputDetails()); + } + + // IDetailCustomization + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + // -- + +protected: + virtual IDetailCategoryBuilder& CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const override; + virtual TArray BuildEventNames(const UFlowAsset& FlowAsset) const override; +}; diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h similarity index 82% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h index 6d55c2971..ce85a66d6 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "IDetailCustomization.h" diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h similarity index 82% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h index c76781877..efb935389 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "IDetailCustomization.h" diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_SubGraphDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_SubGraphDetails.h new file mode 100644 index 000000000..323e0b8d1 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_SubGraphDetails.h @@ -0,0 +1,16 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "IDetailCustomization.h" + +class FFlowNode_SubGraphDetails final : public IDetailCustomization +{ +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowNode_SubGraphDetails); + } + + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowOwnerFunctionRefCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowOwnerFunctionRefCustomization.h new file mode 100644 index 000000000..d3cb3f2a2 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowOwnerFunctionRefCustomization.h @@ -0,0 +1,53 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "UnrealExtensions/IFlowCuratedNamePropertyCustomization.h" + +#include "FlowOwnerFunctionRef.h" + + +// Forward Declaration +class UFlowAsset; +class UFlowNode; +class UObject; +class UClass; +class UFunction; +class UFlowNode_CallOwnerFunction; + + +// Details customization for FFlowOwnerFunctionRef +class FFlowOwnerFunctionRefCustomization : public IFlowCuratedNamePropertyCustomization +{ +private: + typedef IFlowCuratedNamePropertyCustomization Super; + +public: + static TSharedRef MakeInstance() { return MakeShareable(new FFlowOwnerFunctionRefCustomization()); } + +protected: + + //~Begin IPropertyTypeCustomization + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + //~End IPropertyTypeCustomization + + //~Begin ICuratedNamePropertyCustomization + virtual TSharedPtr GetCuratedNamePropertyHandle() const override; + virtual void SetCuratedName(const FName& NewName) override; + virtual FName GetCuratedName() const override; + virtual TArray GetCuratedNameOptions() const override; + //~End ICuratedNamePropertyCustomization + + // Accessor to return the actual struct being edited + FORCEINLINE FFlowOwnerFunctionRef* GetFlowOwnerFunctionRef() const + { return IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle); } + + const UClass* TryGetExpectedOwnerClass() const; + UFlowNode* TryGetFlowNodeOuter() const; + + static TArray GetFlowOwnerFunctionRefs(const UFlowNode_CallOwnerFunction& FlowNodeOwner, const UClass& ExpectedOwnerClass); + + static bool IsFunctionUsable(const UFunction& Function, const UFlowNode_CallOwnerFunction& FlowNodeOwner); + static bool DoesFunctionHaveExpectedParamType(const UFunction& Function, const UFlowNode_CallOwnerFunction& FlowNodeOwner); + +}; diff --git a/Source/FlowEditor/Public/FlowEditorCommands.h b/Source/FlowEditor/Public/FlowEditorCommands.h index 9f3045992..3612577da 100644 --- a/Source/FlowEditor/Public/FlowEditorCommands.h +++ b/Source/FlowEditor/Public/FlowEditorCommands.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "EdGraph/EdGraphSchema.h" @@ -5,19 +7,24 @@ #include "Framework/Commands/UICommandInfo.h" #include "Templates/SharedPointer.h" -class FFlowToolbarCommands final : public TCommands +class FLOWEDITOR_API FFlowToolbarCommands : public TCommands { public: FFlowToolbarCommands(); TSharedPtr RefreshAsset; - TSharedPtr GoToMasterInstance; + TSharedPtr ValidateAsset; + + TSharedPtr SearchInAsset; + TSharedPtr EditAssetDefaults; + + TSharedPtr GoToParentInstance; virtual void RegisterCommands() override; }; /** Generic graph commands for the flow graph */ -class FFlowGraphCommands final : public TCommands +class FLOWEDITOR_API FFlowGraphCommands : public TCommands { public: FFlowGraphCommands(); @@ -37,6 +44,12 @@ class FFlowGraphCommands final : public TCommands TSharedPtr DisablePinBreakpoint; TSharedPtr TogglePinBreakpoint; + /** Execution Override */ + TSharedPtr EnableNode; + TSharedPtr DisableNode; + TSharedPtr SetPassThrough; + TSharedPtr ForcePinActivation; + /** Jumps */ TSharedPtr FocusViewport; TSharedPtr JumpToNodeDefinition; @@ -45,7 +58,7 @@ class FFlowGraphCommands final : public TCommands }; /** Handles spawning nodes by keyboard shortcut */ -class FFlowSpawnNodeCommands : public TCommands +class FLOWEDITOR_API FFlowSpawnNodeCommands : public TCommands { public: FFlowSpawnNodeCommands(); diff --git a/Source/FlowEditor/Public/FlowEditorDefines.h b/Source/FlowEditor/Public/FlowEditorDefines.h new file mode 100644 index 000000000..e991dd4f2 --- /dev/null +++ b/Source/FlowEditor/Public/FlowEditorDefines.h @@ -0,0 +1,33 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Visual-Diff + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9659 + */ +#define ENABLE_FLOW_DIFF 0 + +/** +* Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search +* Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9070 + */ +#define ENABLE_FLOW_SEARCH 0 + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9882 + */ +#define ENABLE_JUMP_TO_INNER_OBJECT 0 + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9943 + */ +#define ENABLE_SEARCH_IN_ASSET_EDITOR 0 + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Import-Utils + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/10004 + */ +#define ENABLE_ASYNC_NODES_IMPORT 0 diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index fe91883ef..2bc6c5a61 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -1,9 +1,10 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "AssetTypeCategories.h" #include "IAssetTypeActions.h" #include "Modules/ModuleInterface.h" -#include "Toolkits/AssetEditorToolkit.h" class FSlateStyleSet; struct FGraphPanelPinConnectionFactory; @@ -11,24 +12,6 @@ struct FGraphPanelPinConnectionFactory; class FFlowAssetEditor; class UFlowAsset; -struct FExtensibilityManagers -{ - TSharedPtr MenuExtensibilityManager; - TSharedPtr ToolBarExtensibilityManager; - - void Init() - { - MenuExtensibilityManager = MakeShareable(new FExtensibilityManager); - ToolBarExtensibilityManager = MakeShareable(new FExtensibilityManager); - } - - void Reset() - { - MenuExtensibilityManager.Reset(); - ToolBarExtensibilityManager.Reset(); - } -}; - DECLARE_LOG_CATEGORY_EXTERN(LogFlowEditor, Log, All) class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface @@ -39,8 +22,7 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface private: TArray> RegisteredAssetActions; TSet CustomClassLayouts; - TSharedPtr FlowGraphConnectionFactory; - FExtensibilityManagers FlowAssetExtensibility; + TSet CustomStructLayouts; public: virtual void StartupModule() override; @@ -50,18 +32,22 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface void RegisterAssets(); void UnregisterAssets(); - void RegisterPropertyCustomizations() const; + void RegisterDetailCustomizations(); + void UnregisterDetailCustomizations(); + void RegisterCustomClassLayout(const TSubclassOf Class, const FOnGetDetailCustomizationInstance DetailLayout); + void RegisterCustomStructLayout(const UScriptStruct& Struct, const FOnGetPropertyTypeCustomizationInstance DetailLayout); public: - TSharedPtr GetFlowAssetMenuExtensibilityManager() const; - TSharedPtr GetFlowAssetToolBarExtensibilityManager() const; - FDelegateHandle FlowTrackCreateEditorHandle; + FDelegateHandle ModulesChangedHandle; private: + void ModulesChangesCallback(FName ModuleName, EModuleChangeReason ReasonForChange) const; + void RegisterAssetIndexers() const; + void CreateFlowToolbar(FToolBarBuilder& ToolbarBuilder) const; public: - TSharedRef CreateFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UFlowAsset* FlowAsset); + static TSharedRef CreateFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UFlowAsset* FlowAsset); }; diff --git a/Source/FlowEditor/Public/FlowEditorStyle.h b/Source/FlowEditor/Public/FlowEditorStyle.h index b25dbbb0b..93b45ec05 100644 --- a/Source/FlowEditor/Public/FlowEditorStyle.h +++ b/Source/FlowEditor/Public/FlowEditorStyle.h @@ -1,8 +1,10 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Styling/SlateStyle.h" -class FFlowEditorStyle +class FLOWEDITOR_API FFlowEditorStyle { public: static TSharedPtr Get() { return StyleSet; } diff --git a/Source/FlowEditor/Public/Graph/FlowGraph.h b/Source/FlowEditor/Public/Graph/FlowGraph.h index e41e22f8a..7c1803481 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraph.h +++ b/Source/FlowEditor/Public/Graph/FlowGraph.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "EdGraph/EdGraph.h" @@ -5,10 +7,10 @@ #include "FlowAsset.h" #include "FlowGraph.generated.h" -class FLOWEDITOR_API FFlowGraphInterface final : public IFlowGraphInterface +class FLOWEDITOR_API FFlowGraphInterface : public IFlowGraphInterface { public: - virtual ~FFlowGraphInterface() {} + virtual ~FFlowGraphInterface() override {} virtual void OnInputTriggered(UEdGraphNode* GraphNode, const int32 Index) const override; virtual void OnOutputTriggered(UEdGraphNode* GraphNode, const int32 Index) const override; @@ -20,6 +22,7 @@ class FLOWEDITOR_API UFlowGraph : public UEdGraph GENERATED_UCLASS_BODY() static UEdGraph* CreateGraph(UFlowAsset* InFlowAsset); + void RefreshGraph(); // UEdGraph virtual void NotifyGraphChanged() override; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h b/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h index f8d2ad367..410a78ae7 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h @@ -1,11 +1,20 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "ConnectionDrawingPolicy.h" #include "EdGraphUtilities.h" -struct FFlowGraphConnectionDrawingPolicyFactory : public FGraphPanelPinConnectionFactory +UENUM() +enum class EFlowConnectionDrawType : uint8 { - virtual ~FFlowGraphConnectionDrawingPolicyFactory() + Default, + Circuit +}; + +struct FLOWEDITOR_API FFlowGraphConnectionDrawingPolicyFactory : public FGraphPanelPinConnectionFactory +{ + virtual ~FFlowGraphConnectionDrawingPolicyFactory() override { } @@ -16,7 +25,7 @@ class FSlateWindowElementList; class UEdGraph; // This class draws the connections between nodes -class FFlowGraphConnectionDrawingPolicy : public FConnectionDrawingPolicy +class FLOWEDITOR_API FFlowGraphConnectionDrawingPolicy : public FConnectionDrawingPolicy { float RecentWireDuration; @@ -36,13 +45,26 @@ class FFlowGraphConnectionDrawingPolicy : public FConnectionDrawingPolicy TMap RecordedPaths; TMap SelectedPaths; + //Used to help reversing pins on nodes that go backwards + TMap RerouteToReversedDirectionMap; + public: FFlowGraphConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj); void BuildPaths(); // FConnectionDrawingPolicy interface - virtual void Draw(TMap, FArrangedWidget>& PinGeometries, FArrangedChildren& ArrangedNodes) override; + virtual void DrawConnection(int32 LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) override; virtual void DetermineWiringStyle(UEdGraphPin* OutputPin, UEdGraphPin* InputPin, FConnectionParams& Params) override; + virtual void Draw(TMap, FArrangedWidget>& PinGeometries, FArrangedChildren& ArrangedNodes) override; // End of FConnectionDrawingPolicy interface + +protected: + void DrawCircuitSpline(const int32& LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) const; + void DrawCircuitConnection(const int32& LayerId, const FVector2D& Start, const FVector2D& StartDirection, const FVector2D& End, const FVector2D& EndDirection, const FConnectionParams& Params) const; + static FVector2D GetControlPoint(const FVector2D& Source, const FVector2D& Target); + + bool ShouldChangeTangentForReroute(class UFlowGraphNode_Reroute* Reroute); + bool FindPinCenter(const UEdGraphPin* Pin, FVector2D& OutCenter) const; + bool GetAverageConnectedPosition(class UFlowGraphNode_Reroute* Reroute, EEdGraphPinDirection Direction, FVector2D& OutPos) const; }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphEditor.h b/Source/FlowEditor/Public/Graph/FlowGraphEditor.h new file mode 100644 index 000000000..ccba5175c --- /dev/null +++ b/Source/FlowEditor/Public/Graph/FlowGraphEditor.h @@ -0,0 +1,147 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "EdGraphUtilities.h" +#include "GraphEditor.h" +#include "GraphEditorActions.h" +#include "SGraphNode.h" +#include "Widgets/DeclarativeSyntaxSupport.h" + +#include "FlowGraph.h" + +class FFlowAssetEditor; +class IDetailsView; + +/** + * + */ +class FLOWEDITOR_API SFlowGraphEditor : public SGraphEditor +{ +public: + SLATE_BEGIN_ARGS(SFlowGraphEditor) + { + } + + SLATE_ARGUMENT(FGraphEditorEvents, GraphEvents) + SLATE_ARGUMENT(TSharedPtr, DetailsView) + SLATE_END_ARGS() + +protected: + TWeakObjectPtr FlowAsset; + + TWeakPtr FlowAssetEditor; + TSharedPtr DetailsView; + + TSharedPtr CommandList; + +public: + void Construct(const FArguments& InArgs, const TSharedPtr InAssetEditor); + + virtual void BindGraphCommands(); + + virtual FGraphAppearanceInfo GetGraphAppearanceInfo() const; + virtual FText GetCornerText() const; + +private: + static void UndoGraphAction(); + static void RedoGraphAction(); + + static FReply OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2D& InPosition, UEdGraph* InGraph); + void OnCreateComment() const; + +public: + static bool CanEdit(); + static bool IsPIE(); + virtual bool IsTabFocused() const; + + virtual void SelectSingleNode(UEdGraphNode* Node); + +protected: + virtual void OnSelectedNodesChanged(const TSet& Nodes); + +public: + FOnSelectionChanged OnSelectionChangedEvent; + + TSet GetSelectedFlowNodes() const; + +protected: + virtual bool CanSelectAllNodes() const { return true; } + + virtual void DeleteSelectedNodes(); + virtual void DeleteSelectedDuplicableNodes(); + virtual bool CanDeleteNodes() const; + + virtual void CopySelectedNodes() const; + virtual bool CanCopyNodes() const; + + virtual void CutSelectedNodes(); + virtual bool CanCutNodes() const; + + virtual void PasteNodes(); + +public: + virtual void PasteNodesHere(const FVector2D& Location); + virtual bool CanPasteNodes() const; + +protected: + virtual void DuplicateNodes(); + virtual bool CanDuplicateNodes() const; + + virtual void OnNodeDoubleClicked(class UEdGraphNode* Node) const; + virtual void OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged); + + virtual void RefreshContextPins() const; + virtual bool CanRefreshContextPins() const; + +private: + void AddInput() const; + bool CanAddInput() const; + + void AddOutput() const; + bool CanAddOutput() const; + + void RemovePin(); + bool CanRemovePin(); + + void OnAddBreakpoint() const; + void OnAddPinBreakpoint(); + + bool CanAddBreakpoint() const; + bool CanAddPinBreakpoint(); + + void OnRemoveBreakpoint() const; + void OnRemovePinBreakpoint(); + + bool CanRemoveBreakpoint() const; + bool CanRemovePinBreakpoint(); + + void OnEnableBreakpoint() const; + void OnEnablePinBreakpoint(); + + bool CanEnableBreakpoint(); + bool CanEnablePinBreakpoint(); + + void OnDisableBreakpoint() const; + void OnDisablePinBreakpoint(); + + bool CanDisableBreakpoint() const; + bool CanDisablePinBreakpoint(); + + void OnToggleBreakpoint() const; + void OnTogglePinBreakpoint(); + + bool CanToggleBreakpoint() const; + bool CanTogglePinBreakpoint(); + + void SetSignalMode(const EFlowSignalMode Mode) const; + bool CanSetSignalMode(const EFlowSignalMode Mode) const; + + void OnForcePinActivation(); + + void FocusViewport() const; + bool CanFocusViewport() const; + + void JumpToNodeDefinition() const; + bool CanJumpToNodeDefinition() const; +}; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h b/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h new file mode 100644 index 000000000..51d00ef27 --- /dev/null +++ b/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h @@ -0,0 +1,60 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Engine/DeveloperSettings.h" +#include "FlowGraphEditorSettings.generated.h" + +UENUM() +enum class EFlowNodeDoubleClickTarget : uint8 +{ + NodeDefinition UMETA(Tooltip = "Open node class: either blueprint or C++ class"), + PrimaryAsset UMETA(Tooltip = "Open asset defined as primary asset, i.e. Dialogue asset for PlayDialogue node") +}; + +/** + * + */ +UCLASS(Config = EditorPerProjectUserSettings, meta = (DisplayName = "Flow Graph")) +class FLOWEDITOR_API UFlowGraphEditorSettings : public UDeveloperSettings +{ + GENERATED_UCLASS_BODY() + + static UFlowGraphEditorSettings* Get() { return StaticClass()->GetDefaultObject(); } + + // Double-clicking a Flow Node might open relevant asset/code editor + UPROPERTY(config, EditAnywhere, Category = "Nodes") + EFlowNodeDoubleClickTarget NodeDoubleClickTarget; + + // Displays information on the graph node, either C++ class name or path to blueprint asset + UPROPERTY(config, EditAnywhere, Category = "Nodes") + bool bShowNodeClass; + + // Shows the node description when you play in editor + UPROPERTY(config, EditAnywhere, Category = "Nodes") + bool bShowNodeDescriptionWhilePlaying; + + // Pin names will will be displayed in a format that is easier to read, even if PinFriendlyName wasn't set + UPROPERTY(EditAnywhere, config, Category = "Nodes") + bool bEnforceFriendlyPinNames; + + // Renders preview of entire graph while hovering over + UPROPERTY(config, EditAnywhere, Category = "Nodes") + bool bShowSubGraphPreview; + + UPROPERTY(config, EditAnywhere, Category = "Nodes", meta = (EditCondition = "bShowSubGraphPreview")) + bool bShowSubGraphPath; + + UPROPERTY(config, EditAnywhere, Category = "Nodes", meta = (EditCondition = "bShowSubGraphPreview")) + FVector2D SubGraphPreviewSize; + + UPROPERTY(EditAnywhere, config, Category = "Wires") + bool bHighlightInputWiresOfSelectedNodes; + + UPROPERTY(EditAnywhere, config, Category = "Wires") + bool bHighlightOutputWiresOfSelectedNodes; + +public: + virtual FName GetCategoryName() const override { return FName("Flow Graph"); } + virtual FText GetSectionText() const override { return INVTEXT("User Settings"); } +}; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h index f961eb174..95be35b9f 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h @@ -1,8 +1,15 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "EdGraph/EdGraphSchema.h" +#include "Templates/SubclassOf.h" #include "FlowGraphSchema.generated.h" +class UFlowAsset; +class UFlowNode; +class UFlowGraphNode; + DECLARE_MULTICAST_DELEGATE(FFlowGraphSchemaRefresh); UCLASS() @@ -10,18 +17,19 @@ class FLOWEDITOR_API UFlowGraphSchema : public UEdGraphSchema { GENERATED_UCLASS_BODY() + friend class UFlowGraph; + private: + static bool bInitialGatherPerformed; static TArray NativeFlowNodes; static TMap BlueprintFlowNodes; - - static TSet UnsortedCategories; - static TArray> FlowNodeCategories; + static TMap GraphNodesByFlowNodes; - static TMap AssignedGraphNodeClasses; + static bool bBlueprintCompilationPending; public: static void SubscribeToAssetChanges(); - static void GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const FString& CategoryName); + static void GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UClass* AssetClass, const FString& CategoryName); // EdGraphSchema virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override; @@ -30,29 +38,41 @@ class FLOWEDITOR_API UFlowGraphSchema : public UEdGraphSchema virtual bool TryCreateConnection(UEdGraphPin* A, UEdGraphPin* B) const override; virtual bool ShouldHidePinDefaultValue(UEdGraphPin* Pin) const override; virtual FLinearColor GetPinTypeColor(const FEdGraphPinType& PinType) const override; + virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override; virtual void BreakNodeLinks(UEdGraphNode& TargetNode) const override; virtual void BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotification) const override; virtual int32 GetNodeSelectionCount(const UEdGraph* Graph) const override; virtual TSharedPtr GetCreateCommentAction() const override; + virtual void OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2D& GraphPosition) const override; // -- static TArray> GetFlowNodeCategories(); static UClass* GetAssignedGraphNodeClass(const UClass* FlowNodeClass); +protected: + static UFlowGraphNode* CreateDefaultNode(UEdGraph& Graph, const UFlowAsset* AssetClassDefaults, const TSubclassOf& NodeClass, const FVector2D& Offset, bool bPlacedAsGhostNode); + private: - static void GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const FString& CategoryName); + static void ApplyNodeFilter(const UFlowAsset* AssetClassDefaults, const UClass* FlowNodeClass, TArray& FilteredNodes); + static void GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* AssetClassDefaults, const FString& CategoryName); static void GetCommentAction(FGraphActionMenuBuilder& ActionMenuBuilder, const UEdGraph* CurrentGraph = nullptr); static bool IsFlowNodePlaceable(const UClass* Class); - static void GatherFlowNodes(); - static void OnHotReload(bool bWasTriggeredAutomatically); + + static void OnBlueprintPreCompile(UBlueprint* Blueprint); + static void OnBlueprintCompiled(); + static void OnHotReload(EReloadCompleteReason ReloadCompleteReason); + + static void GatherNativeNodes(); + static void GatherNodes(); static void OnAssetAdded(const FAssetData& AssetData); static void AddAsset(const FAssetData& AssetData, const bool bBatch); - static void RemoveAsset(const FAssetData& AssetData); - static void RefreshNodeList(); + static void OnAssetRemoved(const FAssetData& AssetData); public: static FFlowGraphSchemaRefresh OnNodeListChanged; - static UBlueprint* GetNodeBlueprint(const FAssetData& AssetData); + static UBlueprint* GetPlaceableNodeBlueprint(const FAssetData& AssetData); + + static const UFlowAsset* GetAssetClassDefaults(const UEdGraph* Graph); }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h b/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h index 1d7eb3ea1..3bb8d230d 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "EdGraph/EdGraphSchema.h" @@ -36,7 +38,7 @@ struct FLOWEDITOR_API FFlowGraphSchemaAction_NewNode : public FEdGraphSchemaActi } FFlowGraphSchemaAction_NewNode(const UFlowNode* Node) - : FEdGraphSchemaAction(FText::FromString(Node->GetNodeCategory()), Node->GetNodeTitle(), Node->GetNodeToolTip(), 0) + : FEdGraphSchemaAction(FText::FromString(Node->GetNodeCategory()), Node->GetNodeTitle(), Node->GetNodeToolTip(), 0, FText::FromString(Node->GetClass()->GetMetaData("Keywords"))) , NodeClass(Node->GetClass()) { } @@ -45,7 +47,9 @@ struct FLOWEDITOR_API FFlowGraphSchemaAction_NewNode : public FEdGraphSchemaActi virtual UEdGraphNode* PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode = true) override; // -- - static UFlowGraphNode* CreateNode(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode = true); + static UFlowGraphNode* CreateNode(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode = true); + static UFlowGraphNode* RecreateNode(UEdGraph* ParentGraph, UEdGraphNode* OldInstance, UFlowNode* FlowNode); + static UFlowGraphNode* ImportNode(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FGuid& NodeGuid, const FVector2D Location); }; /** Action to paste clipboard contents into the graph */ diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h index ef693731d..a21116bb6 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h @@ -1,5 +1,8 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once +#include "FlowGraphConnectionDrawingPolicy.h" #include "Engine/DeveloperSettings.h" #include "FlowTypes.h" @@ -9,10 +12,36 @@ * */ UCLASS(Config = Editor, defaultconfig, meta = (DisplayName = "Flow Graph")) -class UFlowGraphSettings final : public UDeveloperSettings +class FLOWEDITOR_API UFlowGraphSettings : public UDeveloperSettings { GENERATED_UCLASS_BODY() - static UFlowGraphSettings* Get() { return CastChecked(UFlowGraphSettings::StaticClass()->GetDefaultObject()); } + static UFlowGraphSettings* Get() { return StaticClass()->GetDefaultObject(); } + + /** Show Flow Asset in Flow category of "Create Asset" menu? + * Requires restart after making a change. */ + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) + bool bExposeFlowAssetCreation; + + /** Show Flow Node blueprint in Flow category of "Create Asset" menu? + * Requires restart after making a change. */ + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) + bool bExposeFlowNodeCreation; + + /** Show Flow Asset toolbar? + * Requires restart after making a change. */ + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) + bool bShowAssetToolbarAboveLevelEditor; + + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) + FText FlowAssetCategoryName; + + /** Use this class to create new assets. Class picker will show up if None */ + UPROPERTY(EditAnywhere, config, Category = "Default UI") + TSubclassOf DefaultFlowAssetClass; + + /** Flow Asset class allowed to be assigned via Level Editor toolbar*/ + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (EditCondition = "bShowAssetToolbarAboveLevelEditor")) + TSubclassOf WorldAssetClass; /** Hide specific nodes from the Flow Palette without changing the source code. * Requires restart after making a change. */ @@ -26,6 +55,9 @@ class UFlowGraphSettings final : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "Nodes") TMap NodeTitleColors; + UPROPERTY(Config, EditAnywhere, Category = "Nodes") + TMap, FLinearColor> NodeSpecificColors; + UPROPERTY(EditAnywhere, config, Category = "Nodes") FLinearColor ExecPinColorModifier; @@ -38,6 +70,15 @@ class UFlowGraphSettings final : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "NodePopups") FLinearColor NodePreloadedBackground; + UPROPERTY(config, EditAnywhere, Category = "Wires") + EFlowConnectionDrawType ConnectionDrawType; + + UPROPERTY(config, EditAnywhere, Category = "Wires", meta = (EditCondition = "ConnectionDrawType == EFlowConnectionDrawType::Circuit")) + float CircuitConnectionAngle; + + UPROPERTY(config, EditAnywhere, Category = "Wires", meta = (EditCondition = "ConnectionDrawType == EFlowConnectionDrawType::Circuit")) + FVector2D CircuitConnectionSpacing; + UPROPERTY(EditAnywhere, config, Category = "Wires") FLinearColor InactiveWireColor; @@ -60,15 +101,13 @@ class UFlowGraphSettings final : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "Wires", meta = (ClampMin = 0.0f)) float RecordedWireThickness; - UPROPERTY(EditAnywhere, config, Category = "Wires") - bool bHighlightInputWiresOfSelectedNodes; - - UPROPERTY(EditAnywhere, config, Category = "Wires") - bool bHighlightOutputWiresOfSelectedNodes; - UPROPERTY(EditAnywhere, config, Category = "Wires") FLinearColor SelectedWireColor; UPROPERTY(EditAnywhere, config, Category = "Wires", meta = (ClampMin = 0.0f)) float SelectedWireThickness; + +public: + virtual FName GetCategoryName() const override { return FName("Flow Graph"); } + virtual FText GetSectionText() const override { return INVTEXT("Graph Settings"); } }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphUtils.h b/Source/FlowEditor/Public/Graph/FlowGraphUtils.h index 29441e89b..87b8e126c 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphUtils.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphUtils.h @@ -1,13 +1,19 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "CoreMinimal.h" +#include "Templates/SharedPointer.h" class FFlowAssetEditor; +class SFlowGraphEditor; +class UEdGraph; class FLOWEDITOR_API FFlowGraphUtils { public: FFlowGraphUtils() {} - static TSharedPtr GetFlowAssetEditor(const UObject* ObjectToFocusOn); + static TSharedPtr GetFlowAssetEditor(const UEdGraph* Graph); + static TSharedPtr GetFlowGraphEditor(const UEdGraph* Graph); }; diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h index 6ce9e450c..8dba75821 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "EdGraph/EdGraphNode.h" @@ -9,39 +11,9 @@ #include "FlowGraphNode.generated.h" class UEdGraphSchema; - class UFlowNode; -USTRUCT() -struct FFlowBreakpoint -{ - GENERATED_USTRUCT_BODY() - - UPROPERTY() - bool bHasBreakpoint; - - bool bBreakpointEnabled; - bool bBreakpointHit; - - FFlowBreakpoint() - { - bHasBreakpoint = false; - bBreakpointEnabled = false; - bBreakpointHit = false; - }; - - void AddBreakpoint(); - void RemoveBreakpoint(); - bool HasBreakpoint() const; - - void EnableBreakpoint(); - bool CanEnableBreakpoint() const; - - void DisableBreakpoint(); - bool IsBreakpointEnabled() const; - - void ToggleBreakpoint(); -}; +DECLARE_DELEGATE(FFlowGraphNodeEvent); /** * Graph representation of the Flow Node @@ -58,16 +30,19 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode UPROPERTY(Instanced) UFlowNode* FlowNode; + bool bBlueprintCompilationPending; bool bNeedsFullReconstruction; static bool bFlowAssetsLoaded; public: - // actually, it would be intuitive to assign a custom Graph Node class in Flow Node class - // although we shouldn't assign class from editor module to runtime module class + // It would be intuitive to assign a custom Graph Node class in Flow Node class + // However, we shouldn't assign class from editor module to runtime module class UPROPERTY() TArray> AssignedNodeClasses; - void SetFlowNode(UFlowNode* InFlowNode); + void SetNodeTemplate(UFlowNode* InFlowNode); + const UFlowNode* GetNodeTemplate() const; + UFlowNode* GetFlowNode() const; // UObject @@ -85,14 +60,21 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode private: void SubscribeToExternalChanges(); + + void OnBlueprintPreCompile(UBlueprint* Blueprint); + void OnBlueprintCompiled(); + void OnExternalChange(); +public: + virtual void OnGraphRefresh(); + ////////////////////////////////////////////////////////////////////////// // Graph node public: UPROPERTY() - FFlowBreakpoint NodeBreakpoint; + FFlowPinTrait NodeBreakpoint; // UEdGraphNode virtual bool CanCreateUnderSpecifiedSchema(const UEdGraphSchema* Schema) const override; @@ -120,7 +102,8 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode // UEdGraphNode virtual void GetNodeContextMenuActions(class UToolMenu* Menu, class UGraphNodeContextMenuContext* Context) const override; - + virtual bool CanUserDeleteNode() const override; + virtual bool CanDuplicateNode() const override; virtual TSharedPtr CreateVisualWidget() override; virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; virtual FLinearColor GetNodeTitleColor() const override; @@ -133,19 +116,20 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode // Utils public: - // short summary of node's content + // Short summary of node's content FString GetNodeDescription() const; - // get flow node for the inspected asset instance + // Get flow node for the inspected asset instance UFlowNode* GetInspectedNodeInstance() const; - // used for highlighting active nodes of the inspected asset instance + // Used for highlighting active nodes of the inspected asset instance EFlowNodeState GetActivationState() const; - // information displayed while node is active + // Information displayed while node is active FString GetStatusString() const; + FLinearColor GetStatusBackgroundColor() const; - // check this to display information while node is preloaded + // Check this to display information while node is preloaded bool IsContentPreloaded() const; bool CanFocusViewport() const; @@ -163,7 +147,7 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode TArray OutputPins; UPROPERTY() - TMap PinBreakpoints; + TMap PinBreakpoints; void CreateInputPin(const FFlowPin& FlowPin, const int32 Index = INDEX_NONE); void CreateOutputPin(const FFlowPin& FlowPin, const int32 Index = INDEX_NONE); @@ -181,10 +165,10 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode void AddUserInput(); void AddUserOutput(); - // add pin only on this instance of node, under default pins - void AddInstancePin(const EEdGraphPinDirection Direction, const FName& PinName); + // Add pin only on this instance of node, under default pins + void AddInstancePin(const EEdGraphPinDirection Direction, const uint8 NumberedPinsAmount); - // call node and graph updates manually, if using bBatchRemoval + // Call node and graph updates manually, if using bBatchRemoval void RemoveInstancePin(UEdGraphPin* Pin); // Create pins from the context asset, i.e. Sequencer events @@ -207,4 +191,19 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode void OnResumePIE(const bool bIsSimulating); void OnEndPIE(const bool bIsSimulating); void ResetBreakpoints(); + +////////////////////////////////////////////////////////////////////////// +// Execution Override + +public: + FFlowGraphNodeEvent OnSignalModeChanged; + + // Pin activation forced by user during PIE + virtual void ForcePinActivation(const FEdGraphPinReference PinReference) const; + + // Pass-through forced by designer, set per node instance + virtual void SetSignalMode(const EFlowSignalMode Mode); + + virtual EFlowSignalMode GetSignalMode() const; + virtual bool CanSetSignalMode(const EFlowSignalMode Mode) const; }; diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h index ec3c5dd1f..82105197f 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h @@ -1,10 +1,12 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Graph/Nodes/FlowGraphNode.h" #include "FlowGraphNode_ExecutionSequence.generated.h" UCLASS() -class UFlowGraphNode_ExecutionSequence final : public UFlowGraphNode +class FLOWEDITOR_API UFlowGraphNode_ExecutionSequence : public UFlowGraphNode { GENERATED_UCLASS_BODY() diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h index 2124d0b02..c8e2ac632 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Graph/Nodes/FlowGraphNode.h" diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h index e69b1d158..41364c825 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Graph/Nodes/FlowGraphNode.h" diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h index 095f06406..c69e23433 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Graph/Nodes/FlowGraphNode.h" @@ -10,7 +12,5 @@ class FLOWEDITOR_API UFlowGraphNode_Start : public UFlowGraphNode // UEdGraphNode virtual TSharedPtr CreateVisualWidget() override; - virtual bool CanUserDeleteNode() const override { return false; } - virtual bool CanDuplicateNode() const override { return false; } // -- }; diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_SubGraph.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_SubGraph.h new file mode 100644 index 000000000..ae9f1c682 --- /dev/null +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_SubGraph.h @@ -0,0 +1,16 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Graph/Nodes/FlowGraphNode.h" +#include "FlowGraphNode_SubGraph.generated.h" + +UCLASS() +class FLOWEDITOR_API UFlowGraphNode_SubGraph : public UFlowGraphNode +{ + GENERATED_UCLASS_BODY() + + // UEdGraphNode + virtual TSharedPtr CreateVisualWidget() override; + // -- +}; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h index dd21aab64..9ddda3fe3 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "SGraphNode.h" @@ -5,7 +7,7 @@ #include "Graph/Nodes/FlowGraphNode.h" -class SFlowGraphPinExec final : public SGraphPinExec +class FLOWEDITOR_API SFlowGraphPinExec : public SGraphPinExec { public: SFlowGraphPinExec(); @@ -16,7 +18,7 @@ class SFlowGraphPinExec final : public SGraphPinExec void Construct(const FArguments& InArgs, UEdGraphPin* InPin); }; -class SFlowGraphNode : public SGraphNode +class FLOWEDITOR_API SFlowGraphNode : public SGraphNode { public: SLATE_BEGIN_ARGS(SFlowGraphNode) {} @@ -31,19 +33,36 @@ class SFlowGraphNode : public SGraphNode virtual void GetOverlayBrushes(bool bSelected, const FVector2D WidgetSize, TArray& Brushes) const override; // -- - virtual void GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const FFlowBreakpoint& Breakpoint, TArray& Brushes) const; + virtual void GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const FFlowPinTrait& Breakpoint, TArray& Brushes) const; // SGraphNode virtual void UpdateGraphNode() override; + virtual void UpdateErrorInfo() override; + + virtual TSharedRef CreateTitleWidget(TSharedPtr NodeTitle) override; virtual TSharedRef CreateNodeContentArea() override; virtual const FSlateBrush* GetNodeBodyBrush() const override; + // purposely overriden non-virtual methods, added PR #9791 to made these methods virtual: https://github.com/EpicGames/UnrealEngine/pull/9791 + FSlateColor GetNodeTitleColor() const; + FSlateColor GetNodeBodyColor() const; + FSlateColor GetNodeTitleIconColor() const; + FLinearColor GetNodeTitleTextColor() const; + TSharedPtr GetEnabledStateWidget() const; + virtual void CreateStandardPinWidget(UEdGraphPin* Pin) override; + virtual TSharedPtr GetComplexTooltip() override; + virtual void CreateInputSideAddButton(TSharedPtr OutputBox) override; virtual void CreateOutputSideAddButton(TSharedPtr OutputBox) override; - virtual FReply OnAddPin() override; // -- + + // Variant of SGraphNode::AddPinButtonContent + virtual void AddPinButton(TSharedPtr OutputBox, TSharedRef ButtonContent, const EEdGraphPinDirection Direction, FString DocumentationExcerpt = FString(), TSharedPtr CustomTooltip = nullptr); + + // Variant of SGraphNode::OnAddPin + virtual FReply OnAddFlowPin(const EEdGraphPinDirection Direction); -private: +protected: UFlowGraphNode* FlowGraphNode = nullptr; }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h index 7ce3ce865..d054d4a37 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h @@ -1,7 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Graph/Widgets/SFlowGraphNode.h" -class SFlowGraphNode_Finish : public SFlowGraphNode +class FLOWEDITOR_API SFlowGraphNode_Finish : public SFlowGraphNode { }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h index f5baa839f..52a959239 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h @@ -1,7 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Graph/Widgets/SFlowGraphNode.h" -class SFlowGraphNode_Start : public SFlowGraphNode +class FLOWEDITOR_API SFlowGraphNode_Start : public SFlowGraphNode { }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_SubGraph.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_SubGraph.h new file mode 100644 index 000000000..9a97dd3de --- /dev/null +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_SubGraph.h @@ -0,0 +1,13 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Graph/Widgets/SFlowGraphNode.h" + +class FLOWEDITOR_API SFlowGraphNode_SubGraph : public SFlowGraphNode +{ +protected: + // SGraphNode + virtual TSharedPtr GetComplexTooltip() override; + // -- +}; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h index 55066f3e4..dff23d85f 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "SGraphPalette.h" @@ -5,7 +7,7 @@ class FFlowAssetEditor; /** Widget displaying a single item */ -class SFlowPaletteItem : public SGraphPaletteItem +class FLOWEDITOR_API SFlowPaletteItem : public SGraphPaletteItem { public: SLATE_BEGIN_ARGS(SFlowPaletteItem) {} @@ -19,14 +21,14 @@ class SFlowPaletteItem : public SGraphPaletteItem }; /** Flow Palette */ -class SFlowPalette : public SGraphPalette +class FLOWEDITOR_API SFlowPalette : public SGraphPalette { public: SLATE_BEGIN_ARGS(SFlowPalette) {} SLATE_END_ARGS() void Construct(const FArguments& InArgs, TWeakPtr InFlowAssetEditor); - virtual ~SFlowPalette(); + virtual ~SFlowPalette() override; protected: void Refresh(); @@ -40,13 +42,13 @@ class SFlowPalette : public SGraphPalette FString GetFilterCategoryName() const; void CategorySelectionChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo); - void OnActionSelected(const TArray>& InActions, ESelectInfo::Type InSelectionType); + void OnActionSelected(const TArray>& InActions, ESelectInfo::Type InSelectionType) const; public: void ClearGraphActionMenuSelection() const; protected: - TWeakPtr FlowAssetEditorPtr; + TWeakPtr FlowAssetEditor; TArray> CategoryNames; TSharedPtr CategoryComboBox; }; diff --git a/Source/FlowEditor/Private/MovieScene/FlowSection.h b/Source/FlowEditor/Public/MovieScene/FlowSection.h similarity index 79% rename from Source/FlowEditor/Private/MovieScene/FlowSection.h rename to Source/FlowEditor/Public/MovieScene/FlowSection.h index e32c5078b..b630ca77c 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowSection.h +++ b/Source/FlowEditor/Public/MovieScene/FlowSection.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "ISequencerSection.h" @@ -5,7 +7,7 @@ class FSequencerSectionPainter; -class FFlowSectionBase : public FSequencerSection +class FLOWEDITOR_API FFlowSectionBase : public FSequencerSection { public: FFlowSectionBase(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) @@ -24,7 +26,7 @@ class FFlowSectionBase : public FSequencerSection /** * An implementation of flow sections. */ -class FFlowSection final : public FFlowSectionBase +class FLOWEDITOR_API FFlowSection : public FFlowSectionBase { public: FFlowSection(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) @@ -35,7 +37,7 @@ class FFlowSection final : public FFlowSectionBase virtual int32 OnPaintSection(FSequencerSectionPainter& Painter) const override; }; -class FFlowTriggerSection : public FFlowSectionBase +class FLOWEDITOR_API FFlowTriggerSection : public FFlowSectionBase { public: FFlowTriggerSection(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) @@ -46,7 +48,7 @@ class FFlowTriggerSection : public FFlowSectionBase virtual int32 OnPaintSection(FSequencerSectionPainter& Painter) const override; }; -class FFlowRepeaterSection : public FFlowSectionBase +class FLOWEDITOR_API FFlowRepeaterSection : public FFlowSectionBase { public: FFlowRepeaterSection(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) diff --git a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.h b/Source/FlowEditor/Public/MovieScene/FlowTrackEditor.h similarity index 88% rename from Source/FlowEditor/Private/MovieScene/FlowTrackEditor.h rename to Source/FlowEditor/Public/MovieScene/FlowTrackEditor.h index 84abd9c23..c6e32d5a0 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.h +++ b/Source/FlowEditor/Public/MovieScene/FlowTrackEditor.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "CoreMinimal.h" @@ -12,7 +14,7 @@ class FMenuBuilder; /** * A property track editor for named events. */ -class FFlowTrackEditor final : public FMovieSceneTrackEditor +class FLOWEDITOR_API FFlowTrackEditor : public FMovieSceneTrackEditor { public: /** @@ -45,7 +47,7 @@ class FFlowTrackEditor final : public FMovieSceneTrackEditor void AddFlowSubMenu(FMenuBuilder& MenuBuilder); /** Callback for executing the "Add Event Track" menu entry. */ - void HandleAddFlowTrackMenuEntryExecute(UClass* SectionType); + void HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) const; void CreateNewSection(UMovieSceneTrack* Track, int32 RowIndex, UClass* SectionType, bool bSelect) const; }; diff --git a/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h b/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h index 1d2a2a447..e8243cf42 100644 --- a/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h +++ b/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h @@ -1,8 +1,10 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "AssetTypeActions/AssetTypeActions_Blueprint.h" -class FAssetTypeActions_FlowNodeBlueprint final : public FAssetTypeActions_Blueprint +class FLOWEDITOR_API FAssetTypeActions_FlowNodeBlueprint : public FAssetTypeActions_Blueprint { public: virtual FText GetName() const override; diff --git a/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h b/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h index 1a966bc1f..d3b8a129e 100644 --- a/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h +++ b/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h @@ -1,10 +1,12 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "Factories/Factory.h" #include "FlowNodeBlueprintFactory.generated.h" -UCLASS(hidecategories=Object, MinimalAPI) -class UFlowNodeBlueprintFactory : public UFactory +UCLASS(hidecategories=Object) +class FLOWEDITOR_API UFlowNodeBlueprintFactory : public UFactory { GENERATED_UCLASS_BODY() diff --git a/Source/FlowEditor/Public/Pins/SFlowInputPinHandle.h b/Source/FlowEditor/Public/Pins/SFlowInputPinHandle.h new file mode 100644 index 000000000..5355265bb --- /dev/null +++ b/Source/FlowEditor/Public/Pins/SFlowInputPinHandle.h @@ -0,0 +1,18 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "EdGraphUtilities.h" +#include "SFlowPinHandle.h" + +class FLOWEDITOR_API SFlowInputPinHandle : public SFlowPinHandle +{ +protected: + virtual void RefreshNameList() override; +}; + +class FFlowInputPinHandleFactory final : public FGraphPanelPinFactory +{ +public: + virtual TSharedPtr CreatePin(class UEdGraphPin* InPin) const override; +}; diff --git a/Source/FlowEditor/Public/Pins/SFlowOutputPinHandle.h b/Source/FlowEditor/Public/Pins/SFlowOutputPinHandle.h new file mode 100644 index 000000000..a291ac83c --- /dev/null +++ b/Source/FlowEditor/Public/Pins/SFlowOutputPinHandle.h @@ -0,0 +1,18 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "EdGraphUtilities.h" +#include "SFlowPinHandle.h" + +class FLOWEDITOR_API SFlowOutputPinHandle : public SFlowPinHandle +{ +protected: + virtual void RefreshNameList() override; +}; + +class FFlowOutputPinHandleFactory final : public FGraphPanelPinFactory +{ +public: + virtual TSharedPtr CreatePin(class UEdGraphPin* InPin) const override; +}; diff --git a/Source/FlowEditor/Public/Pins/SFlowPinHandle.h b/Source/FlowEditor/Public/Pins/SFlowPinHandle.h new file mode 100644 index 000000000..141ba3c1a --- /dev/null +++ b/Source/FlowEditor/Public/Pins/SFlowPinHandle.h @@ -0,0 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "SGraphPin.h" +#include "SNameComboBox.h" + +class FLOWEDITOR_API SFlowPinHandle : public SGraphPin +{ +public: + SFlowPinHandle(); + virtual ~SFlowPinHandle() override; + + SLATE_BEGIN_ARGS(SFlowPinHandle) {} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj, const UBlueprint* InBlueprint); + virtual TSharedRef GetDefaultValueWidget() override; + +protected: + virtual void RefreshNameList() {} + void ParseDefaultValueData(); + void ComboBoxSelectionChanged(const TSharedPtr NameItem, ESelectInfo::Type SelectInfo) const; + + const UBlueprint* Blueprint; + TArray> PinNames; + + TSharedPtr ComboBox; + TSharedPtr CurrentlySelectedName; +}; diff --git a/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h b/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h new file mode 100644 index 000000000..034f26edc --- /dev/null +++ b/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h @@ -0,0 +1,65 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +// NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. +// If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule + +#pragma once + +#include "IFlowExtendedPropertyTypeCustomization.h" +#include "Widgets/Input/SComboBox.h" + + +// A base-class to do property Customization for a struct that presents a curated list of FNames for selection +class FLOWEDITOR_API IFlowCuratedNamePropertyCustomization : public IFlowExtendedPropertyTypeCustomization +{ +public: + +protected: + + //Begin IExtendedPropertyTypeCustomization + virtual void CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + //End IExtendedPropertyTypeCustomization + + void Initialize(); + + // Helper function to set the property to a specified value + // (and handle all of the side-effects) + bool TrySetCuratedNameWithSideEffects(const FName& NewName); + + // Callbacks for the TextListWidget (see CreateHeaderRowWidget) + FText GetCachedText() const; + TSharedRef GenerateTextListWidget(TSharedPtr InItem); + void OnTextListComboBoxOpening(); + void OnTextSelected(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo); + + void RepaintTextListWidget(); + + TSharedPtr FindCachedOrCreateText(const FName& NewName); + void AddToCachedTextList(TSharedPtr Text); + void InsertAtHeadOfCachedTextList(TSharedPtr Text); + + bool CustomIsResetToDefaultVisible(TSharedPtr Property) const; + void CustomResetToDefault(TSharedPtr Property); + + //Begin IFlowCuratedNamePropertyCustomization + virtual TSharedPtr GetCuratedNamePropertyHandle() const = 0; + virtual void SetCuratedName(const FName& NewName) = 0; + virtual FName GetCuratedName() const = 0; + virtual TArray GetCuratedNameOptions() const = 0; + //End IFlowCuratedNamePropertyCustomization + +public: + + // Cached property handle for the Curated Name property that is being customized + TSharedPtr CachedNameHandle; + + // Cache FTexts for the ComboBox dropdown & current selected + TArray> CachedTextList; + TSharedPtr CachedTextSelected; + + // Preallocated NAME_None as FText + static TSharedPtr NoneAsText; + + // Combo Box widget for displaying the curated list of Names + TSharedPtr>> TextListWidget; +}; diff --git a/Source/FlowEditor/Public/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h b/Source/FlowEditor/Public/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h new file mode 100644 index 000000000..35647cd28 --- /dev/null +++ b/Source/FlowEditor/Public/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h @@ -0,0 +1,78 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +// NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. +// If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule + +#pragma once + +#include "IPropertyTypeCustomization.h" +#include "PropertyHandle.h" +#include "Templates/SharedPointer.h" + +#include "IPropertyTypeCustomization.h" + + +// Forward Declarations +class STextBlock; +class FDetailWidgetRow; +class IDetailChildrenBuilder; +class IPropertyTypeCustomizationUtils; +class IDetailPropertyRow; +class IPropertyHandle; + + +// An extension of IPropertyTypeCustomization +// which adds some quality-of-life improvements for subclasses +class FLOWEDITOR_API IFlowExtendedPropertyTypeCustomization : public IPropertyTypeCustomization +{ +public: + + // IPropertyTypeCustomization interface + virtual void CustomizeHeader( TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) override; + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override + { CustomizeChildrenDefaultImpl(InStructPropertyHandle, StructBuilder, StructCustomizationUtils); } + + static void CustomizeChildrenDefaultImpl(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils); + + template + static StructT* TryGetTypedStructValue(const TSharedPtr& StructPropertyHandle); + +protected: + + void RefreshHeader() const; + + virtual void CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils); + virtual FText BuildHeaderText() const; + + // Callbacks for property editor delegates + void OnAnyChildPropertyChanged(); + +protected: + + // Cached struct property + TSharedPtr StructPropertyHandle; + + // Header property text block, (re-)built in RefreshHeader + TSharedPtr HeaderTextBlock; +}; + + +// Inline Implementations + +template +StructT* IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(const TSharedPtr& StructPropertyHandle) +{ + if (StructPropertyHandle.IsValid()) + { + // Get the actual struct data from the handle and cast it to the correct type + TArray RawData; + StructPropertyHandle->AccessRawData(RawData); + + if (RawData.Num() > 0) + { + return reinterpret_cast(RawData[0]); + } + } + + return nullptr; +} diff --git a/Source/FlowEditor/Public/LevelEditor/SLevelEditorFlow.h b/Source/FlowEditor/Public/Utils/SLevelEditorFlow.h similarity index 78% rename from Source/FlowEditor/Public/LevelEditor/SLevelEditorFlow.h rename to Source/FlowEditor/Public/Utils/SLevelEditorFlow.h index f9dd30881..d004bce48 100644 --- a/Source/FlowEditor/Public/LevelEditor/SLevelEditorFlow.h +++ b/Source/FlowEditor/Public/Utils/SLevelEditorFlow.h @@ -1,3 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + #pragma once #include "CoreMinimal.h" @@ -6,7 +8,7 @@ struct FAssetData; -class SLevelEditorFlow : public SCompoundWidget +class FLOWEDITOR_API SLevelEditorFlow : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SLevelEditorFlow) {}