From a41f71e04e83e550d49977e26fc979584c94382f Mon Sep 17 00:00:00 2001 From: Vasilii Bulgakov Date: Mon, 13 Apr 2026 15:54:30 +0700 Subject: [PATCH 1/3] Actor details can show flow tags in selected actors --- .../DetailCustomizations/FlowActorDetails.cpp | 57 +++++++++++++++++++ .../FlowEditor/Private/FlowEditorModule.cpp | 6 ++ .../DetailCustomizations/FlowActorDetails.h | 17 ++++++ Source/FlowEditor/Public/FlowEditorModule.h | 1 + .../Public/Graph/FlowGraphSettings.h | 8 +++ 5 files changed, 89 insertions(+) create mode 100644 Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp create mode 100644 Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp new file mode 100644 index 00000000..565c8cc2 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp @@ -0,0 +1,57 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "DetailCustomizations/FlowActorDetails.h" +#include "ActorDetailsDelegates.h" +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "FlowComponent.h" +#include "Graph/FlowGraphSettings.h" + + +FFlowActorDetails::~FFlowActorDetails() +{ + OnExtendActorDetails.RemoveAll(this); +} + +void FFlowActorDetails::Register() +{ + if (GetDefault()->bShowFlowTagsInActorDetails) + { + OnExtendActorDetails.AddSP(this, &FFlowActorDetails::AddFlowCategory); + } +} + +void FFlowActorDetails::AddFlowCategory(class IDetailLayoutBuilder& Details, const FGetSelectedActors& GetSelectedActors) +{ + if (GetSelectedActors.IsBound()) + { + TArray Components; + const TArray>& SelectedActors = GetSelectedActors.Execute(); + for (auto& WeakActor : SelectedActors) + { + if (const AActor* Actor = WeakActor.Get()) + { + TInlineComponentArray FlowComps(Actor); + Components.Append(FlowComps); + } + } + + if (!Components.IsEmpty()) + { + const ECategoryPriority::Type Priority = GetDefault()->bMarkFlowCategoryImportant ? ECategoryPriority::Important : ECategoryPriority::Default; + + const FText CategoryName = FText::Format(NSLOCTEXT("FlowDetails", "FlowCategoryFormat", "Flow Components: {0} "), FText::AsNumber(Components.Num())); + const FString Tooltip = FString::JoinBy(Components, LINE_TERMINATOR, [](const UObject* Object) + { + const UActorComponent* Component = CastChecked(Object); + const FString Actor = Component->GetOwner() ? Component->GetOwner()->GetActorNameOrLabel() : TEXT("None"); + return FString(Actor + TEXT(".") + Object->GetName()); + }); + + IDetailCategoryBuilder& Category = Details.EditCategory(TEXT("Flow"), CategoryName, Priority); + Category.SetToolTip(FText::FromString(Tooltip)); + Category.AddExternalObjectProperty(Components, GET_MEMBER_NAME_CHECKED(UFlowComponent, IdentityTags)); + } + } +} diff --git a/Source/FlowEditor/Private/FlowEditorModule.cpp b/Source/FlowEditor/Private/FlowEditorModule.cpp index 09906bf8..3d8410d8 100644 --- a/Source/FlowEditor/Private/FlowEditorModule.cpp +++ b/Source/FlowEditor/Private/FlowEditorModule.cpp @@ -31,6 +31,7 @@ #include "DetailCustomizations/FlowAssetParamsPtrCustomization.h" #include "DetailCustomizations/FlowDataPinValueOwnerCustomizations.h" #include "DetailCustomizations/FlowDataPinValueStandardCustomizations.h" +#include "DetailCustomizations/FlowActorDetails.h" #include "FlowAsset.h" #include "AddOns/FlowNodeAddOn.h" @@ -90,6 +91,9 @@ void FFlowEditorModule::StartupModule() FlowTrackCreateEditorHandle = SequencerModule.RegisterTrackEditor(FOnCreateTrackEditor::CreateStatic(&FFlowTrackEditor::CreateTrackEditor)); RegisterDetailCustomizations(); + + ActorDetails = MakeShared(); + ActorDetails->Register(); // register asset indexers if (FModuleManager::Get().IsModuleLoaded(AssetSearchModuleName)) @@ -120,6 +124,8 @@ void FFlowEditorModule::ShutdownModule() UnregisterDetailCustomizations(); UnregisterAssets(); + + ActorDetails.Reset(); // unregister track editors ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked("Sequencer"); diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h new file mode 100644 index 00000000..c555f930 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h @@ -0,0 +1,17 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "ActorDetailsDelegates.h" + +/** + * + */ +class FLOWEDITOR_API FFlowActorDetails : public TSharedFromThis +{ +public: + virtual ~FFlowActorDetails(); + virtual void Register(); + virtual void AddFlowCategory(class IDetailLayoutBuilder& Details, const FGetSelectedActors& GetSelectedActors); +}; diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index b6ce4e18..55b123c1 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -32,6 +32,7 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface bool bIsRegisteredForAssetChanges = false; + TSharedPtr ActorDetails; public: virtual void StartupModule() override; virtual void ShutdownModule() override; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h index 1c264284..5e14a6dc 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h @@ -181,6 +181,14 @@ class FLOWEDITOR_API UFlowGraphSettings : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "Wires", meta = (ClampMin = 0.0f)) float SelectedWireThickness; + + + UPROPERTY(EditAnywhere, config, Category = "Details", meta = (ConfigRestartRequired = true)) + bool bShowFlowTagsInActorDetails = true; + + /** FlowComponent only. Move category Flow to the top of details panel */ + UPROPERTY(EditAnywhere, config, Category = "Details") + bool bMarkFlowCategoryImportant = true; public: virtual FName GetCategoryName() const override { return FName("Flow Graph"); } From 901ab68171f8d27718529573480eab32ed83f745 Mon Sep 17 00:00:00 2001 From: Vasilii Bulgakov Date: Mon, 13 Apr 2026 16:37:39 +0700 Subject: [PATCH 2/3] Better category name to avoid conflicts --- .../Private/DetailCustomizations/FlowActorDetails.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp index 565c8cc2..c5cc1522 100644 --- a/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp @@ -49,7 +49,7 @@ void FFlowActorDetails::AddFlowCategory(class IDetailLayoutBuilder& Details, con return FString(Actor + TEXT(".") + Object->GetName()); }); - IDetailCategoryBuilder& Category = Details.EditCategory(TEXT("Flow"), CategoryName, Priority); + IDetailCategoryBuilder& Category = Details.EditCategory(TEXT("_FlowDetails"), CategoryName, Priority); Category.SetToolTip(FText::FromString(Tooltip)); Category.AddExternalObjectProperty(Components, GET_MEMBER_NAME_CHECKED(UFlowComponent, IdentityTags)); } From cddfc37bff2536a411c41fe917bd10a3fb0a1f8e Mon Sep 17 00:00:00 2001 From: Vasilii Bulgakov Date: Mon, 15 Jun 2026 21:43:15 +0700 Subject: [PATCH 3/3] fix broken sliders --- Source/FlowEditor/FlowEditor.Build.cs | 1 + .../DetailCustomizations/FlowActorDetails.cpp | 247 ++++++++++++++++-- .../DetailCustomizations/FlowActorDetails.h | 4 +- .../Public/Graph/FlowGraphSettings.h | 2 +- 4 files changed, 232 insertions(+), 22 deletions(-) diff --git a/Source/FlowEditor/FlowEditor.Build.cs b/Source/FlowEditor/FlowEditor.Build.cs index bf270ec9..03c11e56 100644 --- a/Source/FlowEditor/FlowEditor.Build.cs +++ b/Source/FlowEditor/FlowEditor.Build.cs @@ -36,6 +36,7 @@ public FlowEditor(ReadOnlyTargetRules target) : base(target) "EngineAssetDefinitions", "GraphEditor", "GameplayTags", + "GameplayTagsEditor", "InputCore", "Json", "JsonUtilities", diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp index c5cc1522..ef0054aa 100644 --- a/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowActorDetails.cpp @@ -1,12 +1,202 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "DetailCustomizations/FlowActorDetails.h" #include "ActorDetailsDelegates.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" #include "FlowComponent.h" +#include "IDetailChildrenBuilder.h" +#include "ISinglePropertyView.h" +#include "SGameplayTagContainerCombo.h" +#include "SGameplayTagPicker.h" #include "Graph/FlowGraphSettings.h" +#include "HAL/PlatformApplicationMisc.h" + +#define LOCTEXT_NAMESPACE "FlowDetails" + +// Duplicate for UE::GameplayTags::EditorUtilities +namespace FlowActorDetails::TagHelpers +{ + FString GameplayTagExportText(const FGameplayTag Tag) + { + FString ExportString; + FGameplayTag::StaticStruct()->ExportText(ExportString, &Tag, &Tag, /*OwnerObject*/nullptr, /*PortFlags*/0, /*ExportRootScope*/nullptr); + return ExportString; + } + + FGameplayTag GameplayTagTryImportText(const FString& Text) + { + FGameplayTag Tag; + FGameplayTag::StaticStruct()->ImportText(*Text, &Tag, /*OwnerObject*/nullptr, PPF_None, nullptr, FGameplayTag::StaticStruct()->GetName(), /*bAllowNativeOverride*/true); + return Tag; + } + + FString GameplayTagContainerExportText(const FGameplayTagContainer& TagContainer) + { + FString ExportString; + FGameplayTagContainer::StaticStruct()->ExportText(ExportString, &TagContainer, &TagContainer, /*OwnerObject*/nullptr, /*PortFlags*/0, /*ExportRootScope*/nullptr); + return ExportString; + } + + FGameplayTagContainer GameplayTagContainerTryImportText(const FString& Text) + { + FGameplayTagContainer TagContainer; + FGameplayTagContainer::StaticStruct()->ImportText(*Text, &TagContainer, /*OwnerObject*/nullptr, PPF_None, nullptr, FGameplayTagContainer::StaticStruct()->GetName(), /*bAllowNativeOverride*/true); + return TagContainer; + } + +} + + +class FFlowActorDetailsBuilder : public IDetailCustomNodeBuilder, public TSharedFromThis +{ +public: + FFlowActorDetailsBuilder(const FGetSelectedActors& GetSelectedActors) + : Getter(GetSelectedActors) + { + + } + + //~ Begin IDetailCustomNodeBuilder interface + virtual void GenerateHeaderRowContent( FDetailWidgetRow& NodeRow ) override { } + virtual void GenerateChildContent(IDetailChildrenBuilder& ChildBuilder) override + { + TArray Components = FFlowActorDetails::GetSelectedFlowComponents(Getter); + if (Components.Num() == 1) + { + // Special case because AddExternalObjectProperty breaks sliders + const FText IdentityRow = LOCTEXT("RowIdentityTags", "Identity Tags"); + + FPropertyEditorModule& Module = FModuleManager::GetModuleChecked("PropertyEditor"); + FSinglePropertyParams Params; + Params.NamePlacement = EPropertyNamePlacement::Hidden; + PropertyView = Module.CreateSingleProperty(Components[0], GET_MEMBER_NAME_CHECKED(UFlowComponent, IdentityTags), Params); + + if (PropertyView.IsValid()) + { + FUIAction Copy, Paste; + TSharedPtr ViewHandle = PropertyView->GetPropertyHandle(); + if (ViewHandle.IsValid()) + { + ViewHandle->CreateDefaultPropertyCopyPasteActions(Copy, Paste); + Paste = FUIAction( + FExecuteAction::CreateSP(this, &FFlowActorDetailsBuilder::PasteTags), + FCanExecuteAction::CreateSP(this, &FFlowActorDetailsBuilder::CanPasteTags) ); + } + + ChildBuilder.AddCustomRow(IdentityRow) + .CopyAction(Copy) + .PasteAction(Paste) + .NameContent() + [ + SNew(STextBlock).Font(IPropertyTypeCustomizationUtils::GetRegularFont()) + .Text(IdentityRow) + ] + .ValueContent() + [ + PropertyView.ToSharedRef() + ]; + } + } + else + { + TArray Objects; + Objects.Append(Components); + ChildBuilder.AddExternalObjectProperty(Objects, GET_MEMBER_NAME_CHECKED(UFlowComponent, IdentityTags)); + } + } + + virtual bool InitiallyCollapsed() const override { return false; } + virtual FName GetName() const override + { + static const FName Name("FActorFlowDetailsBuilder"); + return Name; + } + //~ End IDetailCustomNodeBuilder interface + + + void PasteTags() + { + if (!PropertyView.IsValid()) + { + return; + } + + TSharedPtr StructPropertyHandle = PropertyView->GetPropertyHandle(); + if (!StructPropertyHandle.IsValid()) + { + return; + } + + + FString PastedText; + FPlatformApplicationMisc::ClipboardPaste(PastedText); + bool bHandled = false; + + // Try to paste single tag + const FGameplayTag PastedTag = FlowActorDetails::TagHelpers::GameplayTagTryImportText(PastedText); + if (PastedTag.IsValid()) + { + TArray NewValues; + SGameplayTagPicker::EnumerateEditableTagContainersFromPropertyHandle(StructPropertyHandle.ToSharedRef(), [&NewValues, PastedTag](const FGameplayTagContainer& EditableTagContainer) + { + FGameplayTagContainer TagContainerCopy = EditableTagContainer; + TagContainerCopy.AddTag(PastedTag); + + NewValues.Add(TagContainerCopy.ToString()); + return true; + }); + + FScopedTransaction Transaction(LOCTEXT("GameplayTagContainerCustomization_PasteTag", "Paste Gameplay Tag")); + StructPropertyHandle->SetPerObjectValues(NewValues); + bHandled = true; + } + + // Try to paste a container + if (!bHandled) + { + const FGameplayTagContainer PastedTagContainer = FlowActorDetails::TagHelpers::GameplayTagContainerTryImportText(PastedText); + if (PastedTagContainer.IsValid()) + { + // From property + FScopedTransaction Transaction(LOCTEXT("GameplayTagContainerCustomization_PasteTagContainer", "Paste Gameplay Tag Container")); + StructPropertyHandle->SetValueFromFormattedString(PastedText); + bHandled = true; + } + } + } + + bool CanPasteTags() + { + if (!PropertyView.IsValid() || !PropertyView->GetPropertyHandle().IsValid()) + { + return false; + } + + FString PastedText; + FPlatformApplicationMisc::ClipboardPaste(PastedText); + + const FGameplayTag PastedTag = FlowActorDetails::TagHelpers::GameplayTagTryImportText(PastedText); + if (PastedTag.IsValid()) + { + return true; + } + + const FGameplayTagContainer PastedTagContainer = FlowActorDetails::TagHelpers::GameplayTagContainerTryImportText(PastedText); + if (PastedTagContainer.IsValid()) + { + return true; + } + + return false; + } + +private: + FGetSelectedActors Getter; + TSharedPtr PropertyView; +}; FFlowActorDetails::~FFlowActorDetails() @@ -16,32 +206,26 @@ FFlowActorDetails::~FFlowActorDetails() void FFlowActorDetails::Register() { - if (GetDefault()->bShowFlowTagsInActorDetails) - { - OnExtendActorDetails.AddSP(this, &FFlowActorDetails::AddFlowCategory); - } + OnExtendActorDetails.AddSP(this, &FFlowActorDetails::AddFlowCategory); } void FFlowActorDetails::AddFlowCategory(class IDetailLayoutBuilder& Details, const FGetSelectedActors& GetSelectedActors) -{ +{ + const bool bEnabled = GetDefault()->bShowFlowTagsInActorDetails; + + if (!bEnabled) + { + return; + } + if (GetSelectedActors.IsBound()) { - TArray Components; - const TArray>& SelectedActors = GetSelectedActors.Execute(); - for (auto& WeakActor : SelectedActors) - { - if (const AActor* Actor = WeakActor.Get()) - { - TInlineComponentArray FlowComps(Actor); - Components.Append(FlowComps); - } - } - + const TArray Components = FFlowActorDetails::GetSelectedFlowComponents(GetSelectedActors); if (!Components.IsEmpty()) { - const ECategoryPriority::Type Priority = GetDefault()->bMarkFlowCategoryImportant ? ECategoryPriority::Important : ECategoryPriority::Default; + const ECategoryPriority::Type Priority = GetDefault()->bMarkFlowCategoryImportant ? ECategoryPriority::Important : ECategoryPriority::Default; - const FText CategoryName = FText::Format(NSLOCTEXT("FlowDetails", "FlowCategoryFormat", "Flow Components: {0} "), FText::AsNumber(Components.Num())); + const FText CategoryName = FText::Format(LOCTEXT("FlowCategoryFormat", "Flow Components: {0} "), FText::AsNumber(Components.Num())); const FString Tooltip = FString::JoinBy(Components, LINE_TERMINATOR, [](const UObject* Object) { const UActorComponent* Component = CastChecked(Object); @@ -51,7 +235,30 @@ void FFlowActorDetails::AddFlowCategory(class IDetailLayoutBuilder& Details, con IDetailCategoryBuilder& Category = Details.EditCategory(TEXT("_FlowDetails"), CategoryName, Priority); Category.SetToolTip(FText::FromString(Tooltip)); - Category.AddExternalObjectProperty(Components, GET_MEMBER_NAME_CHECKED(UFlowComponent, IdentityTags)); + Category.AddCustomBuilder(MakeShared(GetSelectedActors)); } } } + +TArray FFlowActorDetails::GetSelectedFlowComponents(const FGetSelectedActors& GetSelectedActors) +{ + TArray Components; + + if (GetSelectedActors.IsBound()) + { + const TArray>& SelectedActors = GetSelectedActors.Execute(); + for (auto& WeakActor : SelectedActors) + { + const AActor* Actor = WeakActor.Get(); + if (Actor && !Actor->IsA(AWorldSettings::StaticClass())) + { + TInlineComponentArray FlowComps(Actor); + Components.Append(FlowComps); + } + } + } + return Components; +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h index c555f930..3c1cfa6b 100644 --- a/Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowActorDetails.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once @@ -14,4 +14,6 @@ class FLOWEDITOR_API FFlowActorDetails : public TSharedFromThis GetSelectedFlowComponents(const FGetSelectedActors& GetSelectedActors); }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h index 5e14a6dc..db9916e4 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h @@ -183,7 +183,7 @@ class FLOWEDITOR_API UFlowGraphSettings : public UDeveloperSettings float SelectedWireThickness; - UPROPERTY(EditAnywhere, config, Category = "Details", meta = (ConfigRestartRequired = true)) + UPROPERTY(EditAnywhere, config, Category = "Details") bool bShowFlowTagsInActorDetails = true; /** FlowComponent only. Move category Flow to the top of details panel */