From 56b30469291724202f1b2f242de703b47001ae79 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:54:48 -0800 Subject: [PATCH] Omnibus small changes [Flow] Reroute updates/fixes Reroute nodes can now retype themselves if connected to a new type (and in doing so, break incompatible connections) Copy/paste for data pin reroutes preserves the type of the reroute (was being lost) CR - JDurica, BJarvinen, ECalder [Flow] Add Paste option to the right-click menu for flow nodes formerly could only paste onto a flow-node using ctrl+v. Now we have a right-click option for it CR - BJarvinen [Flow] Added Attach AddOn drop-down to FlowNode (and AddOn) details Adds a more convenient method for attaching addons that is fewer-clicks per operation and a bit less hidden. This is in addition to the right-click menu on these nodes. CR - BJarvinen, ECalder [Flow] Updated Flow Palette filters Improved the Flow Palette Filtering by category to also check superclasses of the UFlowAsset subclass being edited The palette filtering can apply a strict or tentative result, as it crawls up the superclass lineage Updated the categories of a few flow nodes to make them filterable created some starter flow node filtering rules for our flow asset types CR - BJarvinen, ECalder [Flow] Restored the "Choose Parent Flow Asset Params" text that was lost in a Flow merge Mainline flow removed this text (by accident I suppose?), but it's actually necessary to tell the user why they need to select another asset params CR - JDurica [Flow] AddOn Predicate RequireGameplayTags (data pin version) CR - BJarvinen, ECalder [Flow] Refactored the IsInput/OutputConnected interface to be more useful & pin connection change event improvements Refactored some of the variants of IsInputConnected/IsOutputConnected to be FindFirstIn/OutputPinConnection and FindIn/OutputPinConnections functions These new function signatures provide multiple connections when present, and also have FConnectedPin structs as their container Updated existing calls to these functions to use the new signatures Also: Augmented the event for OnConnectionsChanged to have an array of changed connections instead of the old connections array. This new version (OnEditorPinConnectionsChanged) is called on all of the addons on a flow node as well, and has a blueprint signature This change allows nodes to be more reactive to pin connections changing (like changing their Config Text), which was possible before, but not smooth. Updated some flow nodes (Log, FormatText) and the new Predicates for RequireGameplayTags to update their config text on pin connection changes CR - ECalder, BJarvinen [Flow] RequireGameplayTags UpdateConfigText on changes to properties Now it is reactive to pin connections CR - ECalder, BJarvinen [Flow] Setting FlowNode pointer on AddOns more reliably in editor Update the FlowNode pointer in editor for AddOns so that it is usable any time while in editor, can updated when addons are moved/rebuilt CR - ECalder, BJarvinen --- Flow.uplugin | 6 +- Source/Flow/Flow.Build.cs | 1 + ...NodeAddOn_PredicateRequireGameplayTags.cpp | 95 +++++ .../Private/Nodes/Developer/FlowNode_Log.cpp | 15 +- Source/Flow/Private/Nodes/FlowNode.cpp | 352 ++++++++++++++---- Source/Flow/Private/Nodes/FlowNodeBase.cpp | 11 +- .../Nodes/Graph/FlowNode_FormatText.cpp | 9 +- .../Private/Nodes/Route/FlowNode_Reroute.cpp | 9 +- ...owNodeAddOn_PredicateRequireGameplayTags.h | 51 +++ .../Public/Nodes/Developer/FlowNode_Log.h | 4 + Source/Flow/Public/Nodes/FlowNode.h | 57 ++- Source/Flow/Public/Nodes/FlowNodeBase.h | 8 +- .../Public/Types/FlowPinConnectionChange.h | 49 +++ .../Private/Asset/FlowAssetParamsFactory.cpp | 6 + .../FlowDetailsAddOnUI.cpp | 92 +++++ .../FlowNodeAddOn_Details.cpp | 59 +++ .../DetailCustomizations/FlowNode_Details.cpp | 57 +++ .../Private/Graph/FlowGraphNodesPolicy.cpp | 12 +- .../Private/Graph/FlowGraphSchema.cpp | 29 +- .../Private/Graph/Nodes/FlowGraphNode.cpp | 20 +- .../DetailCustomizations/FlowDetailsAddOnUI.h | 30 ++ .../FlowNodeAddOn_Details.h | 5 +- .../DetailCustomizations/FlowNode_Details.h | 5 +- .../Public/Graph/FlowGraphNodesPolicy.h | 50 ++- 24 files changed, 915 insertions(+), 117 deletions(-) create mode 100644 Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp create mode 100644 Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h create mode 100644 Source/Flow/Public/Types/FlowPinConnectionChange.h create mode 100644 Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp create mode 100644 Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h diff --git a/Flow.uplugin b/Flow.uplugin index f4eb2d4c..90920bea 100644 --- a/Flow.uplugin +++ b/Flow.uplugin @@ -1,4 +1,4 @@ -{ +{ "FileVersion" : 3, "Version" : 2.2, "FriendlyName" : "Flow", @@ -42,6 +42,10 @@ { "Name": "EngineAssetDefinitions", "Enabled": true + }, + { + "Name": "GameplayAbilities", + "Enabled": true } ] } \ No newline at end of file diff --git a/Source/Flow/Flow.Build.cs b/Source/Flow/Flow.Build.cs index c6847ce4..cc47240d 100644 --- a/Source/Flow/Flow.Build.cs +++ b/Source/Flow/Flow.Build.cs @@ -18,6 +18,7 @@ public Flow(ReadOnlyTargetRules target) : base(target) "CoreUObject", "DeveloperSettings", "Engine", + "GameplayAbilities", // for FGameplayTagRequirements "GameplayTags", "MovieScene", "MovieSceneTracks", diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp new file mode 100644 index 00000000..c3ee2d5f --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp @@ -0,0 +1,95 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h" +#include "FlowLogChannels.h" +#include "Nodes/FlowNode.h" +#include "Logging/LogMacros.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn_PredicateRequireGameplayTags) + +UFlowNodeAddOn_PredicateRequireGameplayTags::UFlowNodeAddOn_PredicateRequireGameplayTags() + : Super() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn_Predicate; + Category = TEXT("DataPins"); +#endif +} + +bool UFlowNodeAddOn_PredicateRequireGameplayTags::EvaluatePredicate_Implementation() const +{ + if (Requirements.IsEmpty()) + { + // And Empty Requirements results in a "true" result + return true; + } + + FGameplayTagContainer TagsValue; + + // Sourcing the tags from the data pin + if (!TryGetTagsToCheckFromDataPin(TagsValue)) + { + return false; + } + + // Execute the Tags vs the Requirements + const bool bResult = Requirements.RequirementsMet(TagsValue); + return bResult; +} + +#if WITH_EDITOR +void UFlowNodeAddOn_PredicateRequireGameplayTags::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + UpdateNodeConfigText(); +} + +void UFlowNodeAddOn_PredicateRequireGameplayTags::OnEditorPinConnectionsChanged(const TArray& Changes) +{ + Super::OnEditorPinConnectionsChanged(Changes); + + UpdateNodeConfigText(); +} +#endif + +bool UFlowNodeAddOn_PredicateRequireGameplayTags::TryGetTagsToCheckFromDataPin(FGameplayTagContainer& TagsToCheckValue) const +{ + static const FName TagsName = GET_MEMBER_NAME_CHECKED(UFlowNodeAddOn_PredicateRequireGameplayTags, Tags); + + const EFlowDataPinResolveResult ResultEnum = TryResolveDataPinValue(TagsName, TagsToCheckValue); + + if (FlowPinType::IsSuccess(ResultEnum)) + { + return true; + } + else + { + UE_LOG(LogFlow, Error, TEXT("Cannot EvaluatePredicate on a data pin value we cannot resolve: %s"), *UEnum::GetDisplayValueAsText(ResultEnum).ToString()); + + return false; + } +} + +void UFlowNodeAddOn_PredicateRequireGameplayTags::UpdateNodeConfigText_Implementation() +{ +#if WITH_EDITOR + const FName TagsName = GET_MEMBER_NAME_CHECKED(UFlowNodeAddOn_PredicateRequireGameplayTags, Tags); + FTextBuilder TextBuilder; + if (Requirements.IsEmpty()) + { + const FName RequirementsName = GET_MEMBER_NAME_CHECKED(UFlowNodeAddOn_PredicateRequireGameplayTags, Requirements); + TextBuilder.AppendLine(FString::Printf(TEXT(""), *RequirementsName.ToString())); + } + else if (Tags.IsEmpty() && !GetFlowNode()->IsInputConnected(TagsName)) + { + TextBuilder.AppendLine(FString::Printf(TEXT(""), *TagsName.ToString())); + } + else + { + TextBuilder.AppendLine(Requirements.ToString()); + } + + SetNodeConfigText(TextBuilder.ToText()); +#endif // WITH_EDITOR +} \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp index 10bb0ecf..e5bb4b90 100644 --- a/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp +++ b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp @@ -88,14 +88,23 @@ void UFlowNode_Log::PostEditChangeChainProperty(FPropertyChangedChainEvent& Prop Super::PostEditChangeChainProperty(PropertyChainEvent); } +void UFlowNode_Log::OnEditorPinConnectionsChanged(const TArray& Changes) +{ + Super::OnEditorPinConnectionsChanged(Changes); + + UpdateNodeConfigText(); +} + void UFlowNode_Log::UpdateNodeConfigText_Implementation() { - constexpr bool bErrorIfInputPinNotFound = false; - const bool bIsInputConnected = IsInputConnected(GET_MEMBER_NAME_CHECKED(ThisClass, Message), bErrorIfInputPinNotFound); + constexpr bool bErrorIfInputPinNotFound = true; + + FConnectedPin ConnectedPin; + const bool bIsInputConnected = FindFirstInputPinConnection(GET_MEMBER_NAME_CHECKED(ThisClass, Message), bErrorIfInputPinNotFound, ConnectedPin); if (bIsInputConnected) { - SetNodeConfigText(FText()); + SetNodeConfigText(FText::Format(LOCTEXT("LogFromPin", "Message from: {0}"), { FText::FromString(ConnectedPin.PinName.ToString()) })); } else { diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index ecf2b1d2..923c475b 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -6,9 +6,10 @@ #include "FlowAsset.h" #include "FlowSettings.h" #include "Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h" -#include "Types/FlowPinType.h" -#include "Types/FlowDataPinValue.h" #include "Types/FlowAutoDataPinsWorkingData.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowPinConnectionChange.h" +#include "Types/FlowPinType.h" #include "Components/ActorComponent.h" #if WITH_EDITOR @@ -99,14 +100,12 @@ void UFlowNode::ValidateFlowPinArrayIsUnique(const TArray& FlowPins, T } } -void UFlowNode::EnsureSetFlowNodeForEditorForAllAddOns() const +void UFlowNode::EnsureAddOnFlowNodePointersForEditor() { - UFlowNode* MutableThis = const_cast(this); - - MutableThis->ForEachAddOn( - [MutableThis](UFlowNodeAddOn& AddOn) -> EFlowForEachAddOnFunctionReturnValue + ForEachAddOn( + [this](UFlowNodeAddOn& AddOn) -> EFlowForEachAddOnFunctionReturnValue { - AddOn.SetFlowNodeForEditor(MutableThis); + AddOn.SetFlowNodeForEditor(this); return EFlowForEachAddOnFunctionReturnValue::Continue; }); } @@ -130,7 +129,7 @@ void UFlowNode::PostLoad() bool UFlowNode::IsSupportedInputPinName(const FName& PinName) const { - const FFlowPin* InputPin = FindFlowPinByName(PinName, InputPins); + const FFlowPin* InputPin = FindInputPinByName(PinName); if (AddOns.IsEmpty()) { @@ -159,6 +158,14 @@ void UFlowNode::AddOutputPins(const TArray& Pins) #if WITH_EDITOR +void UFlowNode::SetupForEditing(UEdGraphNode& EdGraphNode) +{ + Super::SetupForEditing(EdGraphNode); + + // Ensure AddOn editor pointers are correct as soon as we're prepared for editing. + EnsureAddOnFlowNodePointersForEditor(); +} + bool UFlowNode::RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin) { bool bIsChanged; @@ -328,8 +335,6 @@ bool UFlowNode::SupportsContextPins() const TArray UFlowNode::GetContextInputs() const { - EnsureSetFlowNodeForEditorForAllAddOns(); - TArray ContextOutputs = Super::GetContextInputs(); // Add the Auto-Generated DataPins as GetContextInputs @@ -343,8 +348,6 @@ TArray UFlowNode::GetContextInputs() const TArray UFlowNode::GetContextOutputs() const { - EnsureSetFlowNodeForEditorForAllAddOns(); - TArray ContextOutputs = Super::GetContextOutputs(); // Add the Auto-Generated DataPins as ContextOutputs @@ -652,11 +655,15 @@ bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, T TryAddSupplierDataToArray(NewPinValueSupplier, InOutPinValueSupplierDatas); // If the pin is connected, try to add the connected node as the priority supplier - FFlowPinValueSupplierData ConnectedPinValueSupplier; - FGuid ConnectedNodeGuid; + FConnectedPin ConnectedPin; - if (FindConnectedNodeForPinFast(PinName, &ConnectedNodeGuid, &ConnectedPinValueSupplier.SupplierPinName)) + if (FindConnectedNodeForPinCached(PinName, ConnectedPin)) { + const FGuid& ConnectedNodeGuid = ConnectedPin.NodeGuid; + + FFlowPinValueSupplierData ConnectedPinValueSupplier; + ConnectedPinValueSupplier.SupplierPinName = ConnectedPin.PinName; + if (const UFlowAsset* FlowAsset = GetFlowAsset()) { const UFlowNode* SupplierFlowNode = FlowAsset->GetNode(ConnectedNodeGuid); @@ -732,11 +739,118 @@ void UFlowNode::FixupDataPinTypesForPin(FFlowPin& MutableDataPin) // -- #if WITH_EDITOR + +void UFlowNode::BuildConnectionChangeList( + const UFlowAsset& FlowAsset, + const TMap& OldConnections, + const TMap& NewConnections, + TArray& OutChanges) +{ + OutChanges.Reset(); + + // Gather union of keys + TSet Keys; + Keys.Reserve(OldConnections.Num() + NewConnections.Num()); + + for (const TPair& KVP : OldConnections) + { + Keys.Add(KVP.Key); + } + + for (const TPair& KVP : NewConnections) + { + Keys.Add(KVP.Key); + } + + for (const FName& PinName : Keys) + { + const FConnectedPin* OldConnectedPin = OldConnections.Find(PinName); + const FConnectedPin* NewConnectedPin = NewConnections.Find(PinName); + + const bool bHadOld = (OldConnectedPin != nullptr); + const bool bHasNew = (NewConnectedPin != nullptr); + + // If present in both and equal => no change + if (bHadOld && bHasNew && (*OldConnectedPin == *NewConnectedPin)) + { + continue; + } + + UFlowNode* OldConnectedNode = nullptr; + FName OldConnectedPinName; + if (bHadOld) + { + OldConnectedNode = FlowAsset.GetNode(OldConnectedPin->NodeGuid); + OldConnectedPinName = OldConnectedPin->PinName; + } + + UFlowNode* NewConnectedNode = nullptr; + FName NewConnectedPinName; + if (bHasNew) + { + NewConnectedNode = FlowAsset.GetNode(NewConnectedPin->NodeGuid); + NewConnectedPinName = NewConnectedPin->PinName; + } + + FFlowPinConnectionChange Change = + FFlowPinConnectionChange( + PinName, + OldConnectedNode, + OldConnectedPinName, + NewConnectedNode, + NewConnectedPinName); + + OutChanges.Add(MoveTemp(Change)); + } +} + +void UFlowNode::BroadcastEditorPinConnectionsChanged(const TArray& Changes) +{ + OnEditorPinConnectionsChanged(Changes); + + ForEachAddOn([&Changes](UFlowNodeAddOn& AddOn) -> EFlowForEachAddOnFunctionReturnValue + { + AddOn.OnEditorPinConnectionsChanged(Changes); + + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); +} + void UFlowNode::SetConnections(const TMap& InConnections) { const TMap OldConnections = Connections; + + // Early-out if maps are identical (cheap check first, then deep equality). + // Note: TMap equality operator exists for comparable value types; keep explicit check to be safe. + if (OldConnections.Num() == InConnections.Num()) + { + bool bAllEqual = true; + for (const TPair& KVP : OldConnections) + { + const FConnectedPin* Other = InConnections.Find(KVP.Key); + if (!Other || !(*Other == KVP.Value)) + { + bAllEqual = false; + break; + } + } + + if (bAllEqual) + { + return; + } + } + Connections = InConnections; - OnConnectionsChanged(OldConnections); + + // Compute per-pin deltas and broadcast to self + addons + TArray Changes; + BuildConnectionChangeList(*GetFlowAsset(), OldConnections, Connections, Changes); + + if (!Changes.IsEmpty()) + { + BroadcastEditorPinConnectionsChanged(Changes); + } } #endif @@ -766,112 +880,214 @@ FName UFlowNode::GetPinConnectedToNode(const FGuid& OtherNodeGuid) bool UFlowNode::IsInputConnected(const FName& PinName, bool bErrorIfPinNotFound) const { - if (const FFlowPin* FlowPin = FindFlowPinByName(PinName, InputPins)) + // TODO (gtaylor) Maybe we make a blueprint accessible version with the FConnectedPin array access + constexpr TArray* ConnectedPins = nullptr; + return FindInputPinConnections(PinName, bErrorIfPinNotFound, ConnectedPins); +} + +bool UFlowNode::IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound) const +{ + // TODO (gtaylor) Maybe we make a blueprint accessible version with the FConnectedPin array access + constexpr TArray* ConnectedPins = nullptr; + return FindOutputPinConnections(PinName, bErrorIfPinNotFound, ConnectedPins); +} + +bool UFlowNode::FindFirstInputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const +{ + if (const FFlowPin* FlowPin = FindInputPinByName(PinName)) { - return IsInputConnected(*FlowPin); + return FindFirstInputPinConnection(*FlowPin, FirstConnectedPin); } if (bErrorIfPinNotFound) { - LogError(FString::Printf(TEXT("Unknown pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + LogError(FString::Printf(TEXT("Unknown input pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); } return false; } -bool UFlowNode::IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound) const +bool UFlowNode::FindInputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins) const { - if (const FFlowPin* FlowPin = FindFlowPinByName(PinName, OutputPins)) + if (const FFlowPin* FlowPin = FindInputPinByName(PinName)) { - return IsOutputConnected(*FlowPin); + return FindInputPinConnections(*FlowPin, ConnectedPins); } if (bErrorIfPinNotFound) { - LogError(FString::Printf(TEXT("Unknown pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + LogError(FString::Printf(TEXT("Unknown input pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); } return false; } -FFlowPin* UFlowNode::FindInputPinByName(const FName& PinName) +bool UFlowNode::FindFirstOutputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const { - if (FFlowPin* FlowPin = FindFlowPinByName(PinName, InputPins)) + if (const FFlowPin* FlowPin = FindOutputPinByName(PinName)) { - return FlowPin; + return FindFirstOutputPinConnection(*FlowPin, FirstConnectedPin); } - return nullptr; + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown output pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; } -FFlowPin* UFlowNode::FindOutputPinByName(const FName& PinName) +bool UFlowNode::FindOutputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins) const { - if (FFlowPin* FlowPin = FindFlowPinByName(PinName, OutputPins)) + if (const FFlowPin* FlowPin = FindOutputPinByName(PinName)) { - return FlowPin; + return FindOutputPinConnections(*FlowPin, ConnectedPins); } - return nullptr; + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown output pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; } -bool UFlowNode::IsInputConnected(const FFlowPin& FlowPin, FGuid* FoundGuid, FName* OutConnectedPinName) const +template +bool UFlowNode::FindFirstPinConnection( + const FFlowPin& FlowPin, + const TArray& FlowPinArray, + FConnectedPin& FirstConnectedPin) const { - if (!InputPins.Contains(FlowPin.PinName)) + if (!FlowPinArray.Contains(FlowPin.PinName)) { return false; } - if (FlowPin.IsExecPin()) + const bool bUseCachedPath = (bExecIsCached == FlowPin.IsExecPin()); + if (bUseCachedPath) { - // We don't cache the input exec pins for fast lookup in Connections, so use the slow path for them: - - return FindConnectedNodeForPinSlow(FlowPin.PinName, FoundGuid, OutConnectedPinName); + // Cached category: fast lookup (0/1 connection) + return FindConnectedNodeForPinCached(FlowPin.PinName, FirstConnectedPin); } else { - return FindConnectedNodeForPinFast(FlowPin.PinName, FoundGuid, OutConnectedPinName); + // NOTE (gtaylor) For optimal perf, you should use the array signature when asking for uncached path + // (aka optimal use should use this branch) + TArray ConnectedPins; + if (FindConnectedNodeForPinUncached(FlowPin.PinName, &ConnectedPins)) + { + check(ConnectedPins.Num() > 0); + FirstConnectedPin = ConnectedPins[0]; + return true; + } + else + { + return false; + } } } -bool UFlowNode::IsOutputConnected(const FFlowPin& FlowPin, FGuid* FirstFoundGuid, FName* OutFirstConnectedPinName) const +template +bool UFlowNode::FindPinConnections(const FFlowPin& FlowPin, const TArray& FlowPinArray, TArray* ConnectedPins) const { - if (!OutputPins.Contains(FlowPin.PinName)) + if (!FlowPinArray.Contains(FlowPin.PinName)) { return false; } - if (FlowPin.IsExecPin()) + const bool bUseCachedPath = bExecIsCached == FlowPin.IsExecPin(); + if (bUseCachedPath) { - return FindConnectedNodeForPinFast(FlowPin.PinName, FirstFoundGuid, OutFirstConnectedPinName); + // NOTE (gtaylor) For optimal perf, you should use the non-array signature when asking for cached path + // (aka optimal use should use this branch) + FConnectedPin ConnectedPin; + const bool bFoundPin = FindConnectedNodeForPinCached(FlowPin.PinName, ConnectedPin); + if (bFoundPin && ConnectedPins) + { + ConnectedPins->Add(ConnectedPin); + } + + return bFoundPin; } else { - // We don't cache the input data pins for fast lookup in Connections, so use the slow path for them: + // We don't cache the output data pins for fast lookup in Connections, so use the slow path for them: - return FindConnectedNodeForPinSlow(FlowPin.PinName, FirstFoundGuid, OutFirstConnectedPinName); + return FindConnectedNodeForPinUncached(FlowPin.PinName, ConnectedPins); } } -bool UFlowNode::FindConnectedNodeForPinFast(const FName& PinName, FGuid* OutGuid, FName* OutConnectedPinName) const +bool UFlowNode::FindFirstInputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const +{ + // Exec Input pins - not cached + // Data Input pins - cached + constexpr bool bIsExecCached = false; + return FindFirstPinConnection(FlowPin, InputPins, FirstConnectedPin); +} + +bool UFlowNode::FindInputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins) const +{ + // Exec Input pins - not cached + // Data Input pins - cached + constexpr bool bIsExecCached = false; + return FindPinConnections(FlowPin, InputPins, ConnectedPins); +} + +bool UFlowNode::FindFirstOutputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const +{ + // Exec Output pins - cached + // Data Output pins - not cached + constexpr bool bIsExecCached = true; + return FindFirstPinConnection(FlowPin, OutputPins, FirstConnectedPin); +} + +bool UFlowNode::FindOutputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins) const { - const FConnectedPin* FoundConnectedPin = Connections.Find(PinName); + // Exec Output pins - cached + // Data Output pins - not cached + constexpr bool bIsExecCached = true; + return FindPinConnections(FlowPin, OutputPins, ConnectedPins); +} + +FFlowPin* UFlowNode::FindInputPinByName(const FName& PinName) +{ + if (FFlowPin* FlowPin = FindFlowPinByName(PinName, InputPins)) + { + return FlowPin; + } + + return nullptr; +} + +FFlowPin* UFlowNode::FindOutputPinByName(const FName& PinName) +{ + if (FFlowPin* FlowPin = FindFlowPinByName(PinName, OutputPins)) + { + return FlowPin; + } + + return nullptr; +} + +bool UFlowNode::FindConnectedNodeForPinCached(const FName& FlowPinName, FConnectedPin& ConnectedPin) const +{ + // NOTE (gtaylor) The Connections array only caches: + // - exec output pins + // - data input pins + // In both cases, there must be only one connection (due to schema rules in Flow). + // For the opposite direction (exec inputs, data outputs, the uncached version must be used. + const FConnectedPin* FoundConnectedPin = Connections.Find(FlowPinName); if (FoundConnectedPin) { - if (OutGuid) - { - *OutGuid = FoundConnectedPin->NodeGuid; - } + ConnectedPin = *FoundConnectedPin; - if (OutConnectedPinName) - { - *OutConnectedPinName = FoundConnectedPin->PinName; - } + return true; } - return FoundConnectedPin != nullptr; + return false; } -bool UFlowNode::FindConnectedNodeForPinSlow(const FName& PinName, FGuid* OutGuid, FName* OutConnectedPinName) const +bool UFlowNode::FindConnectedNodeForPinUncached(const FName& PinName, TArray* ConnectedPins) const { const UFlowAsset* FlowAsset = GetFlowAsset(); @@ -880,9 +1096,10 @@ bool UFlowNode::FindConnectedNodeForPinSlow(const FName& PinName, FGuid* OutGuid return false; } + check(!ConnectedPins || ConnectedPins->IsEmpty()); + for (const TPair& Pair : ObjectPtrDecay(FlowAsset->Nodes)) { - const FGuid& ConnectedFromGuid = Pair.Key; const UFlowNode* ConnectedFromFlowNode = Pair.Value; if (!IsValid(ConnectedFromFlowNode)) @@ -890,27 +1107,30 @@ bool UFlowNode::FindConnectedNodeForPinSlow(const FName& PinName, FGuid* OutGuid continue; } - for (const TPair& Connection : Pair.Value->Connections) + for (const TPair& Connection : ConnectedFromFlowNode->Connections) { const FConnectedPin& ConnectedPinStruct = Connection.Value; if (ConnectedPinStruct.NodeGuid == NodeGuid && ConnectedPinStruct.PinName == PinName) { - if (OutGuid) + if (ConnectedPins) { - *OutGuid = ConnectedFromGuid; + ConnectedPins->Add(ConnectedPinStruct); } - - if (OutConnectedPinName) + else { - *OutConnectedPinName = Connection.Key; + // Early return if not collecting the ConnectedPins, since only connected true/false matters + return true; } - - return true; } } } + if (ConnectedPins && !ConnectedPins->IsEmpty()) + { + return true; + } + return false; } diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index e4105666..20ccb493 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -9,6 +9,7 @@ #include "AddOns/FlowNodeAddOn.h" #include "Interfaces/FlowDataPinValueSupplierInterface.h" #include "Interfaces/FlowNamedPropertiesSupplierInterface.h" +#include "Nodes/FlowNode.h" #include "Types/FlowArray.h" #include "Types/FlowDataPinResults.h" #include "Types/FlowPinTypesStandard.h" @@ -264,7 +265,7 @@ TArray UFlowNodeBase::GetContextOutputs() const return ContextOutputs; } -#endif // WITH_EDITOR +#endif void UFlowNodeBase::LogValidationError(const FString& Message) { @@ -409,7 +410,7 @@ EFlowAddOnAcceptResult UFlowNodeBase::CheckAcceptFlowNodeAddOnChild( return CombinedResult; } -#endif // WITH_EDITOR +#endif EFlowForEachAddOnFunctionReturnValue UFlowNodeBase::ForEachAddOnConst( const FConstFlowNodeAddOnFunction& Function, @@ -622,7 +623,7 @@ void UFlowNodeBase::PostEditChangeProperty(FPropertyChangedEvent& PropertyChange UpdateNodeConfigText(); } -#endif // WITH_EDITOR +#endif FString UFlowNodeBase::GetStatusString() const { @@ -846,7 +847,7 @@ FText UFlowNodeBase::GetNodeConfigText() const return DevNodeConfigText; #else return FText::GetEmpty(); -#endif // WITH_EDITORONLY_DATA +#endif } void UFlowNodeBase::SetNodeConfigText(const FText& NodeConfigText) @@ -856,7 +857,7 @@ void UFlowNodeBase::SetNodeConfigText(const FText& NodeConfigText) { DevNodeConfigText = NodeConfigText; } -#endif // WITH_EDITOR +#endif } void UFlowNodeBase::UpdateNodeConfigText_Implementation() diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp index 5da5f18b..5968eebd 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp @@ -66,10 +66,13 @@ void UFlowNode_FormatText::PostEditChangeChainProperty(FPropertyChangedChainEven void UFlowNode_FormatText::UpdateNodeConfigText_Implementation() { - constexpr bool bErrorIfInputPinNotFound = false; - if (IsInputConnected(GET_MEMBER_NAME_CHECKED(ThisClass, FormatText), bErrorIfInputPinNotFound)) + constexpr bool bErrorIfInputPinNotFound = true; + FConnectedPin ConnectedPin; + const bool bIsInputConnected = FindFirstInputPinConnection(GET_MEMBER_NAME_CHECKED(ThisClass, FormatText), bErrorIfInputPinNotFound, ConnectedPin); + + if (bIsInputConnected) { - SetNodeConfigText(FText()); + SetNodeConfigText(FText::Format(LOCTEXT("FormatTextFromPin", "Format from: {0}"), { FText::FromString(ConnectedPin.PinName.ToString()) })); } else { diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp index d04c1a87..1c32d287 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp @@ -46,14 +46,13 @@ FFlowDataPinResult UFlowNode_Reroute::TrySupplyDataPin(FName PinName) const return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); } - FGuid FoundGuid; - FName ConnectedPinName; - if (!IsInputConnected(*InputPin, &FoundGuid, &ConnectedPinName)) + FConnectedPin ConnectedPin; + if (!FindFirstInputPinConnection(*InputPin, ConnectedPin)) { return FFlowDataPinResult(EFlowDataPinResolveResult::FailedNotConnected); } - const UFlowNode* ConnectedFlowNodeSupplier = GetFlowAsset()->GetNode(FoundGuid); + const UFlowNode* ConnectedFlowNodeSupplier = GetFlowAsset()->GetNode(ConnectedPin.NodeGuid); if (!IsValid(ConnectedFlowNodeSupplier)) { checkf(IsValid(ConnectedFlowNodeSupplier), TEXT("This node should be valid if IsInputConnected returned true")); @@ -62,5 +61,5 @@ FFlowDataPinResult UFlowNode_Reroute::TrySupplyDataPin(FName PinName) const } // Hand-off to the connected flow node to supply the value - return ConnectedFlowNodeSupplier->TrySupplyDataPin(ConnectedPinName); + return ConnectedFlowNodeSupplier->TrySupplyDataPin(ConnectedPin.PinName); } diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h new file mode 100644 index 00000000..614aa50b --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h @@ -0,0 +1,51 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPredicateInterface.h" + +#include "GameplayEffectTypes.h" + +#include "FlowNodeAddOn_PredicateRequireGameplayTags.generated.h" + +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "Require Gameplay Tags")) +class UFlowNodeAddOn_PredicateRequireGameplayTags + : public UFlowNodeAddOn + , public IFlowPredicateInterface +{ + GENERATED_BODY() + +public: + + UFlowNodeAddOn_PredicateRequireGameplayTags(); + + // IFlowPredicateInterface + virtual bool EvaluatePredicate_Implementation() const override; + // -- + + // UFlowNodeBase + virtual void UpdateNodeConfigText_Implementation() override; + // -- + +#if WITH_EDITOR + // UObject Interface + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + // -- + + // UFlowNodeBase + void OnEditorPinConnectionsChanged(const TArray& Changes) override; + // -- +#endif + + bool TryGetTagsToCheckFromDataPin(FGameplayTagContainer& TagsToCheckValue) const; + +public: + + // DataPin input for the Gameplay Tag or Tag Container to test with the Requirements + UPROPERTY(EditAnywhere, Category = Configuration, meta = (DefaultForInputFlowPin, FlowPinType = "GameplayTagContainer")) + FGameplayTagContainer Tags; + + // Requirements to evaluate the Test Tags with + UPROPERTY(EditAnywhere, Category = Configuration) + FGameplayTagRequirements Requirements; +}; diff --git a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h index 3c04ecce..938ec98d 100644 --- a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h +++ b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h @@ -56,6 +56,10 @@ class FLOW_API UFlowNode_Log : public UFlowNode_DefineProperties virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; // -- + // UFlowNodeBase + virtual void OnEditorPinConnectionsChanged(const TArray& Changes) override; + // -- + virtual void UpdateNodeConfigText_Implementation() override; #endif diff --git a/Source/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index 90985a18..159faf3d 100644 --- a/Source/Flow/Public/Nodes/FlowNode.h +++ b/Source/Flow/Public/Nodes/FlowNode.h @@ -11,6 +11,7 @@ #include "Interfaces/FlowDataPinValueSupplierInterface.h" #include "Nodes/FlowPin.h" #include "Types/FlowArray.h" +#include "Types/FlowPinConnectionChange.h" #include "FlowNode.generated.h" struct FFlowAutoDataPinsWorkingData; @@ -70,9 +71,15 @@ class FLOW_API UFlowNode : public UFlowNodeBase // -- #if WITH_EDITOR - /* Called before operations on AddOns in the editor - * where we need the AddOns to have a value FlowNode pointer to use. */ - void EnsureSetFlowNodeForEditorForAllAddOns() const; + /* Set up UFlowNodeBase when being opened for edit in the editor. */ + virtual void SetupForEditing(UEdGraphNode& EdGraphNode) override; + + /** + * Editor-only: ensure any editor-time parent pointers are correctly set for this node and any child AddOns. + * Goal: AddOns always have a valid FlowNode pointer while being edited (creation/paste/undo/reconstruct/open). + * Safe to call repeatedly. + */ + virtual void EnsureAddOnFlowNodePointersForEditor(); #endif public: @@ -143,7 +150,7 @@ class FLOW_API UFlowNode : public UFlowNodeBase * returns true if the InOutPins array was rebuilt. */ bool RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin); bool RebuildPinArray(const TArray& NewPins, TArray& InOutPins, const FFlowPin& DefaultPin); -#endif // WITH_EDITOR; +#endif /* Always use default range for nodes with user-created outputs i.e. Execution Sequence. */ void SetNumberedInputPins(const uint8 FirstNumber = 0, const uint8 LastNumber = 1); @@ -197,7 +204,6 @@ class FLOW_API UFlowNode : public UFlowNodeBase public: #if WITH_EDITOR void SetConnections(const TMap& InConnections); - virtual void OnConnectionsChanged(const TMap& OldConnections) {} #endif FConnectedPin GetConnection(const FName OutputName) const { return Connections.FindRef(OutputName); } @@ -216,8 +222,24 @@ class FLOW_API UFlowNode : public UFlowNodeBase UFUNCTION(BlueprintPure, Category= "FlowNode") bool IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound = true) const; - bool IsInputConnected(const FFlowPin& FlowPin, FGuid* FoundGuid = nullptr, FName* OutConnectedPinName = nullptr) const; - bool IsOutputConnected(const FFlowPin& FlowPin, FGuid* FirstFoundGuid = nullptr, FName* OutFirstConnectedPinName = nullptr) const; + // Preferred signatures for: + // - exec output pins + // - data input pins + // ... otherwise use the array signatures below + bool FindFirstInputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const; + bool FindFirstOutputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const; + bool FindFirstInputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const; + bool FindFirstOutputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const; + + // Preferred signatures for: + // - exec input pins + // - data output pins + // - cases where you do not need the connection info (with ConnectedPins == nullptr) + // ... otherwise use the non-array signatures above + bool FindInputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins = nullptr) const; + bool FindOutputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins = nullptr) const; + bool FindInputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins = nullptr) const; + bool FindOutputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins = nullptr) const; FFlowPin* FindInputPinByName(const FName& PinName); FFlowPin* FindOutputPinByName(const FName& PinName); @@ -229,8 +251,14 @@ class FLOW_API UFlowNode : public UFlowNodeBase protected: /* Slow and fast lookup functions, based on whether we are proactively caching the connections for quick lookup * in the Connections array (by PinCategory). */ - bool FindConnectedNodeForPinFast(const FName& FlowPinName, FGuid* FoundGuid = nullptr, FName* OutConnectedPinName = nullptr) const; - bool FindConnectedNodeForPinSlow(const FName& FlowPinName, FGuid* FoundGuid = nullptr, FName* OutConnectedPinName = nullptr) const; + bool FindConnectedNodeForPinCached(const FName& FlowPinName, FConnectedPin& ConnectedPin) const; + bool FindConnectedNodeForPinUncached(const FName& FlowPinName, TArray* ConnectedPins = nullptr) const; + + /* Helper templates for Find*PinConnection* functions */ + template + bool FindFirstPinConnection(const FFlowPin& FlowPin, const TArray& FlowPinArray, FConnectedPin& FirstConnectedPin) const; + template + bool FindPinConnections(const FFlowPin& FlowPin, const TArray& FlowPinArray, TArray* ConnectedPins) const; /* Return all connections to a Pin this Node knows about. * Connections are only stored on one of the Nodes they connect depending on pin type. @@ -238,6 +266,17 @@ class FLOW_API UFlowNode : public UFlowNodeBase * Use UFlowAsset::GetAllPinsConnectedToPin() to do a guaranteed find of all Connections. */ TArray GetKnownConnectionsToPin(const FConnectedPin& Pin) const; +#if WITH_EDITOR + static void BuildConnectionChangeList( + const UFlowAsset& FlowAsset, + const TMap& OldConnections, + const TMap& NewConnections, + TArray& OutChanges); + + /* Broadcasts OnEditorPinConnectionsChanged to this node and all AddOns */ + void BroadcastEditorPinConnectionsChanged(const TArray& Changes); +#endif + ////////////////////////////////////////////////////////////////////////// // Data Pins diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h index 6bbd3d9b..82be586a 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBase.h +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -11,6 +11,7 @@ #include "FlowTags.h" // used by subclasses #include "FlowTypes.h" #include "Types/FlowDataPinResults.h" +#include "Types/FlowPinConnectionChange.h" #include "Types/FlowPinTypeTemplates.h" #include "FlowNodeBase.generated.h" @@ -157,7 +158,12 @@ class FLOW_API UFlowNodeBase virtual TArray GetContextOutputs() const override; // -- #endif - + + /** Called in the editor when this node's pin connections change. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "On Editor Pin Connections Changed") + void K2_OnEditorPinConnectionsChanged(const TArray& Changes); + virtual void OnEditorPinConnectionsChanged(const TArray& Changes) { K2_OnEditorPinConnectionsChanged(Changes); } + ////////////////////////////////////////////////////////////////////////// // Owners diff --git a/Source/Flow/Public/Types/FlowPinConnectionChange.h b/Source/Flow/Public/Types/FlowPinConnectionChange.h new file mode 100644 index 00000000..264c6d2b --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinConnectionChange.h @@ -0,0 +1,49 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "UObject/Object.h" +#include "FlowPinConnectionChange.generated.h" + +class UFlowNode; + +/** + * Editor-only representation of a change to a node pin's connection. + * PinName is the *final* pin name visible on the node (after any disambiguation / mangling). + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinConnectionChange +{ + GENERATED_BODY() + +public: + + FFlowPinConnectionChange() = default; + explicit FFlowPinConnectionChange( + const FName& ChangedPinName, + UFlowNode* InOldConnectedNode, + const FName& InOldConnectedPinName, + UFlowNode* InNewConnectedNode, + const FName& InNewConnectedPinName) + : PinName(ChangedPinName) + , OldConnectedNode(InOldConnectedNode) + , OldConnectedPinName(InOldConnectedPinName) + , NewConnectedNode(InNewConnectedNode) + , NewConnectedPinName(InNewConnectedPinName) + {} + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Flow") + FName PinName = NAME_None; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Flow") + UFlowNode* OldConnectedNode = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + FName OldConnectedPinName = NAME_None; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + UFlowNode* NewConnectedNode = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + FName NewConnectedPinName = NAME_None; +}; diff --git a/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp index 12608a73..194e1d3f 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp @@ -77,6 +77,12 @@ bool UFlowAssetParamsFactory::ShowParentPickerDialog() SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateChildParamsHelp", "Choose Parent Flow Asset Params:\n")) + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) [ ParamsPicker ] diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp new file mode 100644 index 00000000..1286546d --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp @@ -0,0 +1,92 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowDetailsAddOnUI.h" + +#include "Graph/Nodes/FlowGraphNode.h" +#include "Graph/Widgets/SGraphEditorActionMenuFlow.h" +#include "Nodes/FlowNodeBase.h" + +#include "EdGraph/EdGraph.h" +#include "UObject/UObjectIterator.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowDetailsAddOnUI" + +UFlowGraphNode* FFlowDetailsAddOnUI::FindGraphNodeForEditedObject(UObject* EditedObject) +{ + if (!IsValid(EditedObject)) + { + return nullptr; + } + + // Find any UFlowGraphNode whose node instance matches the edited object. + for (TObjectIterator It; It; ++It) + { + UFlowGraphNode* Candidate = *It; + if (!IsValid(Candidate)) + { + continue; + } + + UObject* NodeInstance = Candidate->GetFlowNodeBase(); + if (NodeInstance == EditedObject) + { + return Candidate; + } + } + + return nullptr; +} + +UEdGraph* FFlowDetailsAddOnUI::GetOwningEdGraph(UFlowGraphNode* GraphNode) +{ + return IsValid(GraphNode) ? GraphNode->GetGraph() : nullptr; +} + +bool FFlowDetailsAddOnUI::CanAttachAddOn(UObject* EditedObject) +{ + UFlowGraphNode* GraphNode = FindGraphNodeForEditedObject(EditedObject); + if (!IsValid(GraphNode)) + { + return false; + } + + return IsValid(GetOwningEdGraph(GraphNode)); +} + +TSharedRef FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(UObject* EditedObject) +{ + UFlowGraphNode* GraphNode = FindGraphNodeForEditedObject(EditedObject); + UEdGraph* Graph = GetOwningEdGraph(GraphNode); + + if (!IsValid(GraphNode) || !IsValid(Graph)) + { + return SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnUnavailable", "Attach AddOn is unavailable (no owning graph node found).")); + } + + return BuildAttachAddOnMenuContent(Graph, GraphNode); +} + +TSharedRef FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(UEdGraph* Graph, UFlowGraphNode* GraphNode) +{ + if (!IsValid(Graph) || !IsValid(GraphNode)) + { + return SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnUnavailable2", "Attach AddOn is unavailable.")); + } + + // Wrap for sizing similar to the context menu widget + return SNew(SBox) + .WidthOverride(420.0f) + .HeightOverride(520.0f) + [ + SNew(SGraphEditorActionMenuFlow) + .GraphObj(Graph) + .GraphNode(GraphNode) + .AutoExpandActionMenu(true) + ]; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp index 3ce051f4..82947792 100644 --- a/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp @@ -1,9 +1,17 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "DetailCustomizations/FlowNodeAddOn_Details.h" + #include "AddOns/FlowNodeAddOn.h" +#include "DetailCustomizations/FlowDetailsAddOnUI.h" +#include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Input/SComboButton.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowNodeAddOnDetails" void FFlowNodeAddOn_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { @@ -14,6 +22,57 @@ void FFlowNodeAddOn_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout DetailLayout.HideCategory(TEXT("FlowNodeAddOn")); } + // Cache edited addon + { + TArray> Objects; + DetailLayout.GetObjectsBeingCustomized(Objects); + + EditedAddOn = nullptr; + + for (const TWeakObjectPtr& Obj : Objects) + { + if (UFlowNodeAddOn* AsAddOn = Cast(Obj.Get())) + { + EditedAddOn = AsAddOn; + + break; + } + } + } + + // Add "Attach AddOn..." dropdown (menu button) + if (EditedAddOn.IsValid() && !DetailLayout.HasClassDefaultObject()) + { + IDetailCategoryBuilder& AddOnsCategory = DetailLayout.EditCategory( + TEXT("AddOns"), + LOCTEXT("AddOnsCategory", "AddOns"), + ECategoryPriority::Important); + + AddOnsCategory.AddCustomRow(LOCTEXT("AttachAddOnSearch", "Attach AddOn")) + .WholeRowContent() + [ + SNew(SComboButton) + .ButtonContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnButton", "Attach AddOn...")) + ] + .ToolTipText(LOCTEXT("AttachAddOnButtonTooltip", "Attach an AddOn to the selected node/addon.")) + .IsEnabled_Lambda([this]() + { + return EditedAddOn.IsValid() && FFlowDetailsAddOnUI::CanAttachAddOn(EditedAddOn.Get()); + }) + .OnGetMenuContent_Lambda([this]() + { + return EditedAddOn.IsValid() + ? FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(EditedAddOn.Get()) + : SNullWidget::NullWidget; + }) + ]; + } + // Call base template to set up rebuild delegate wiring TFlowDataPinValueOwnerCustomization::CustomizeDetails(DetailLayout); } + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp index 41c09eb6..9a699f8b 100644 --- a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp @@ -1,9 +1,17 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "DetailCustomizations/FlowNode_Details.h" + +#include "DetailCustomizations/FlowDetailsAddOnUI.h" #include "Nodes/FlowNode.h" +#include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Input/SComboButton.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowNodeDetails" void FFlowNode_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { @@ -13,6 +21,55 @@ void FFlowNode_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) DetailLayout.HideCategory(TEXT("FlowNode")); } + // Cache edited object + { + TArray> Objects; + DetailLayout.GetObjectsBeingCustomized(Objects); + + EditedNode = nullptr; + for (const TWeakObjectPtr& Obj : Objects) + { + if (UFlowNode* AsNode = Cast(Obj.Get())) + { + EditedNode = AsNode; + break; + } + } + } + + // Add "Attach AddOn..." dropdown (menu button) + if (EditedNode.IsValid() && !DetailLayout.HasClassDefaultObject()) + { + IDetailCategoryBuilder& AddOnsCategory = DetailLayout.EditCategory( + TEXT("AddOns"), + LOCTEXT("AddOnsCategory", "AddOns"), + ECategoryPriority::Important); + + AddOnsCategory.AddCustomRow(LOCTEXT("AttachAddOnSearch", "Attach AddOn")) + .WholeRowContent() + [ + SNew(SComboButton) + .ButtonContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnButton", "Attach AddOn...")) + ] + .ToolTipText(LOCTEXT("AttachAddOnButtonTooltip", "Attach an AddOn to the selected node/addon.")) + .IsEnabled_Lambda([this]() + { + return EditedNode.IsValid() && FFlowDetailsAddOnUI::CanAttachAddOn(EditedNode.Get()); + }) + .OnGetMenuContent_Lambda([this]() + { + return EditedNode.IsValid() + ? FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(EditedNode.Get()) + : SNullWidget::NullWidget; + }) + ]; + } + // Call base template to set up rebuild delegate wiring TFlowDataPinValueOwnerCustomization::CustomizeDetails(DetailLayout); } + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp b/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp index 45c038df..29ea839f 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp @@ -7,11 +7,11 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNodesPolicy) #if WITH_EDITOR -bool FFlowGraphNodesPolicy::IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const +EFlowGraphPolicyResult FFlowGraphNodesPolicy::IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const { if (!IsValid(FlowNodeBase)) { - return false; + return EFlowGraphPolicyResult::TentativeForbidden; } const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*FlowNodeBase); @@ -19,23 +19,23 @@ bool FFlowGraphNodesPolicy::IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeB const bool bIsInAllowedCategory = !AllowedCategories.IsEmpty() && IsAnySubcategory(NodeCategoryString, AllowedCategories); if (bIsInAllowedCategory) { - return true; + return EFlowGraphPolicyResult::Allowed; } const bool bIsInDisallowedCategory = !DisallowedCategories.IsEmpty() && IsAnySubcategory(NodeCategoryString, DisallowedCategories); if (bIsInDisallowedCategory) { - return false; + return EFlowGraphPolicyResult::Forbidden; } if (AllowedCategories.IsEmpty()) { // If the AllowedCategories is empty, then we consider any node that isn't disallowed, as allowed - return true; + return EFlowGraphPolicyResult::TentativeAllowed; } else { - return false; + return EFlowGraphPolicyResult::TentativeForbidden; } } diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp index 575fbf7d..b660570c 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp @@ -1170,17 +1170,36 @@ void UFlowGraphSchema::ApplyNodeOrAddOnFilter(const UFlowAsset* EditedFlowAsset, return; } + using namespace EFlowGraphPolicyResult_Classifiers; + UFlowNodeBase* FlowNodeBaseCDO = FlowNodeClass->GetDefaultObject(); - if (const FFlowGraphNodesPolicy* FlowAssetPolicy = GraphSettings->PerAssetSubclassFlowNodePolicies.Find(FSoftClassPath(EditedFlowAsset->GetClass()))) + UClass* CurFlowAssetClass = EditedFlowAsset->GetClass(); + + // Crawl up the superclass parentage until we find a strict result, otherwise accept the best tentative result + EFlowGraphPolicyResult BestResult = EFlowGraphPolicyResult::Invalid; + while (IsValid(CurFlowAssetClass) && CurFlowAssetClass->IsChildOf()) { - const bool bIsAllowedByPolicy = FlowAssetPolicy->IsNodeAllowedByPolicy(FlowNodeBaseCDO); - if (!bIsAllowedByPolicy) + if (const FFlowGraphNodesPolicy* FlowAssetPolicy = GraphSettings->PerAssetSubclassFlowNodePolicies.Find(FSoftClassPath(CurFlowAssetClass))) { - return; + const EFlowGraphPolicyResult CurPolicyResult = FlowAssetPolicy->IsNodeAllowedByPolicy(FlowNodeBaseCDO); + + // Choose the most applicable result for this class + BestResult = MergePolicyResult(BestResult, CurPolicyResult); + + if (IsStrictPolicyResult(BestResult)) + { + // A strict policy stops the crawl up the superclass parentage + break; + } } + + CurFlowAssetClass = CurFlowAssetClass->GetSuperClass(); } - FilteredNodes.Emplace(FlowNodeBaseCDO); + if (IsAnyAllowedPolicyResult(BestResult)) + { + FilteredNodes.Emplace(FlowNodeBaseCDO); + } } void UFlowGraphSchema::GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* EditedFlowAsset, const FString& CategoryName) diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index fab76e8e..c146263a 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -320,9 +320,15 @@ void UFlowGraphNode::ReconstructNode() return; } - bIsReconstructingNode = true; + TGuardValue GuardIsResonstructingNode(bIsReconstructingNode, true); + FScopedTransaction Transaction(LOCTEXT("ReconstructNode", "Reconstruct Node"), !GUndo); + if (UFlowNode* FlowNode = Cast(NodeInstance)) + { + FlowNode->SetupForEditing(*this); + } + const bool bAnyPinsUpdated = TryUpdateNodePins(); // Updates all pins of the Flow Node (native pins, meta auto pins, and context pins which include data pins for now) const bool bAreGraphPinsMismatched = !CheckGraphPinsMatchNodePins(); // This must be called last since it checks the existing graph node against the cleaned up Flow Node instance @@ -357,16 +363,9 @@ void UFlowGraphNode::ReconstructNode() bNeedsFullReconstruction = false; } - if (UFlowNode* FlowNode = Cast(NodeInstance)) - { - FlowNode->UpdateNodeConfigText(); - } - // This ensures the graph editor 'Refresh' button still rebuilds all the graph widgets even if the FlowGraphNode has nothing to update // Ideally we could get rid of the 'Refresh' button, but I think it will keep being useful, esp. for users making rough custom widgets (void)OnReconstructNodeCompleted.ExecuteIfBound(); - - bIsReconstructingNode = false; } void UFlowGraphNode::AllocateDefaultPins() @@ -533,6 +532,7 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra Section.AddMenuEntry(GenericCommands.Cut); Section.AddMenuEntry(GenericCommands.Copy); Section.AddMenuEntry(GenericCommands.Duplicate); + Section.AddMenuEntry(GenericCommands.Paste); Section.AddMenuEntry(GraphCommands.BreakNodeLinks); @@ -1848,7 +1848,7 @@ bool UFlowGraphNode::TryUpdateNodePins() const } // Ensure the AddOns for this FlowNode have their FlowNode pointer set - FlowNodeInstance->EnsureSetFlowNodeForEditorForAllAddOns(); + FlowNodeInstance->EnsureAddOnFlowNodePointersForEditor(); // Attempt to update auto-generated pins // This must be called first, it updates the underlying data for data pins of the Flow Node @@ -1878,7 +1878,7 @@ bool UFlowGraphNode::TryUpdateNodePins() const // Fix up old pins on the CDO UFlowNode* MutableCDO = const_cast(FlowNodeCDO); - MutableCDO->EnsureSetFlowNodeForEditorForAllAddOns(); + MutableCDO->EnsureAddOnFlowNodePointersForEditor(); MutableCDO->FixupDataPinTypes(); const bool bIsRerouteGraphNode = (Cast(this) != nullptr); diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h new file mode 100644 index 00000000..799f8942 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h @@ -0,0 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Object.h" + +class SWidget; +class UEdGraph; +class UFlowGraphNode; + +/** +* Shared UI helpers for "Attach AddOn..." in details panels. +*/ +class FLOWEDITOR_API FFlowDetailsAddOnUI +{ +public: + /** Try to resolve the edited UObject (node or addon instance) to its UFlowGraphNode wrapper. */ + static UFlowGraphNode* FindGraphNodeForEditedObject(UObject* EditedObject); + + /** Return the owning UEdGraph for the graph node. */ + static UEdGraph* GetOwningEdGraph(UFlowGraphNode* GraphNode); + + /** Returns true if we can open/build the Attach AddOn menu for the edited object. */ + static bool CanAttachAddOn(UObject* EditedObject); + + /** Builds the menu widget content for attaching an addon (the same selector UI used by the context menu). */ + static TSharedRef BuildAttachAddOnMenuContent(UObject* EditedObject); + + /** Lower-level overload if caller already resolved graph + node. */ + static TSharedRef BuildAttachAddOnMenuContent(UEdGraph* Graph, UFlowGraphNode* GraphNode); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h index 9d69651a..4f980bc8 100644 --- a/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h @@ -18,4 +18,7 @@ class FFlowNodeAddOn_Details final : public TFlowDataPinValueOwnerCustomization< // IDetailCustomization virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; // -- -}; + +private: + TWeakObjectPtr EditedAddOn = nullptr; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h index 7d4148f4..2b2b9205 100644 --- a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h @@ -18,4 +18,7 @@ class FFlowNode_Details final : public TFlowDataPinValueOwnerCustomization EditedNode = nullptr; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h b/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h index c6178264..5684006f 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h @@ -1,10 +1,57 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once +#include "Types/FlowEnumUtils.h" + #include "FlowGraphNodesPolicy.generated.h" class UFlowNodeBase; +UENUM() +enum class EFlowGraphPolicyResult : int8 +{ + // Forbidden by the policy unless a more specific rule applies + TentativeForbidden, + + // Allowed by the policy unless a more specific rule applies + TentativeAllowed, + + // Strictly forbidden by the policy + Forbidden, + + // Strictly allowed by the policy + Allowed, + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), + + // Subrange for strict results + StrictFirst = Forbidden UMETA(Hidden), + StrictLast = Allowed UMETA(Hidden), + + // Subrange for tentative results + TentativeFirst = TentativeForbidden UMETA(Hidden), + TentativeLast = TentativeAllowed UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowGraphPolicyResult); + +namespace EFlowGraphPolicyResult_Classifiers +{ + FORCEINLINE bool IsStrictPolicyResult(const EFlowGraphPolicyResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowGraphPolicyResult::Strict); } + FORCEINLINE bool IsTentativePolicyResult(const EFlowGraphPolicyResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowGraphPolicyResult::Tentative); } + FORCEINLINE bool IsAnyAllowedPolicyResult(const EFlowGraphPolicyResult Result) { return Result == EFlowGraphPolicyResult::Allowed || Result == EFlowGraphPolicyResult::TentativeAllowed; } + FORCEINLINE bool IsAnyForbiddenPolicyResult(const EFlowGraphPolicyResult Result) { return Result == EFlowGraphPolicyResult::Forbidden || Result == EFlowGraphPolicyResult::TentativeForbidden; } + + FORCEINLINE EFlowGraphPolicyResult MergePolicyResult(const EFlowGraphPolicyResult Result0, const EFlowGraphPolicyResult Result1) + { + checkf(!(IsStrictPolicyResult(Result0) && IsStrictPolicyResult(Result1)), TEXT("Should not be deciding between two strict results")); + + // Numerically prefer: Allowed or Forbidden > TentativeAllowed > TentativeForbidden > Invalid + return static_cast(FMath::Max(FlowEnum::ToInt(Result0), FlowEnum::ToInt(Result1))); + } +} + USTRUCT() struct FFlowGraphNodesPolicy { @@ -21,9 +68,10 @@ struct FFlowGraphNodesPolicy #if WITH_EDITOR public: - bool IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const; + EFlowGraphPolicyResult IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const; protected: static bool IsAnySubcategory(const FString& CheckCategory, const TArray& Categories); #endif }; +