├── .gitignore ├── LICENSE ├── MyCustomPlugin.uplugin ├── README.md ├── Resources └── Icon128.png ├── Source ├── MyCustomPlugin │ ├── MyCustomPlugin.Build.cs │ ├── Private │ │ ├── CustomDataAsset.cpp │ │ ├── CustomGraphNodeAsset.cpp │ │ ├── CustomNodeData.cpp │ │ └── MyCustomPlugin.cpp │ └── Public │ │ ├── CustomDataAsset.h │ │ ├── CustomGraphNodeAsset.h │ │ ├── CustomNodeData.h │ │ └── MyCustomPlugin.h └── MyCustomPluginEditor │ ├── MyCustomPluginEditor.Build.cs │ ├── Private │ ├── AssetEditor │ │ ├── CustomConnectionDrawingPolicy.cpp │ │ ├── CustomDragConnection.cpp │ │ ├── CustomPinDragDropConnection.cpp │ │ ├── EdCustomGraph.cpp │ │ ├── EdCustomGraphNode.cpp │ │ ├── EdCustomGraphSchema.cpp │ │ └── SCustomGraphNode.cpp │ ├── CustomDataAssetActions.cpp │ ├── CustomDataAssetEditor.cpp │ ├── CustomDataAssetFactory.cpp │ ├── CustomNodeEditorWidget.cpp │ ├── CustomNodeFactory.cpp │ └── MyCustomPluginEditor.cpp │ └── Public │ ├── AssetEditor │ ├── CustomConnectionDrawingPolicy.h │ ├── CustomDragConnection.h │ ├── CustomPinDragDropConnection.h │ ├── EdCustomGraph.h │ ├── EdCustomGraphNode.h │ ├── EdCustomGraphSchema.h │ └── SCustomGraphNode.h │ ├── CustomDataAssetActions.h │ ├── CustomDataAssetEditor.h │ ├── CustomDataAssetFactory.h │ ├── CustomNodeEditorWidget.h │ ├── CustomNodeFactory.h │ └── MyCustomPluginEditor.h └── graph_tool.PNG /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | *.ipa 33 | 34 | # These project files can be generated by the engine 35 | *.xcodeproj 36 | *.xcworkspace 37 | *.sln 38 | *.suo 39 | *.opensdf 40 | *.sdf 41 | *.VC.db 42 | *.VC.opendb 43 | 44 | # Precompiled Assets 45 | SourceArt/**/*.png 46 | SourceArt/**/*.tga 47 | 48 | # Binary Files 49 | Binaries/* 50 | Plugins/*/Binaries/* 51 | 52 | # Builds 53 | Build/* 54 | 55 | # Whitelist PakBlacklist-.txt files 56 | !Build/*/ 57 | Build/*/** 58 | !Build/*/PakBlacklist*.txt 59 | 60 | # Don't ignore icon files in Build 61 | !Build/**/*.ico 62 | 63 | # Built data for maps 64 | *_BuiltData.uasset 65 | 66 | # Configuration files generated by the Editor 67 | Saved/* 68 | 69 | # Compiled source files for the engine to use 70 | Intermediate/* 71 | Plugins/*/Intermediate/* 72 | 73 | # Cache files for the editor to use 74 | DerivedDataCache/* 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tomas Wallin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MyCustomPlugin.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "MyCustomPlugin", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "MyCustomPlugin", 20 | "Type": "Runtime", 21 | "LoadingPhase": "PreDefault" 22 | }, 23 | { 24 | "Name": "MyCustomPluginEditor", 25 | "Type": "Editor", 26 | "LoadingPhase": "Default" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A custom graph tool plugin for Unreal Engine 5 2 | 3 | This is a custom graph tool plugin for Unreal Engine 5 that I created. With this tool you can create custom nodes, establish connections between them, and get the structured data with helper functions. 4 | 5 | ## Motivation 6 | 7 | I made this tool in order to learn more about creating custom tools in Unreal Engine. This project is motivated by the perceived shortcomings in the documentation currently available for building custom tools in the Unreal Editor. My goal in creating this customized Graph Tool is to give those who want to create their own customized solutions in the Unreal Engine environment a useful tool that can also be used as a learning resource. 8 | 9 | ![The Custom Graph Tool](/graph_tool.PNG) 10 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Myggski/graph-tool-plugin-ue5/ae7926d91f8af242a16dbb4701c492ca4d32c583/Resources/Icon128.png -------------------------------------------------------------------------------- /Source/MyCustomPlugin/MyCustomPlugin.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class MyCustomPlugin : ModuleRules 4 | { 5 | public MyCustomPlugin(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 8 | bLegacyPublicIncludePaths = false; 9 | ShadowVariableWarningLevel = WarningLevel.Error; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | // ... add public include paths required here ... 14 | } 15 | ); 16 | 17 | PrivateIncludePaths.AddRange( 18 | new string[] { 19 | // ... add other private include paths required here ... 20 | "MyCustomPlugin/Private" 21 | } 22 | ); 23 | 24 | PublicDependencyModuleNames.AddRange( 25 | new string[] 26 | { 27 | "Core", 28 | "CoreUObject", 29 | "Engine", 30 | // ... add other public dependencies that you statically link with here ... 31 | } 32 | ); 33 | 34 | PrivateDependencyModuleNames.AddRange( 35 | new string[] 36 | { 37 | "CoreUObject", 38 | "Engine", 39 | "Slate", 40 | "SlateCore", 41 | // ... add private dependencies that you statically link with here ... 42 | } 43 | ); 44 | 45 | DynamicallyLoadedModuleNames.AddRange( 46 | new string[] 47 | { 48 | // ... add any modules that your module loads dynamically here ... 49 | } 50 | ); 51 | } 52 | } -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Private/CustomDataAsset.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomDataAsset.h" 2 | 3 | const TArray& UCustomDataAsset::GetRootNodes() const 4 | { 5 | return RootNodes; 6 | } 7 | 8 | int UCustomDataAsset::GetLevelNum() const 9 | { 10 | int Level = 0; 11 | TArray CurrLevelNodes = RootNodes; TArray NextLevelNodes; 12 | 13 | while (CurrLevelNodes.Num() != 0) 14 | { 15 | for (int i = 0; i < CurrLevelNodes.Num(); ++i) 16 | { 17 | UCustomGraphNodeAsset* Node = CurrLevelNodes[i]; 18 | check(Node != nullptr); 19 | 20 | for (int j = 0; j < Node->ChildrenNodes.Num(); ++j) 21 | { 22 | NextLevelNodes.Add(Node->ChildrenNodes[j]); 23 | } 24 | } 25 | 26 | CurrLevelNodes = NextLevelNodes; 27 | NextLevelNodes.Reset(); 28 | ++Level; 29 | } 30 | 31 | return Level; 32 | } 33 | 34 | const TArray UCustomDataAsset::GetNodesByLevel(int Level) 35 | { 36 | int CurrLEvel = 0; 37 | TArray Nodes; 38 | TArray NextLevelNodes; 39 | 40 | Nodes = RootNodes; 41 | 42 | while (Nodes.Num() != 0) 43 | { 44 | if (CurrLEvel == Level) 45 | break; 46 | 47 | for (int i = 0; i < Nodes.Num(); ++i) 48 | { 49 | UCustomGraphNodeAsset* Node = Nodes[i]; 50 | check(Node != nullptr); 51 | 52 | for (int j = 0; j < Node->ChildrenNodes.Num(); ++j) 53 | { 54 | NextLevelNodes.Add(Node->ChildrenNodes[j]); 55 | } 56 | } 57 | 58 | Nodes = NextLevelNodes; 59 | NextLevelNodes.Reset(); 60 | ++CurrLEvel; 61 | } 62 | 63 | return Nodes; 64 | } 65 | 66 | void UCustomDataAsset::ClearGraph() 67 | { 68 | for (int i = 0; i < AllNodes.Num(); ++i) 69 | { 70 | UCustomGraphNodeAsset* Node = AllNodes[i]; 71 | if (Node) 72 | { 73 | Node->ParentNodes.Empty(); 74 | Node->ChildrenNodes.Empty(); 75 | } 76 | } 77 | 78 | AllNodes.Empty(); 79 | RootNodes.Empty(); 80 | } -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Private/CustomGraphNodeAsset.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomGraphNodeAsset.h" 2 | 3 | UCustomGraphNodeAsset::UCustomGraphNodeAsset() 4 | { 5 | 6 | } 7 | 8 | bool UCustomGraphNodeAsset::IsLeafNode() const 9 | { 10 | return ChildrenNodes.IsEmpty(); 11 | } -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Private/CustomNodeData.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomNodeData.h" 2 | 3 | -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Private/MyCustomPlugin.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "MyCustomPlugin.h" 4 | 5 | #define LOCTEXT_NAMESPACE "FMyCustomPluginModule" 6 | 7 | void FMyCustomPluginModule::StartupModule() 8 | { 9 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 10 | } 11 | 12 | void FMyCustomPluginModule::ShutdownModule() 13 | { 14 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 15 | // we call this function before unloading the module. 16 | } 17 | 18 | #undef LOCTEXT_NAMESPACE 19 | 20 | IMPLEMENT_MODULE(FMyCustomPluginModule, MyCustomPlugin) -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Public/CustomDataAsset.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Engine/DataAsset.h" 5 | #include "CustomNodeData.h" 6 | #include 7 | #include "CustomDataAsset.generated.h" 8 | 9 | UCLASS(BlueprintType) 10 | class MYCUSTOMPLUGIN_API UCustomDataAsset : public UObject 11 | { 12 | GENERATED_BODY() 13 | 14 | public: 15 | UCustomDataAsset() = default; 16 | 17 | public: 18 | UFUNCTION(BlueprintCallable, Category = "Custom Graph") 19 | const TArray& GetRootNodes() const; 20 | 21 | UFUNCTION(BlueprintCallable, Category = "Custom Graph") 22 | int GetLevelNum() const; 23 | 24 | UFUNCTION(BlueprintCallable, Category = "Custom Graph") 25 | const TArray GetNodesByLevel(int Level); 26 | 27 | UFUNCTION(BlueprintCallable, Category = "Custom Graph") 28 | void ClearGraph(); 29 | 30 | public: 31 | UPROPERTY(BlueprintReadOnly) 32 | TArray NodeTypeList; 33 | 34 | UPROPERTY(BlueprintReadOnly, Category = "Custom Graph") 35 | TArray RootNodes; 36 | 37 | UPROPERTY(BlueprintReadOnly, Category = "Custom Graph") 38 | TArray AllNodes; 39 | 40 | #if WITH_EDITORONLY_DATA 41 | UPROPERTY() 42 | class UEdGraph* EdGraph; 43 | #endif 44 | }; -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Public/CustomGraphNodeAsset.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "UObject/NoExportTypes.h" 5 | #include "CustomGraphNodeAsset.generated.h" 6 | 7 | UCLASS(BlueprintType, EditInlineNew) 8 | class MYCUSTOMPLUGIN_API UCustomGraphNodeAsset : public UObject 9 | { 10 | GENERATED_BODY() 11 | 12 | public: 13 | UCustomGraphNodeAsset(); 14 | 15 | UFUNCTION(BlueprintCallable, Category = "GenericGraphNode") 16 | bool IsLeafNode() const; 17 | public: 18 | 19 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly) 20 | FString Name; 21 | 22 | UPROPERTY(BlueprintReadOnly, Category = "Custom Node Asset") 23 | TArray ParentNodes; 24 | 25 | UPROPERTY(BlueprintReadOnly, Category = "Custom Node Asset") 26 | TArray ChildrenNodes; 27 | }; -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Public/CustomNodeData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "UObject/NoExportTypes.h" 5 | #include "CustomNodeData.generated.h" 6 | 7 | /* 8 | This class is used to list the unique nodes in a list viewer 9 | May change this to a UObject in order to change it's name in the Property-widget 10 | */ 11 | 12 | USTRUCT(Blueprintable) 13 | struct FCustomNodeData 14 | { 15 | GENERATED_USTRUCT_BODY() 16 | 17 | public: 18 | FCustomNodeData() = default; 19 | 20 | public: 21 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Custom Node") 22 | FString Name; 23 | }; -------------------------------------------------------------------------------- /Source/MyCustomPlugin/Public/MyCustomPlugin.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | class FMyCustomPluginModule : public IModuleInterface 9 | { 10 | public: 11 | 12 | /** IModuleInterface implementation */ 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | }; 16 | -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/MyCustomPluginEditor.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class MyCustomPluginEditor : ModuleRules 4 | { 5 | public MyCustomPluginEditor(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 8 | bLegacyPublicIncludePaths = false; 9 | ShadowVariableWarningLevel = WarningLevel.Error; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | // ... add public include paths required here ... 14 | } 15 | ); 16 | 17 | PrivateIncludePaths.AddRange( 18 | new string[] { 19 | // ... add other private include paths required here ... 20 | "MyCustomPluginEditor/Private", 21 | "MyCustomPluginEditor/Public" 22 | } 23 | ); 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] 27 | { 28 | "Core", 29 | "CoreUObject", 30 | "Engine", 31 | "UnrealEd", 32 | // ... add other public dependencies that you statically link with here ... 33 | } 34 | ); 35 | 36 | PrivateDependencyModuleNames.AddRange( 37 | new string[] 38 | { 39 | "MyCustomPlugin", 40 | "AssetTools", 41 | "Slate", 42 | "InputCore", 43 | "SlateCore", 44 | "GraphEditor", 45 | "PropertyEditor", 46 | "EditorStyle", 47 | "Kismet", 48 | "KismetWidgets", 49 | "ApplicationCore", 50 | "ToolMenus", 51 | // ... add private dependencies that you statically link with here ... 52 | } 53 | ); 54 | 55 | DynamicallyLoadedModuleNames.AddRange( 56 | new string[] 57 | { 58 | // ... add any modules that your module loads dynamically here ... 59 | } 60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/AssetEditor/CustomConnectionDrawingPolicy.cpp: -------------------------------------------------------------------------------- 1 | #include "AssetEditor/CustomConnectionDrawingPolicy.h" 2 | 3 | FCustomConnectionDrawingPolicy::FCustomConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj) 4 | : FConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, ZoomFactor, InClippingRect, InDrawElements) 5 | , GraphObj(InGraphObj) 6 | { 7 | } 8 | 9 | void FCustomConnectionDrawingPolicy::DrawConnection(int32 LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) 10 | { 11 | // Draw a line/spline 12 | FConnectionDrawingPolicy::DrawConnection(WireLayerID, Start, End, Params); 13 | } 14 | 15 | void FCustomConnectionDrawingPolicy::DetermineWiringStyle(UEdGraphPin* OutputPin, UEdGraphPin* InputPin, /*inout*/ FConnectionParams& Params) 16 | { 17 | Params.AssociatedPin1 = OutputPin; 18 | Params.AssociatedPin2 = InputPin; 19 | Params.WireThickness = 1.5f; 20 | 21 | const bool bDeemphasizeUnhoveredPins = HoveredPins.Num() > 0; 22 | if (bDeemphasizeUnhoveredPins) 23 | { 24 | ApplyHoverDeemphasis(OutputPin, InputPin, /*inout*/ Params.WireThickness, /*inout*/ Params.WireColor); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/AssetEditor/CustomDragConnection.cpp: -------------------------------------------------------------------------------- 1 | #include "AssetEditor/CustomDragConnection.h" 2 | #include "AssetEditor/EdCustomGraphNode.h" 3 | #include "SGraphPanel.h" 4 | 5 | TSharedRef FCustomDragConnection::New(SGraphPanel* GraphPanel, TSharedPtr InCustomNodeData) 6 | { 7 | TSharedRef Operation = MakeShareable(new FCustomDragConnection(GraphPanel, InCustomNodeData.Get())); 8 | Operation->Construct(); 9 | 10 | return Operation; 11 | } 12 | 13 | FReply FCustomDragConnection::DroppedOnPanel(const TSharedRef& Panel, FVector2D ScreenPosition, FVector2D GraphPosition, UEdGraph& Graph) 14 | { 15 | GraphPanel->OnStopMakingConnection(); 16 | 17 | // Get the geometry of the graph panel 18 | const FGeometry& PanelGeometry = GraphPanel->GetCachedGeometry(); 19 | 20 | // Convert the screen position to local coordinates relative to the graph panel 21 | FVector2D LocalPosition = PanelGeometry.AbsoluteToLocal(ScreenPosition); 22 | 23 | // Convert the local position to panel coordinates within the graph panel 24 | FVector2D PanelPosition = LocalPosition / PanelGeometry.Scale; 25 | 26 | // Convert the panel coordinates to graph coordinates 27 | FVector2D GraphCoord = GraphPanel->PanelCoordToGraphCoord(PanelPosition); 28 | 29 | // Create a new node in the graph at the local position 30 | FGraphNodeCreator NodeCreator(Graph); 31 | UEdCustomGraphNode* NewNode = NodeCreator.CreateNode(); 32 | 33 | UCustomGraphNodeAsset* NewNodeAsset = NewObject(NewNode); 34 | NewNodeAsset->Name = CustomNodeData->Name; 35 | 36 | NewNode->SetFlags(RF_Transactional); 37 | NewNode->GraphNodeAsset = NewNodeAsset; 38 | NewNode->bCanRenameNode = true; 39 | NewNode->NodePosX = GraphCoord.X; 40 | NewNode->NodePosY = GraphCoord.Y; 41 | 42 | NodeCreator.Finalize(); 43 | 44 | return FReply::Handled().EndDragDrop(); 45 | } 46 | 47 | FCustomDragConnection::FCustomDragConnection(SGraphPanel* GraphPanelIn, FCustomNodeData* CustomNodeDataIn) 48 | : GraphPanel(GraphPanelIn), 49 | CustomNodeData(CustomNodeDataIn), 50 | DecoratorAdjust(FSlateApplication::Get().GetCursorSize()) { } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/AssetEditor/CustomPinDragDropConnection.cpp: -------------------------------------------------------------------------------- 1 | #include "AssetEditor/CustomPinDragDropConnection.h" 2 | 3 | #include "SGraphPanel.h" 4 | #include "AssetEditor/EdCustomGraphNode.h" 5 | 6 | TSharedRef FCustomPinDragDropConnection::New(const TSharedRef& GraphPanel, const FDraggedPinTable& DraggedPins) 7 | { 8 | TSharedRef Operation = MakeShareable(new FCustomPinDragDropConnection(GraphPanel, DraggedPins)); 9 | Operation->Construct(); 10 | 11 | return Operation; 12 | } 13 | 14 | void FCustomPinDragDropConnection::OnDrop(bool bDropWasHandled, const FPointerEvent& MouseEvent) 15 | { 16 | GraphPanel->OnStopMakingConnection(); 17 | 18 | Super::OnDrop(bDropWasHandled, MouseEvent); 19 | } 20 | 21 | void FCustomPinDragDropConnection::HoverTargetChanged() 22 | { 23 | TArray UniqueMessages; 24 | 25 | if (UEdGraphPin* TargetPinObj = GetHoveredPin()) 26 | { 27 | TArray ValidSourcePins; 28 | ValidateGraphPinList(/*out*/ ValidSourcePins); 29 | 30 | // Check the schema for connection responses 31 | for (UEdGraphPin* StartingPinObj : ValidSourcePins) 32 | { 33 | // The Graph object in which the pins reside. 34 | UEdGraph* GraphObj = StartingPinObj->GetOwningNode()->GetGraph(); 35 | 36 | // Determine what the schema thinks about the wiring action 37 | const FPinConnectionResponse Response = GraphObj->GetSchema()->CanCreateConnection(StartingPinObj, TargetPinObj); 38 | 39 | if (Response.Response == ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW) 40 | { 41 | TSharedPtr NodeWidget = TargetPinObj->GetOwningNode()->DEPRECATED_NodeWidget.Pin(); 42 | if (NodeWidget.IsValid()) 43 | { 44 | NodeWidget->NotifyDisallowedPinConnection(StartingPinObj, TargetPinObj); 45 | } 46 | } 47 | 48 | UniqueMessages.AddUnique(Response); 49 | } 50 | } 51 | else if (UEdCustomGraphNode* TargetNodeObj = Cast(GetHoveredNode())) 52 | { 53 | TArray ValidSourcePins; 54 | ValidateGraphPinList(/*out*/ ValidSourcePins); 55 | 56 | // Check the schema for connection responses 57 | for (UEdGraphPin* StartingPinObj : ValidSourcePins) 58 | { 59 | FPinConnectionResponse Response; 60 | FText ResponseText; 61 | 62 | const UEdGraphSchema* Schema = StartingPinObj->GetSchema(); 63 | UEdGraphPin* TargetPin = TargetNodeObj->GetInputPin(); 64 | 65 | if (Schema && TargetPin) 66 | { 67 | Response = Schema->CanCreateConnection(StartingPinObj, TargetPin); 68 | if (Response.Response == ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW) 69 | { 70 | TSharedPtr NodeWidget = TargetPin->GetOwningNode()->DEPRECATED_NodeWidget.Pin(); 71 | if (NodeWidget.IsValid()) 72 | { 73 | NodeWidget->NotifyDisallowedPinConnection(StartingPinObj, TargetPinObj); 74 | } 75 | } 76 | } 77 | else 78 | { 79 | #define LOCTEXT_NAMESPACE "AssetSchema_GenericGraph" 80 | Response = FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("PinError", "Not a valid UGenericGraphEdNode")); 81 | #undef LOCTEXT_NAMESPACE 82 | } 83 | 84 | UniqueMessages.AddUnique(Response); 85 | } 86 | } 87 | else if (UEdGraph* CurrentHoveredGraph = GetHoveredGraph()) 88 | { 89 | TArray ValidSourcePins; 90 | ValidateGraphPinList(/*out*/ ValidSourcePins); 91 | 92 | for (UEdGraphPin* StartingPinObj : ValidSourcePins) 93 | { 94 | // Let the schema describe the connection we might make 95 | FPinConnectionResponse Response = CurrentHoveredGraph->GetSchema()->CanCreateNewNodes(StartingPinObj); 96 | if (!Response.Message.IsEmpty()) 97 | { 98 | UniqueMessages.AddUnique(Response); 99 | } 100 | } 101 | } 102 | 103 | if (!UniqueMessages.IsEmpty()) 104 | { 105 | // Take the unique responses and create visual feedback for it 106 | TSharedRef FeedbackBox = SNew(SVerticalBox); 107 | for (auto ResponseIt = UniqueMessages.CreateConstIterator(); ResponseIt; ++ResponseIt) 108 | { 109 | // Determine the icon 110 | const FSlateBrush* StatusSymbol = NULL; 111 | 112 | switch (ResponseIt->Response) 113 | { 114 | case CONNECT_RESPONSE_MAKE: 115 | case CONNECT_RESPONSE_BREAK_OTHERS_A: 116 | case CONNECT_RESPONSE_BREAK_OTHERS_B: 117 | case CONNECT_RESPONSE_BREAK_OTHERS_AB: 118 | StatusSymbol = FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.OK")); 119 | break; 120 | 121 | case CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE: 122 | StatusSymbol = FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.ViaCast")); 123 | break; 124 | 125 | case CONNECT_RESPONSE_DISALLOW: 126 | default: 127 | StatusSymbol = FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error")); 128 | break; 129 | } 130 | 131 | // Add a new message row 132 | FeedbackBox->AddSlot() 133 | .AutoHeight() 134 | [ 135 | SNew(SHorizontalBox) 136 | + SHorizontalBox::Slot() 137 | .AutoWidth() 138 | .Padding(3.0f) 139 | .VAlign(VAlign_Center) 140 | [ 141 | SNew(SImage).Image(StatusSymbol) 142 | ] 143 | + SHorizontalBox::Slot() 144 | .AutoWidth() 145 | .VAlign(VAlign_Center) 146 | [ 147 | SNew(STextBlock).Text(ResponseIt->Message) 148 | ] 149 | ]; 150 | } 151 | 152 | SetFeedbackMessage(FeedbackBox); 153 | } 154 | } 155 | 156 | FReply FCustomPinDragDropConnection::DroppedOnPin(FVector2D ScreenPosition, FVector2D GraphPosition) 157 | { 158 | TArray ValidSourcePins; 159 | ValidateGraphPinList(/*out*/ ValidSourcePins); 160 | 161 | const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "GraphEd_CreateConnection", "Create Pin Link")); 162 | 163 | UEdGraphPin* PinB = GetHoveredPin(); 164 | bool bError = false; 165 | TSet NodeList; 166 | 167 | for (UEdGraphPin* PinA : ValidSourcePins) 168 | { 169 | if ((PinA != NULL) && (PinB != NULL)) 170 | { 171 | UEdGraph* MyGraphObj = PinA->GetOwningNode()->GetGraph(); 172 | 173 | if (MyGraphObj->GetSchema()->TryCreateConnection(PinA, PinB)) 174 | { 175 | if (!PinA->IsPendingKill()) 176 | { 177 | NodeList.Add(PinA->GetOwningNode()); 178 | } 179 | if (!PinB->IsPendingKill()) 180 | { 181 | NodeList.Add(PinB->GetOwningNode()); 182 | } 183 | } 184 | } 185 | else 186 | { 187 | bError = true; 188 | } 189 | } 190 | 191 | // Send all nodes that received a new pin connection a notification 192 | for (auto It = NodeList.CreateConstIterator(); It; ++It) 193 | { 194 | UEdGraphNode* Node = (*It); 195 | Node->NodeConnectionListChanged(); 196 | } 197 | 198 | if (bError) 199 | { 200 | return FReply::Unhandled(); 201 | } 202 | 203 | return FReply::Handled(); 204 | } 205 | 206 | FReply FCustomPinDragDropConnection::DroppedOnNode(FVector2D ScreenPosition, FVector2D GraphPosition) 207 | { 208 | bool bHandledPinDropOnNode = false; 209 | UEdGraphNode* NodeOver = GetHoveredNode(); 210 | 211 | if (NodeOver) 212 | { 213 | // Gather any source drag pins 214 | TArray ValidSourcePins; 215 | ValidateGraphPinList(/*out*/ ValidSourcePins); 216 | 217 | if (ValidSourcePins.Num()) 218 | { 219 | for (UEdGraphPin* SourcePin : ValidSourcePins) 220 | { 221 | // Check for pin drop support 222 | FText ResponseText; 223 | if (SourcePin->GetOwningNode() != NodeOver && SourcePin->GetSchema()->SupportsDropPinOnNode(NodeOver, SourcePin->PinType, SourcePin->Direction, ResponseText)) 224 | { 225 | bHandledPinDropOnNode = true; 226 | 227 | // Find which pin name to use and drop the pin on the node 228 | const FName PinName = SourcePin->PinFriendlyName.IsEmpty() ? SourcePin->PinName : *SourcePin->PinFriendlyName.ToString(); 229 | 230 | const FScopedTransaction Transaction((SourcePin->Direction == EGPD_Output) ? NSLOCTEXT("UnrealEd", "AddInParam", "Add In Parameter") : NSLOCTEXT("UnrealEd", "AddOutParam", "Add Out Parameter")); 231 | 232 | UEdGraphPin* EdGraphPin = NodeOver->GetSchema()->DropPinOnNode(GetHoveredNode(), PinName, SourcePin->PinType, SourcePin->Direction); 233 | 234 | // This can invalidate the source pin due to node reconstruction, abort in that case 235 | if (SourcePin->GetOwningNodeUnchecked() && EdGraphPin) 236 | { 237 | SourcePin->Modify(); 238 | EdGraphPin->Modify(); 239 | SourcePin->GetSchema()->TryCreateConnection(SourcePin, EdGraphPin); 240 | } 241 | } 242 | 243 | // If we have not handled the pin drop on node and there is an error message, do not let other actions occur. 244 | if (!bHandledPinDropOnNode && !ResponseText.IsEmpty()) 245 | { 246 | bHandledPinDropOnNode = true; 247 | } 248 | } 249 | } 250 | } 251 | 252 | return bHandledPinDropOnNode ? FReply::Handled() : FReply::Unhandled(); 253 | } 254 | 255 | void FCustomPinDragDropConnection::OnDragged(const class FDragDropEvent& DragDropEvent) 256 | { 257 | FVector2D TargetPosition = DragDropEvent.GetScreenSpacePosition(); 258 | 259 | // Reposition the info window wrt to the drag 260 | CursorDecoratorWindow->MoveWindowTo(DragDropEvent.GetScreenSpacePosition() + DecoratorAdjust); 261 | // Request the active panel to scroll if required 262 | GraphPanel->RequestDeferredPan(TargetPosition); 263 | } 264 | 265 | void FCustomPinDragDropConnection::ValidateGraphPinList(TArray& OutValidPins) 266 | { 267 | OutValidPins.Empty(DraggingPins.Num()); 268 | for (const FGraphPinHandle& PinHandle : DraggingPins) 269 | { 270 | if (UEdGraphPin* GraphPin = PinHandle.GetPinObj(*GraphPanel)) 271 | { 272 | OutValidPins.Add(GraphPin); 273 | } 274 | } 275 | } 276 | 277 | FCustomPinDragDropConnection::FCustomPinDragDropConnection(const TSharedRef& GraphPanelIn, const FDraggedPinTable& DraggedPinsIn) 278 | : GraphPanel(GraphPanelIn) 279 | , DraggingPins(DraggedPinsIn) 280 | , DecoratorAdjust(FSlateApplication::Get().GetCursorSize()) 281 | { 282 | if (DraggingPins.Num() > 0) 283 | { 284 | const UEdGraphPin* PinObj = FDraggedPinTable::TConstIterator(DraggedPinsIn)->GetPinObj(*GraphPanelIn); 285 | 286 | if (PinObj && PinObj->Direction == EGPD_Input) 287 | { 288 | DecoratorAdjust *= FVector2D(-1.0f, 1.0f); 289 | } 290 | } 291 | 292 | for (const FGraphPinHandle& DraggedPin : DraggedPinsIn) 293 | { 294 | GraphPanelIn->OnBeginMakingConnection(DraggedPin); 295 | } 296 | } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/AssetEditor/EdCustomGraph.cpp: -------------------------------------------------------------------------------- 1 | #include "AssetEditor/EdCustomGraph.h" 2 | #include "CustomDataAsset.h" 3 | 4 | UCustomDataAsset* UEdCustomGraph::GetCustomData() const 5 | { 6 | return CastChecked(GetOuter()); 7 | } 8 | 9 | bool UEdCustomGraph::Modify(bool bAlwaysMarkDirty /*= true*/) 10 | { 11 | bool Rtn = Super::Modify(bAlwaysMarkDirty); 12 | 13 | GetCustomData()->Modify(); 14 | 15 | for (int32 i = 0; i < Nodes.Num(); ++i) 16 | { 17 | Nodes[i]->Modify(); 18 | } 19 | 20 | return Rtn; 21 | } 22 | 23 | 24 | void UEdCustomGraph::PostEditUndo() 25 | { 26 | Super::PostEditUndo(); 27 | 28 | NotifyGraphChanged(); 29 | } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/AssetEditor/EdCustomGraphNode.cpp: -------------------------------------------------------------------------------- 1 | #include "Kismet2/Kismet2NameValidators.h" 2 | #include "Kismet2/BlueprintEditorUtils.h" 3 | #include "AssetEditor/EdCustomGraphNode.h" 4 | 5 | #define LOCTEXT_NAMESPACE "EdNode_CustomGraphNode" 6 | 7 | UEdCustomGraphNode::UEdCustomGraphNode() 8 | { 9 | bCanRenameNode = true; 10 | } 11 | 12 | UEdCustomGraphNode::~UEdCustomGraphNode() 13 | { 14 | 15 | } 16 | 17 | void UEdCustomGraphNode::AllocateDefaultPins() 18 | { 19 | CreatePin(EGPD_Input, "MultipleNodes", FName(), TEXT("In")); 20 | CreatePin(EGPD_Output, "MultipleNodes", FName(), TEXT("Out")); 21 | } 22 | 23 | FText UEdCustomGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const 24 | { 25 | return FText::FromString(GraphNodeAsset->Name); 26 | } 27 | 28 | FText UEdCustomGraphNode::GetTooltipText() const 29 | { 30 | return FText::FromString(GraphNodeAsset->Name); 31 | } 32 | 33 | FLinearColor UEdCustomGraphNode::GetBackgroundColor() const 34 | { 35 | return FLinearColor::Black; 36 | } 37 | 38 | FLinearColor UEdCustomGraphNode::GetBorderColor() const 39 | { 40 | return FLinearColor(.83f, .83f, .83f); 41 | } 42 | 43 | UEdGraphPin* UEdCustomGraphNode::GetInputPin() const 44 | { 45 | return Pins[0]; 46 | } 47 | 48 | UEdGraphPin* UEdCustomGraphNode::GetOutputPin() const 49 | { 50 | return Pins[1]; 51 | } 52 | 53 | void UEdCustomGraphNode::AutowireNewNode(UEdGraphPin* FromPin) 54 | { 55 | Super::AutowireNewNode(FromPin); 56 | 57 | if (FromPin != nullptr) 58 | { 59 | if (GetSchema()->TryCreateConnection(FromPin, GetInputPin())) 60 | { 61 | FromPin->GetOwningNode()->NodeConnectionListChanged(); 62 | } 63 | } 64 | } 65 | 66 | 67 | #undef LOCTEXT_NAMESPACE -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/AssetEditor/EdCustomGraphSchema.cpp: -------------------------------------------------------------------------------- 1 | #include "AssetEditor/EdCustomGraphSchema.h" 2 | #include "AssetEditor/EdCustomGraphNode.h" 3 | #include "AssetEditor/CustomConnectionDrawingPolicy.h" 4 | 5 | #define LOCTEXT_NAMESPACE "CustomGraphSchema" 6 | 7 | class FNodeVisitorCycleChecker 8 | { 9 | public: 10 | /** Check whether a loop in the graph would be caused by linking the passed-in nodes */ 11 | bool CheckForLoop(UEdGraphNode* StartNode, UEdGraphNode* EndNode) 12 | { 13 | 14 | VisitedNodes.Add(StartNode); 15 | 16 | return TraverseNodes(EndNode); 17 | } 18 | 19 | private: 20 | bool TraverseNodes(UEdGraphNode* Node) 21 | { 22 | VisitedNodes.Add(Node); 23 | 24 | for (auto MyPin : Node->Pins) 25 | { 26 | if (MyPin->Direction == EGPD_Output) 27 | { 28 | for (auto OtherPin : MyPin->LinkedTo) 29 | { 30 | UEdGraphNode* OtherNode = OtherPin->GetOwningNode(); 31 | if (VisitedNodes.Contains(OtherNode)) 32 | { 33 | // Only an issue if this is a back-edge 34 | return false; 35 | } 36 | else if (!FinishedNodes.Contains(OtherNode)) 37 | { 38 | // Only should traverse if this node hasn't been traversed 39 | if (!TraverseNodes(OtherNode)) 40 | return false; 41 | } 42 | } 43 | } 44 | } 45 | 46 | VisitedNodes.Remove(Node); 47 | FinishedNodes.Add(Node); 48 | return true; 49 | }; 50 | 51 | 52 | TSet VisitedNodes; 53 | TSet FinishedNodes; 54 | }; 55 | 56 | bool UEdCustomGraphSchema::TryCreateConnection(UEdGraphPin* A, UEdGraphPin* B) const 57 | { 58 | // We don't actually care about the pin, we want the node that is being dragged between 59 | UEdCustomGraphNode* NodeA = Cast(A->GetOwningNode()); 60 | UEdCustomGraphNode* NodeB = Cast(B->GetOwningNode()); 61 | 62 | // Check that this edge doesn't already exist 63 | for (UEdGraphPin* TestPin : NodeA->GetOutputPin()->LinkedTo) 64 | { 65 | UEdGraphNode* ChildNode = TestPin->GetOwningNode(); 66 | if (UEdCustomGraphNode* EdNode_Edge = Cast(ChildNode)) 67 | { 68 | if (!EdNode_Edge->GetOutputPin()->LinkedTo.IsEmpty()) 69 | { 70 | ChildNode = Cast(EdNode_Edge->GetOutputPin()->LinkedTo[0]->GetOwningNode()); 71 | } 72 | } 73 | 74 | if (ChildNode == NodeB) 75 | return false; 76 | } 77 | 78 | if (NodeA && NodeB) 79 | { 80 | Super::TryCreateConnection(NodeA->GetOutputPin(), NodeB->GetInputPin()); 81 | return true; 82 | } 83 | 84 | return false; 85 | } 86 | 87 | const FPinConnectionResponse UEdCustomGraphSchema::CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const 88 | { 89 | // Make sure the pins are not on the same node 90 | if (A->GetOwningNode() == B->GetOwningNode()) 91 | { 92 | return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("PinErrorSameNode", "Can't connect node to itself")); 93 | } 94 | 95 | const UEdGraphPin* Out = A; 96 | const UEdGraphPin* In = B; 97 | 98 | UEdCustomGraphNode* EdNode_Out = Cast(Out->GetOwningNode()); 99 | UEdCustomGraphNode* EdNode_In = Cast(In->GetOwningNode()); 100 | 101 | if (EdNode_Out == nullptr || EdNode_In == nullptr) 102 | { 103 | return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("PinError", "Not a valid UGenericGraphEdNode")); 104 | } 105 | 106 | //Determine if we can have cycles or not 107 | bool bAllowCycles = false; 108 | 109 | // check for cycles 110 | FNodeVisitorCycleChecker CycleChecker; 111 | if (!bAllowCycles && !CycleChecker.CheckForLoop(Out->GetOwningNode(), In->GetOwningNode())) 112 | { 113 | return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, LOCTEXT("PinErrorCycle", "Can't create a graph cycle")); 114 | } 115 | 116 | return FPinConnectionResponse(CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE, LOCTEXT("PinConnect", "Connect nodes with edge")); 117 | } 118 | 119 | void UEdCustomGraphSchema::BreakNodeLinks(UEdGraphNode& TargetNode) const 120 | { 121 | const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "GraphEd_BreakNodeLinks", "Break Node Links")); 122 | 123 | UEdGraphSchema::BreakNodeLinks(TargetNode); 124 | } 125 | 126 | void UEdCustomGraphSchema::BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotifcation) const 127 | { 128 | const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "GraphEd_BreakPinLinks", "Break Pin Links")); 129 | 130 | UEdGraphSchema::BreakPinLinks(TargetPin, bSendsNodeNotifcation); 131 | } 132 | 133 | void UEdCustomGraphSchema::BreakSinglePinLink(UEdGraphPin* SourcePin, UEdGraphPin* TargetPin) const 134 | { 135 | const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "GraphEd_BreakSinglePinLink", "Break Pin Link")); 136 | 137 | UEdGraphSchema::BreakSinglePinLink(SourcePin, TargetPin); 138 | } 139 | 140 | bool UEdCustomGraphSchema::SupportsDropPinOnNode(UEdGraphNode* InTargetNode, const FEdGraphPinType& InSourcePinType, EEdGraphPinDirection InSourcePinDirection, FText& OutErrorMessage) const 141 | { 142 | return Cast(InTargetNode) != nullptr; 143 | } 144 | 145 | UEdGraphPin* UEdCustomGraphSchema::DropPinOnNode(UEdGraphNode* InTargetNode, const FName& InSourcePinName, const FEdGraphPinType& InSourcePinType, EEdGraphPinDirection InSourcePinDirection) const 146 | { 147 | UEdCustomGraphNode* EdNode = Cast(InTargetNode); 148 | switch (InSourcePinDirection) 149 | { 150 | case EGPD_Input: 151 | return EdNode->GetOutputPin(); 152 | case EGPD_Output: 153 | return EdNode->GetInputPin(); 154 | default: 155 | return nullptr; 156 | } 157 | } 158 | 159 | bool UEdCustomGraphSchema::CreateAutomaticConversionNodeAndConnections(UEdGraphPin* A, UEdGraphPin* B) const 160 | { 161 | UEdCustomGraphNode* NodeA = Cast(A->GetOwningNode()); 162 | UEdCustomGraphNode* NodeB = Cast(B->GetOwningNode()); 163 | 164 | // Are nodes and pins all valid? 165 | if (!NodeA || !NodeA->GetOutputPin() || !NodeB || !NodeB->GetInputPin()) 166 | return false; 167 | 168 | const auto& PinA = NodeA->GetOutputPin(); 169 | const auto& PinB = NodeB->GetInputPin(); 170 | 171 | // Create a connection from PinA to PinB 172 | PinA->Modify(); 173 | PinB->Modify(); 174 | PinA->MakeLinkTo(PinB); 175 | 176 | // Notify the nodes about the connection change 177 | NodeA->PinConnectionListChanged(PinA); 178 | NodeB->PinConnectionListChanged(PinB); 179 | 180 | return true; 181 | } 182 | 183 | class FConnectionDrawingPolicy* UEdCustomGraphSchema::CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const 184 | { 185 | return new FCustomConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, InZoomFactor, InClippingRect, InDrawElements, InGraphObj); 186 | } 187 | 188 | #undef LOCTEXT_NAMESPACE 189 | -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/AssetEditor/SCustomGraphNode.cpp: -------------------------------------------------------------------------------- 1 | #include "AssetEditor/SCustomGraphNode.h" 2 | #include "AssetEditor/EdCustomGraphNode.h" 3 | #include "SGraphNode.h" 4 | #include "Widgets/Text/SInlineEditableTextBlock.h" 5 | #include "SGraphPin.h" 6 | #include "AssetEditor/CustomPinDragDropConnection.h" 7 | 8 | #define LOCTEXT_NAMESPACE "EdNode_GenericGraph" 9 | 10 | class SGenericGraphPin : public SGraphPin 11 | { 12 | public: 13 | SLATE_BEGIN_ARGS(SGenericGraphPin) {} 14 | SLATE_END_ARGS() 15 | 16 | void Construct(const FArguments& InArgs, UEdGraphPin* InPin) 17 | { 18 | this->SetCursor(EMouseCursor::Default); 19 | 20 | bShowLabel = true; 21 | 22 | GraphPinObj = InPin; 23 | check(GraphPinObj != nullptr); 24 | 25 | const UEdGraphSchema* Schema = GraphPinObj->GetSchema(); 26 | check(Schema); 27 | 28 | SBorder::Construct(SBorder::FArguments() 29 | .BorderImage(this, &SGenericGraphPin::GetPinBorder) 30 | .BorderBackgroundColor(this, &SGenericGraphPin::GetPinColor) 31 | .OnMouseButtonDown(this, &SGenericGraphPin::OnPinMouseDown) 32 | .Cursor(this, &SGenericGraphPin::GetPinCursor) 33 | .Padding(FMargin(5.0f)) 34 | ); 35 | } 36 | 37 | protected: 38 | virtual FSlateColor GetPinColor() const override 39 | { 40 | return FLinearColor::White; 41 | } 42 | 43 | virtual TSharedRef GetDefaultValueWidget() override 44 | { 45 | return SNew(STextBlock); 46 | } 47 | 48 | const FSlateBrush* GetPinBorder() const 49 | { 50 | return FAppStyle::GetBrush(TEXT("Graph.StateNode.Body")); 51 | } 52 | 53 | 54 | virtual TSharedRef SpawnPinDragEvent(const TSharedRef& InGraphPanel, const TArray< TSharedRef >& InStartingPins) override 55 | { 56 | FCustomPinDragDropConnection::FDraggedPinTable PinHandles; 57 | PinHandles.Reserve(InStartingPins.Num()); 58 | 59 | for (const TSharedRef& PinWidget : InStartingPins) 60 | { 61 | PinHandles.Add(PinWidget->GetPinObj()); 62 | } 63 | 64 | return FCustomPinDragDropConnection::New(InGraphPanel, PinHandles); 65 | } 66 | }; 67 | 68 | 69 | void SCustomGraphNode::Construct(const FArguments& InArgs, UEdCustomGraphNode* InNode) 70 | { 71 | GraphNode = InNode; 72 | Node = InNode; 73 | UpdateGraphNode(); 74 | InNode->SEdNode = this; 75 | } 76 | 77 | void SCustomGraphNode::UpdateGraphNode() 78 | { 79 | TSharedPtr NodeBody; 80 | TSharedPtr NodeTitle = SNew(SNodeTitle, Node); 81 | 82 | auto Derp = NodeTitle->GetHeadTitle(); 83 | 84 | this->ContentScale.Bind(this, &SGraphNode::GetContentScale); 85 | 86 | this->GetOrAddSlot(ENodeZone::Center) 87 | .HAlign(HAlign_Fill) 88 | .VAlign(VAlign_Center) 89 | [ 90 | SNew(SBorder) 91 | .BorderImage(FAppStyle::GetBrush("Graph.StateNode.Body")) 92 | .Padding(0.0f) 93 | .BorderBackgroundColor(this, &SCustomGraphNode::GetBorderBackgroundColor) 94 | [ 95 | SNew(SOverlay) 96 | 97 | + SOverlay::Slot() 98 | .HAlign(HAlign_Fill) 99 | .VAlign(VAlign_Fill) 100 | [ 101 | SNew(SHorizontalBox) 102 | 103 | // Input Pin Area 104 | + SHorizontalBox::Slot() 105 | .FillWidth(1) 106 | [ 107 | SAssignNew(LeftNodeBox, SVerticalBox) 108 | ] 109 | 110 | // Output Pin Area 111 | + SHorizontalBox::Slot() 112 | .FillWidth(1) 113 | [ 114 | SAssignNew(RightNodeBox, SVerticalBox) 115 | ] 116 | ] 117 | 118 | + SOverlay::Slot() 119 | .HAlign(HAlign_Center) 120 | .VAlign(VAlign_Center) 121 | .Padding(FMargin(1.f)) 122 | [ 123 | SNew(SBorder) 124 | .BorderImage(FAppStyle::GetBrush("Graph.StateNode.ColorSpill")) 125 | .BorderBackgroundColor(FLinearColor(0.6f, 0.6f, 0.6f)) 126 | .HAlign(HAlign_Center) 127 | .VAlign(VAlign_Center) 128 | .Visibility(EVisibility::SelfHitTestInvisible) 129 | .Padding(FMargin(8.f)) 130 | [ 131 | SAssignNew(NodeBody, SVerticalBox) 132 | 133 | // Title 134 | + SVerticalBox::Slot() 135 | .AutoHeight() 136 | [ 137 | SNew(SHorizontalBox) 138 | 139 | // Node Title 140 | + SHorizontalBox::Slot() 141 | .Padding(FMargin(4.0f, 0.0f, 4.0f, 0.f)) 142 | [ 143 | SNew(SVerticalBox) 144 | + SVerticalBox::Slot() 145 | .AutoHeight() 146 | [ 147 | SAssignNew(InlineEditableText, SInlineEditableTextBlock) 148 | .Style(FAppStyle::Get(), "Graph.StateNode.NodeTitleInlineEditableText") 149 | .Text(NodeTitle.Get(), &SNodeTitle::GetHeadTitle) 150 | .IsSelected(this, &SCustomGraphNode::IsSelectedExclusively) 151 | .IsReadOnly(true) 152 | ] 153 | + SVerticalBox::Slot() 154 | .AutoHeight() 155 | [ 156 | NodeTitle.ToSharedRef() 157 | ] 158 | ] 159 | ] 160 | ] 161 | ] 162 | ] 163 | ]; 164 | 165 | CreatePinWidgets(); 166 | } 167 | 168 | void SCustomGraphNode::CreatePinWidgets() 169 | { 170 | UEdCustomGraphNode* StateNode = CastChecked(GraphNode); 171 | 172 | for (int32 PinIdx = 0; PinIdx < StateNode->Pins.Num(); PinIdx++) 173 | { 174 | UEdGraphPin* MyPin = StateNode->Pins[PinIdx]; 175 | if (!MyPin->bHidden) 176 | { 177 | TSharedPtr NewPin = SNew(SGenericGraphPin, MyPin); 178 | 179 | AddPin(NewPin.ToSharedRef()); 180 | } 181 | } 182 | } 183 | 184 | void SCustomGraphNode::AddPin(const TSharedRef& PinToAdd) 185 | { 186 | PinToAdd->SetOwner(SharedThis(this)); 187 | 188 | const UEdGraphPin* PinObj = PinToAdd->GetPinObj(); 189 | const bool bAdvancedParameter = PinObj && PinObj->bAdvancedView; 190 | if (bAdvancedParameter) 191 | { 192 | PinToAdd->SetVisibility( TAttribute(PinToAdd, &SGraphPin::IsPinVisibleAsAdvanced) ); 193 | } 194 | 195 | TSharedPtr PinBox; 196 | if (PinToAdd->GetDirection() == EEdGraphPinDirection::EGPD_Input) 197 | { 198 | PinBox = LeftNodeBox; 199 | InputPins.Add(PinToAdd); 200 | } 201 | else // Direction == EEdGraphPinDirection::EGPD_Output 202 | { 203 | PinBox = RightNodeBox; 204 | OutputPins.Add(PinToAdd); 205 | } 206 | 207 | if (PinBox) 208 | { 209 | PinBox->AddSlot() 210 | .HAlign(HAlign_Fill) 211 | .VAlign(VAlign_Fill) 212 | .FillHeight(1.0f) 213 | [ 214 | PinToAdd 215 | ]; 216 | } 217 | } 218 | 219 | FSlateColor SCustomGraphNode::GetBorderBackgroundColor() const 220 | { 221 | UEdCustomGraphNode* MyNode = CastChecked(GraphNode); 222 | return MyNode ? MyNode->GetBorderColor() : FLinearColor(1.f, 1.f, 0.0f); 223 | } 224 | 225 | const FSlateBrush* SCustomGraphNode::GetNameIcon() const 226 | { 227 | return FAppStyle::GetBrush(TEXT("BTEditor.Graph.BTNode.Icon")); 228 | } 229 | 230 | 231 | #undef LOCTEXT_NAMESPACE -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/CustomDataAssetActions.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomDataAssetActions.h" 2 | #include "CustomDataAsset.h" 3 | #include "CustomDataAssetEditor.h" 4 | 5 | FText FCustomDataAssetActions::GetName() const 6 | { 7 | return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_CustomDataAsset", "Custom Data Asset"); 8 | } 9 | 10 | FColor FCustomDataAssetActions::GetTypeColor() const 11 | { 12 | return FColor::Green; 13 | } 14 | 15 | UClass* FCustomDataAssetActions::GetSupportedClass() const 16 | { 17 | return UCustomDataAsset::StaticClass(); 18 | } 19 | 20 | uint32 FCustomDataAssetActions::GetCategories() 21 | { 22 | return AssetCategory; 23 | } 24 | 25 | void FCustomDataAssetActions::OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) 26 | { 27 | const EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone; 28 | 29 | for (auto ObjIt = InObjects.CreateConstIterator(); ObjIt; ++ObjIt) 30 | { 31 | if (UCustomDataAsset* DataAsset = Cast(*ObjIt)) 32 | { 33 | TSharedRef NewGraphEditor(new FCustomDataAssetEditor()); 34 | NewGraphEditor->InitCustomDataAssetEditor(Mode, EditWithinLevelEditor, DataAsset); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/CustomDataAssetEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomDataAssetEditor.h" 2 | #include "Kismet2/BlueprintEditorUtils.h" 3 | #include "CustomDataAsset.h" 4 | #include "Framework/Commands/GenericCommands.h" 5 | #include "EdGraphUtilities.h" 6 | #include "HAL/PlatformApplicationMisc.h" 7 | #include "AssetEditor/EdCustomGraphNode.h" 8 | #include "AssetEditor/EdCustomGraphSchema.h" 9 | #include 10 | 11 | #define LOCTEXT_NAMESPACE "CustomDataAssetEditor" 12 | 13 | const FName CustomDataAssetEditorAppName = FName(TEXT("CustomDataAssetEditorApp")); 14 | 15 | struct FCustomDataAssetAssetEditorTabs 16 | { 17 | static const FName GraphViewId; 18 | static const FName NodeListID; 19 | static const FName CustomDataAssetPropertyID; 20 | }; 21 | 22 | // Tab IDs 23 | const FName FCustomDataAssetAssetEditorTabs::GraphViewId(TEXT("RuleList")); 24 | const FName FCustomDataAssetAssetEditorTabs::NodeListID(TEXT("NodeList")); 25 | const FName FCustomDataAssetAssetEditorTabs::CustomDataAssetPropertyID(TEXT("CustomDataAssetPropertyID")); 26 | 27 | void FCustomDataAssetEditor::RegisterTabSpawners(const TSharedRef& InTabManager) 28 | { 29 | WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_CustomDataAssetEditor", "Custom Data Asset Editor")); 30 | auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef(); 31 | 32 | FAssetEditorToolkit::RegisterTabSpawners(InTabManager); 33 | 34 | InTabManager->RegisterTabSpawner(FCustomDataAssetAssetEditorTabs::GraphViewId, FOnSpawnTab::CreateSP(this, &FCustomDataAssetEditor::SpawnTab_RuleList)) 35 | .SetDisplayName(LOCTEXT("DetailsTab", "Graph")) 36 | .SetGroup(WorkspaceMenuCategoryRef) 37 | .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")); 38 | 39 | InTabManager->RegisterTabSpawner(FCustomDataAssetAssetEditorTabs::NodeListID, FOnSpawnTab::CreateSP(this, &FCustomDataAssetEditor::SpawnTab_NodeList)) 40 | .SetDisplayName(LOCTEXT("NodeListTab", "Node List")) 41 | .SetGroup(WorkspaceMenuCategoryRef) 42 | .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")); 43 | 44 | InTabManager->RegisterTabSpawner(FCustomDataAssetAssetEditorTabs::CustomDataAssetPropertyID, FOnSpawnTab::CreateSP(this, &FCustomDataAssetEditor::SpawnTab_Details)) 45 | .SetDisplayName(LOCTEXT("DetailsTab", "Property")) 46 | .SetGroup(WorkspaceMenuCategoryRef) 47 | .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")); 48 | 49 | } 50 | 51 | void FCustomDataAssetEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) 52 | { 53 | FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); 54 | 55 | InTabManager->UnregisterTabSpawner(FCustomDataAssetAssetEditorTabs::GraphViewId); 56 | InTabManager->UnregisterTabSpawner(FCustomDataAssetAssetEditorTabs::NodeListID); 57 | InTabManager->UnregisterTabSpawner(FCustomDataAssetAssetEditorTabs::CustomDataAssetPropertyID); 58 | } 59 | 60 | void FCustomDataAssetEditor::InitCustomDataAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UCustomDataAsset* InCustomDataAsset) 61 | { 62 | CustomDataAsset = InCustomDataAsset; 63 | CreateEdGraph(); 64 | 65 | FGenericCommands::Register(); 66 | 67 | CreateInternalWidgets(); 68 | 69 | // Layout of the tabs in the editor window 70 | const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("Standalone_CustomDataAssetEditor_Layout_v1") 71 | ->AddArea 72 | ( 73 | FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical) 74 | #if ENGINE_MAJOR_VERSION < 5 75 | ->Split 76 | ( 77 | FTabManager::NewStack() 78 | ->SetSizeCoefficient(0.1f) 79 | ->AddTab(GetToolbarTabId(), ETabState::OpenedTab)->SetHideTabWell(true) 80 | ) 81 | #endif // #if ENGINE_MAJOR_VERSION < 5 82 | ->Split 83 | ( 84 | FTabManager::NewSplitter()->SetOrientation(Orient_Horizontal)->SetSizeCoefficient(0.9f) 85 | ->Split 86 | ( 87 | FTabManager::NewSplitter()->SetOrientation(Orient_Vertical) 88 | ->Split( 89 | FTabManager::NewStack() 90 | ->SetSizeCoefficient(0.5f) 91 | ->AddTab(FCustomDataAssetAssetEditorTabs::GraphViewId, ETabState::OpenedTab)->SetHideTabWell(true) 92 | ) 93 | ->Split( 94 | FTabManager::NewStack() 95 | ->SetSizeCoefficient(0.5f) 96 | ->AddTab(FCustomDataAssetAssetEditorTabs::NodeListID, ETabState::OpenedTab) 97 | ) 98 | ) 99 | ->Split 100 | ( 101 | FTabManager::NewSplitter()->SetOrientation(Orient_Vertical) 102 | ->Split 103 | ( 104 | FTabManager::NewStack() 105 | ->SetSizeCoefficient(1.0f) 106 | ->AddTab(FCustomDataAssetAssetEditorTabs::CustomDataAssetPropertyID, ETabState::OpenedTab) 107 | ) 108 | ) 109 | ) 110 | ); 111 | 112 | const bool bCreateDefaultStandaloneMenu = true; 113 | const bool bCreateDefaultToolbar = true; 114 | FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, CustomDataAssetEditorAppName, StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, CustomDataAsset); 115 | } 116 | 117 | FName FCustomDataAssetEditor::GetToolkitFName() const 118 | { 119 | return FName("CustomDataAssetEditor"); 120 | } 121 | 122 | FText FCustomDataAssetEditor::GetBaseToolkitName() const 123 | { 124 | return LOCTEXT("CustomDataAssetEditorAppLabel", "Custom Data Asset Editor"); 125 | } 126 | 127 | FText FCustomDataAssetEditor::GetToolkitName() const 128 | { 129 | return FText::FromString(CustomDataAsset->GetName()); 130 | } 131 | 132 | FLinearColor FCustomDataAssetEditor::GetWorldCentricTabColorScale() const 133 | { 134 | return FLinearColor::White; 135 | } 136 | 137 | FString FCustomDataAssetEditor::GetWorldCentricTabPrefix() const 138 | { 139 | return TEXT("CustomDataAssetEditor"); 140 | } 141 | 142 | void FCustomDataAssetEditor::SaveAsset_Execute() 143 | { 144 | RebuildCustomData(); 145 | FAssetEditorToolkit::SaveAsset_Execute(); 146 | } 147 | 148 | TSharedRef FCustomDataAssetEditor::SpawnTab_RuleList(const FSpawnTabArgs& Args) 149 | { 150 | check(Args.GetTabId() == FCustomDataAssetAssetEditorTabs::GraphViewId); 151 | 152 | TSharedRef SpawnedTab = SNew(SDockTab) 153 | .Label(LOCTEXT("RuleListTab_Title", "RuleList")); 154 | 155 | if (CustomGraphWidget.IsValid()) 156 | { 157 | SpawnedTab->SetContent(CustomGraphWidget.ToSharedRef()); 158 | } 159 | 160 | return SpawnedTab; 161 | } 162 | 163 | void FCustomDataAssetEditor::OnSelectedNodesChanged(const TSet& NewSelection) 164 | { 165 | TArray Selection; 166 | 167 | for (UObject* SelectionEntry : NewSelection) 168 | { 169 | Selection.Add(SelectionEntry); 170 | } 171 | 172 | if (Selection.Num() == 0) 173 | { 174 | PropertyWidget->SetObject(CustomDataAsset); 175 | } 176 | else 177 | { 178 | PropertyWidget->SetObjects(Selection); 179 | } 180 | } 181 | 182 | TSharedRef FCustomDataAssetEditor::CreateGraphtWidget() 183 | { 184 | FGraphAppearanceInfo AppearanceInfo; 185 | AppearanceInfo.CornerText = LOCTEXT("AppearanceCornerText_CustomGraph", "Custom Graph"); 186 | 187 | CreateCommandList(); 188 | 189 | SGraphEditor::FGraphEditorEvents InEvents; 190 | InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FCustomDataAssetEditor::OnSelectedNodesChanged); 191 | 192 | return SNew(SGraphEditor) 193 | .AdditionalCommands(GraphEditorCommands) 194 | .IsEditable(true) 195 | .Appearance(AppearanceInfo) 196 | .GraphToEdit(CustomDataAsset->EdGraph) 197 | .GraphEvents(InEvents) 198 | .AutoExpandActionMenu(true) 199 | .ShowGraphStateOverlay(false); 200 | } 201 | 202 | TSharedRef FCustomDataAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args) 203 | { 204 | check(Args.GetTabId() == FCustomDataAssetAssetEditorTabs::CustomDataAssetPropertyID); 205 | 206 | return SNew(SDockTab) 207 | #if ENGINE_MAJOR_VERSION < 5 208 | .Icon(FAppStyle::GetBrush("LevelEditor.Tabs.Details")) 209 | #endif // #if ENGINE_MAJOR_VERSION < 5 210 | .Label(LOCTEXT("Details_Title", "Property")) 211 | [ 212 | PropertyWidget.ToSharedRef() 213 | ]; 214 | } 215 | 216 | TSharedRef FCustomDataAssetEditor::SpawnTab_NodeList(const FSpawnTabArgs& Args) 217 | { 218 | check(Args.GetTabId() == FCustomDataAssetAssetEditorTabs::NodeListID); 219 | 220 | return SNew(SDockTab) 221 | #if ENGINE_MAJOR_VERSION < 5 222 | .Icon(FAppStyle::GetBrush("LevelEditor.Tabs.Details")) 223 | #endif // #if ENGINE_MAJOR_VERSION < 5 224 | .Label(LOCTEXT("Details_Title", "Property")) 225 | [ 226 | CustomNodeEditorWidget.ToSharedRef() 227 | ]; 228 | } 229 | 230 | void FCustomDataAssetEditor::CreateInternalWidgets() 231 | { 232 | CustomGraphWidget = CreateGraphtWidget(); 233 | 234 | CustomNodeEditorWidget = SNew(SCustomNodeEditorWidget) 235 | .CustomDataAsset(CustomDataAsset) 236 | .CustomGraphWidget(CustomGraphWidget.Get()) 237 | .PropertyWidget(PropertyWidget.Get()); 238 | 239 | FDetailsViewArgs Args; 240 | Args.bHideSelectionTip = true; 241 | 242 | FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); 243 | PropertyWidget = PropertyModule.CreateDetailView(Args); 244 | PropertyWidget->SetObject(CustomDataAsset); 245 | 246 | EditorSettingsWidget = PropertyModule.CreateDetailView(Args); 247 | } 248 | 249 | void FCustomDataAssetEditor::CreateEdGraph() 250 | { 251 | if (CustomDataAsset && CustomDataAsset->EdGraph == nullptr) 252 | { 253 | CustomDataAsset->EdGraph = CastChecked(FBlueprintEditorUtils::CreateNewGraph(CustomDataAsset, NAME_None, UEdCustomGraph::StaticClass(), UEdCustomGraphSchema::StaticClass())); 254 | CustomDataAsset->EdGraph->bAllowDeletion = false; 255 | 256 | // Give the schema a chance to fill out any required nodes (like the results node) 257 | const UEdGraphSchema* Schema = CustomDataAsset->EdGraph->GetSchema(); 258 | Schema->CreateDefaultNodesForGraph(*CustomDataAsset->EdGraph); 259 | } 260 | } 261 | 262 | void FCustomDataAssetEditor::CreateCommandList() 263 | { 264 | if (GraphEditorCommands.IsValid()) 265 | { 266 | return; 267 | } 268 | 269 | GraphEditorCommands = MakeShareable(new FUICommandList); 270 | 271 | // Can't use CreateSP here because derived editor are already implementing TSharedFromThis 272 | // however it should be safe, since commands are being used only within this editor 273 | // if it ever crashes, this function will have to go away and be reimplemented in each derived class 274 | 275 | GraphEditorCommands->MapAction(FGenericCommands::Get().SelectAll, 276 | FExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::SelectAllNodes), 277 | FCanExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CanSelectAllNodes) 278 | ); 279 | 280 | GraphEditorCommands->MapAction(FGenericCommands::Get().Delete, 281 | FExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::DeleteSelectedNodes), 282 | FCanExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CanDeleteNodes) 283 | ); 284 | 285 | GraphEditorCommands->MapAction(FGenericCommands::Get().Copy, 286 | FExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CopySelectedNodes), 287 | FCanExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CanCopyNodes) 288 | ); 289 | 290 | GraphEditorCommands->MapAction(FGenericCommands::Get().Cut, 291 | FExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CutSelectedNodes), 292 | FCanExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CanCutNodes) 293 | ); 294 | 295 | GraphEditorCommands->MapAction(FGenericCommands::Get().Paste, 296 | FExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::PasteNodes), 297 | FCanExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CanPasteNodes) 298 | ); 299 | 300 | GraphEditorCommands->MapAction(FGenericCommands::Get().Duplicate, 301 | FExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::DuplicateNodes), 302 | FCanExecuteAction::CreateRaw(this, &FCustomDataAssetEditor::CanDuplicateNodes) 303 | ); 304 | } 305 | 306 | void FCustomDataAssetEditor::SelectAllNodes() 307 | { 308 | TSharedPtr CurrentGraphEditor = CustomGraphWidget; 309 | if (CurrentGraphEditor.IsValid()) 310 | { 311 | CurrentGraphEditor->SelectAllNodes(); 312 | } 313 | } 314 | 315 | bool FCustomDataAssetEditor::CanSelectAllNodes() 316 | { 317 | return true; 318 | } 319 | 320 | void FCustomDataAssetEditor::DeleteSelectedNodes() 321 | { 322 | TSharedPtr CurrentGraphEditor = CustomGraphWidget; 323 | if (!CurrentGraphEditor.IsValid()) 324 | { 325 | return; 326 | } 327 | 328 | const FScopedTransaction Transaction(FGenericCommands::Get().Delete->GetDescription()); 329 | 330 | CurrentGraphEditor->GetCurrentGraph()->Modify(); 331 | 332 | const FGraphPanelSelectionSet SelectedNodes = CurrentGraphEditor->GetSelectedNodes(); 333 | CurrentGraphEditor->ClearSelectionSet(); 334 | 335 | for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) 336 | { 337 | UEdCustomGraphNode* EdNode = Cast(*NodeIt); 338 | if (EdNode == nullptr || !EdNode->CanUserDeleteNode()) 339 | continue;; 340 | 341 | if (UEdCustomGraphNode* EdNode_Node = Cast(EdNode)) 342 | { 343 | EdNode_Node->Modify(); 344 | 345 | const UEdGraphSchema* Schema = EdNode_Node->GetSchema(); 346 | if (Schema != nullptr) 347 | { 348 | Schema->BreakNodeLinks(*EdNode_Node); 349 | } 350 | 351 | EdNode_Node->DestroyNode(); 352 | } 353 | else 354 | { 355 | EdNode->Modify(); 356 | EdNode->DestroyNode(); 357 | } 358 | } 359 | } 360 | 361 | bool FCustomDataAssetEditor::CanDeleteNodes() 362 | { 363 | // If any of the nodes can be deleted then we should allow deleting 364 | const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); 365 | for (FGraphPanelSelectionSet::TConstIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) 366 | { 367 | UEdCustomGraphNode* Node = Cast(*SelectedIter); 368 | if (Node != nullptr && Node->CanUserDeleteNode()) 369 | { 370 | return true; 371 | } 372 | } 373 | 374 | return false; 375 | } 376 | 377 | void FCustomDataAssetEditor::DeleteSelectedDuplicatableNodes() 378 | { 379 | TSharedPtr CurrentGraphEditor = CustomGraphWidget; 380 | if (!CurrentGraphEditor.IsValid()) 381 | { 382 | return; 383 | } 384 | 385 | const FGraphPanelSelectionSet OldSelectedNodes = CurrentGraphEditor->GetSelectedNodes(); 386 | CurrentGraphEditor->ClearSelectionSet(); 387 | 388 | for (FGraphPanelSelectionSet::TConstIterator SelectedIter(OldSelectedNodes); SelectedIter; ++SelectedIter) 389 | { 390 | UEdCustomGraphNode* Node = Cast(*SelectedIter); 391 | if (Node && Node->CanDuplicateNode()) 392 | { 393 | CurrentGraphEditor->SetNodeSelection(Node, true); 394 | } 395 | } 396 | 397 | // Delete the duplicatable nodes 398 | DeleteSelectedNodes(); 399 | 400 | CurrentGraphEditor->ClearSelectionSet(); 401 | 402 | for (FGraphPanelSelectionSet::TConstIterator SelectedIter(OldSelectedNodes); SelectedIter; ++SelectedIter) 403 | { 404 | if (UEdCustomGraphNode* Node = Cast(*SelectedIter)) 405 | { 406 | CurrentGraphEditor->SetNodeSelection(Node, true); 407 | } 408 | } 409 | } 410 | 411 | void FCustomDataAssetEditor::CutSelectedNodes() 412 | { 413 | CopySelectedNodes(); 414 | DeleteSelectedDuplicatableNodes(); 415 | } 416 | 417 | bool FCustomDataAssetEditor::CanCutNodes() 418 | { 419 | return CanCopyNodes() && CanDeleteNodes(); 420 | } 421 | 422 | void FCustomDataAssetEditor::CopySelectedNodes() 423 | { 424 | // Export the selected nodes and place the text on the clipboard 425 | FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); 426 | 427 | FString ExportedText; 428 | 429 | for (FGraphPanelSelectionSet::TIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) 430 | { 431 | UEdCustomGraphNode* Node = Cast(*SelectedIter); 432 | if (Node == nullptr) 433 | { 434 | SelectedIter.RemoveCurrent(); 435 | continue; 436 | } 437 | 438 | Node->PrepareForCopying(); 439 | } 440 | 441 | FEdGraphUtilities::ExportNodesToText(SelectedNodes, ExportedText); 442 | FPlatformApplicationMisc::ClipboardCopy(*ExportedText); 443 | } 444 | 445 | bool FCustomDataAssetEditor::CanCopyNodes() 446 | { 447 | // If any of the nodes can be duplicated then we should allow copying 448 | const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); 449 | for (FGraphPanelSelectionSet::TConstIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) 450 | { 451 | UEdCustomGraphNode* Node = Cast(*SelectedIter); 452 | if (Node && Node->CanDuplicateNode()) 453 | { 454 | return true; 455 | } 456 | } 457 | 458 | return false; 459 | } 460 | 461 | void FCustomDataAssetEditor::PasteNodes() 462 | { 463 | TSharedPtr CurrentGraphEditor = CustomGraphWidget; 464 | if (CurrentGraphEditor.IsValid()) 465 | { 466 | PasteNodesHere(CurrentGraphEditor->GetPasteLocation()); 467 | } 468 | } 469 | 470 | FGraphPanelSelectionSet FCustomDataAssetEditor::GetSelectedNodes() const 471 | { 472 | FGraphPanelSelectionSet CurrentSelection; 473 | TSharedPtr FocusedGraphEd = CustomGraphWidget; 474 | if (FocusedGraphEd.IsValid()) 475 | { 476 | CurrentSelection = FocusedGraphEd->GetSelectedNodes(); 477 | } 478 | 479 | return CurrentSelection; 480 | } 481 | 482 | void FCustomDataAssetEditor::PasteNodesHere(const FVector2D& Location) 483 | { 484 | // Find the graph editor with focus 485 | TSharedPtr CurrentGraphEditor = CustomGraphWidget; 486 | if (!CurrentGraphEditor.IsValid()) 487 | { 488 | return; 489 | } 490 | 491 | // Select the newly pasted stuff 492 | UEdGraph* EdGraph = CurrentGraphEditor->GetCurrentGraph(); 493 | { 494 | const FScopedTransaction Transaction(FGenericCommands::Get().Paste->GetDescription()); 495 | EdGraph->Modify(); 496 | 497 | // Clear the selection set (newly pasted stuff will be selected) 498 | CurrentGraphEditor->ClearSelectionSet(); 499 | 500 | // Grab the text to paste from the clipboard. 501 | FString TextToImport; 502 | FPlatformApplicationMisc::ClipboardPaste(TextToImport); 503 | 504 | // Import the nodes 505 | TSet PastedNodes; 506 | FEdGraphUtilities::ImportNodesFromText(EdGraph, TextToImport, PastedNodes); 507 | 508 | //Average position of nodes so we can move them while still maintaining relative distances to each other 509 | FVector2D AvgNodePosition(0.0f, 0.0f); 510 | 511 | for (TSet::TIterator It(PastedNodes); It; ++It) 512 | { 513 | UEdGraphNode* Node = *It; 514 | AvgNodePosition.X += Node->NodePosX; 515 | AvgNodePosition.Y += Node->NodePosY; 516 | } 517 | 518 | float InvNumNodes = 1.0f / float(PastedNodes.Num()); 519 | AvgNodePosition.X *= InvNumNodes; 520 | AvgNodePosition.Y *= InvNumNodes; 521 | 522 | for (TSet::TIterator It(PastedNodes); It; ++It) 523 | { 524 | UEdGraphNode* Node = *It; 525 | CurrentGraphEditor->SetNodeSelection(Node, true); 526 | 527 | Node->NodePosX = (Node->NodePosX - AvgNodePosition.X) + Location.X; 528 | Node->NodePosY = (Node->NodePosY - AvgNodePosition.Y) + Location.Y; 529 | 530 | Node->SnapToGrid(16); 531 | 532 | // Give new node a different Guid from the old one 533 | Node->CreateNewGuid(); 534 | } 535 | } 536 | 537 | // Update UI 538 | CurrentGraphEditor->NotifyGraphChanged(); 539 | 540 | UObject* GraphOwner = EdGraph->GetOuter(); 541 | if (GraphOwner) 542 | { 543 | GraphOwner->PostEditChange(); 544 | GraphOwner->MarkPackageDirty(); 545 | } 546 | } 547 | 548 | bool FCustomDataAssetEditor::CanPasteNodes() 549 | { 550 | TSharedPtr CurrentGraphEditor = CustomGraphWidget; 551 | if (!CurrentGraphEditor.IsValid()) 552 | { 553 | return false; 554 | } 555 | 556 | FString ClipboardContent; 557 | FPlatformApplicationMisc::ClipboardPaste(ClipboardContent); 558 | 559 | return FEdGraphUtilities::CanImportNodesFromText(CurrentGraphEditor->GetCurrentGraph(), ClipboardContent); 560 | } 561 | 562 | void FCustomDataAssetEditor::DuplicateNodes() 563 | { 564 | CopySelectedNodes(); 565 | PasteNodes(); 566 | } 567 | 568 | bool FCustomDataAssetEditor::CanDuplicateNodes() 569 | { 570 | return CanCopyNodes(); 571 | } 572 | 573 | void FCustomDataAssetEditor::RebuildCustomData() 574 | { 575 | if (CustomDataAsset == nullptr || CustomGraphWidget->GetCurrentGraph() == nullptr) 576 | { 577 | return; 578 | } 579 | 580 | CustomDataAsset->ClearGraph(); 581 | 582 | UEdGraph* Graph = CustomGraphWidget->GetCurrentGraph(); 583 | const TArray>& Nodes = Graph->Nodes; 584 | 585 | for (int Index = 0; Index < Nodes.Num(); Index++) 586 | { 587 | if (UEdCustomGraphNode* EdNode = Cast(Nodes[Index])) 588 | { 589 | UCustomGraphNodeAsset* GraphNodeAsset = EdNode->GraphNodeAsset; 590 | CustomDataAsset->AllNodes.Add(GraphNodeAsset); 591 | 592 | for (int PinIndex = 0; PinIndex < EdNode->Pins.Num(); ++PinIndex) 593 | { 594 | UEdGraphPin* Pin = EdNode->Pins[PinIndex]; 595 | 596 | if (Pin->Direction != EEdGraphPinDirection::EGPD_Output) 597 | { 598 | continue; 599 | } 600 | 601 | for (int LinkToIndex = 0; LinkToIndex < Pin->LinkedTo.Num(); ++LinkToIndex) 602 | { 603 | UCustomGraphNodeAsset* ChildNode = nullptr; 604 | if(UEdCustomGraphNode* EdNode_Child = Cast(Pin->LinkedTo[LinkToIndex]->GetOwningNode())) 605 | { 606 | ChildNode = EdNode_Child->GraphNodeAsset; 607 | } 608 | 609 | if (ChildNode != nullptr) 610 | { 611 | GraphNodeAsset->ChildrenNodes.Add(ChildNode); 612 | ChildNode->ParentNodes.Add(GraphNodeAsset); 613 | } 614 | } 615 | } 616 | } 617 | } 618 | 619 | for (int i = 0; i < CustomDataAsset->AllNodes.Num(); ++i) 620 | { 621 | UCustomGraphNodeAsset* Node = CustomDataAsset->AllNodes[i]; 622 | if (Node->ParentNodes.IsEmpty()) 623 | { 624 | CustomDataAsset->RootNodes.Add(Node); 625 | } 626 | } 627 | } 628 | 629 | #undef LOCTEXT_NAMESPACE -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/CustomDataAssetFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomDataAssetFactory.h" 2 | #include "CustomDataAsset.h" 3 | 4 | UCustomDataAssetFactory::UCustomDataAssetFactory() 5 | { 6 | // Assign the class that this factory will create 7 | SupportedClass = UCustomDataAsset::StaticClass(); 8 | bCreateNew = true; 9 | bEditAfterNew = true; 10 | } 11 | 12 | UObject* UCustomDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) 13 | { 14 | UCustomDataAsset* NewAsset = NewObject(InParent, InClass, InName, Flags); 15 | return NewAsset; 16 | } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/CustomNodeEditorWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomNodeEditorWidget.h" 2 | #include "AssetEditor/EdCustomGraphNode.h" 3 | #include "AssetEditor/CustomDragConnection.h" 4 | 5 | void SCustomNodeEditorWidget::Construct(const FArguments& InArgs) 6 | { 7 | CustomDataAsset = InArgs._CustomDataAsset; 8 | CustomGraphWidget = InArgs._CustomGraphWidget; 9 | PropertyWidget = InArgs._PropertyWidget; 10 | 11 | if (CustomDataAsset) 12 | { 13 | const TArray& AssetNodes = CustomDataAsset->NodeTypeList; 14 | Nodes.Empty(); 15 | 16 | for (const FCustomNodeData& Node : AssetNodes) 17 | { 18 | Nodes.Add(MakeShared(Node)); // Allocate new instances using MakeShared 19 | } 20 | } 21 | 22 | ChildSlot 23 | [ 24 | SNew(SVerticalBox) 25 | 26 | + SVerticalBox::Slot() 27 | [ 28 | SAssignNew(ListViewWidget, SListView>) 29 | .ListItemsSource(&Nodes) 30 | .OnGenerateRow(this, &SCustomNodeEditorWidget::GenerateRowWidget) 31 | .OnSelectionChanged(this, &SCustomNodeEditorWidget::OnSelectedNodesChanged) 32 | ] 33 | 34 | + SVerticalBox::Slot() 35 | .AutoHeight() 36 | [ 37 | SNew(SHorizontalBox) 38 | 39 | + SHorizontalBox::Slot() 40 | .FillWidth(1.f) 41 | [ 42 | SAssignNew(NodeNameTextBox, SEditableTextBox) 43 | .HintText(FText::FromString("Enter Node Name")) 44 | .OnTextCommitted(this, &SCustomNodeEditorWidget::HandleNodeNameCommitted) 45 | ] 46 | 47 | + SHorizontalBox::Slot() 48 | .AutoWidth() 49 | [ 50 | SNew(SButton) 51 | .Text(FText::FromString("Add")) 52 | .OnClicked(this, &SCustomNodeEditorWidget::HandleAddButtonClicked) 53 | .IsEnabled(this, &SCustomNodeEditorWidget::IsAddButtonEnabled) 54 | ] 55 | ] 56 | ]; 57 | } 58 | 59 | void SCustomNodeEditorWidget::OnSelectedNodesChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectInfo) 60 | { 61 | if (!PropertyWidget) 62 | { 63 | return; 64 | } 65 | 66 | // Check if a node is selected 67 | if (SelectedItem.IsValid()) 68 | { 69 | // Handle node selection here 70 | // For example, you can update the PropertyWidget with the selected node's properties 71 | PropertyWidget->SetObject(CustomDataAsset); 72 | } 73 | } 74 | 75 | void SCustomNodeEditorWidget::UpdateNodes(const TArray& NewNodes) 76 | { 77 | // Convert the array of custom data to an array of shared pointers 78 | TArray> SharedPtrNodes; 79 | for (const FCustomNodeData& Node : NewNodes) 80 | { 81 | SharedPtrNodes.Add(MakeShared(Node)); 82 | } 83 | 84 | // Clear the array and append the new nodes 85 | Nodes.Empty(); 86 | Nodes.Append(SharedPtrNodes); 87 | 88 | if (ListViewWidget.IsValid()) 89 | { 90 | ListViewWidget->RequestListRefresh(); 91 | } 92 | } 93 | 94 | FReply SCustomNodeEditorWidget::HandleAddButtonClicked() 95 | { 96 | if (NodeNameTextBox.IsValid()) 97 | { 98 | // Create a new node with the entered name 99 | FCustomNodeData NewNode; 100 | NewNode.Name = NodeNameTextBox->GetText().ToString(); 101 | Nodes.Add(MakeShared(NewNode)); 102 | 103 | if (ListViewWidget.IsValid()) 104 | { 105 | ListViewWidget->RequestListRefresh(); 106 | } 107 | 108 | // Clear the node name text box 109 | NodeNameTextBox->SetText(FText::GetEmpty()); 110 | 111 | SaveCustomDataAsset(); 112 | } 113 | 114 | return FReply::Handled(); 115 | } 116 | 117 | void SCustomNodeEditorWidget::HandleNodeNameCommitted(const FText& NewText, ETextCommit::Type CommitType) 118 | { 119 | if (CommitType == ETextCommit::OnEnter) 120 | { 121 | // Create a new node with the entered name 122 | FCustomNodeData NewNode; 123 | NewNode.Name = NewText.ToString(); 124 | Nodes.Add(MakeShared(NewNode)); 125 | 126 | if (ListViewWidget.IsValid()) 127 | { 128 | ListViewWidget->RequestListRefresh(); 129 | } 130 | 131 | // Clear the node name text box 132 | NodeNameTextBox->SetText(FText::GetEmpty()); 133 | 134 | SaveCustomDataAsset(); 135 | } 136 | } 137 | 138 | void SCustomNodeEditorWidget::SaveCustomDataAsset() 139 | { 140 | if (CustomDataAsset) 141 | { 142 | // Update the nodes in the CustomDataAsset with the added/removed nodes 143 | TArray AssetNodes; 144 | for (const TSharedPtr& Node : Nodes) 145 | { 146 | AssetNodes.Add(*Node); 147 | } 148 | 149 | CustomDataAsset->NodeTypeList = MoveTemp(AssetNodes); 150 | } 151 | } 152 | 153 | TSharedRef SCustomNodeEditorWidget::GenerateRowWidget(TSharedPtr Item, const TSharedRef& OwnerTable) 154 | { 155 | return SNew(STableRow>, OwnerTable) 156 | .Padding(FMargin(8.f, 8.f, 8.f, 8.f)) 157 | .OnDragDetected_Lambda([this, Item](const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)->FReply 158 | { 159 | return HandleDragDetected(MyGeometry, MouseEvent, Item); 160 | }) 161 | [ 162 | SNew(SHorizontalBox) 163 | 164 | + SHorizontalBox::Slot() 165 | .FillWidth(1.0f) 166 | [ 167 | SNew(STextBlock) 168 | .Text(FText::FromString(Item->Name)) 169 | ] 170 | 171 | + SHorizontalBox::Slot() 172 | .AutoWidth() 173 | .HAlign(HAlign_Right) 174 | [ 175 | SNew(SButton) 176 | .OnClicked_Lambda([this, Item]() -> FReply 177 | { 178 | Nodes.Remove(Item); 179 | SaveCustomDataAsset(); 180 | ListViewWidget->RequestListRefresh(); 181 | return FReply::Handled(); 182 | }) 183 | .ButtonStyle(FCoreStyle::Get(), "NoBorder") 184 | .ForegroundColor(FSlateColor(FLinearColor::Red)) 185 | .ToolTipText(FText::FromString("Delete")) 186 | [ 187 | SNew(STextBlock) 188 | .Text(FText::FromString("X")) 189 | ] 190 | ] 191 | ]; 192 | } 193 | 194 | FReply SCustomNodeEditorWidget::HandleDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, TSharedPtr Item) 195 | { 196 | if (Item.IsValid()) 197 | { 198 | SGraphPanel* GraphPanelPtr = CustomGraphWidget->GetGraphPanel(); 199 | 200 | DragDropOperation = FCustomDragConnection::New(GraphPanelPtr, Item); // Create an instance using MakeShareable 201 | 202 | DraggedItem = Item; 203 | 204 | return FReply::Handled().BeginDragDrop(DragDropOperation.ToSharedRef()); 205 | } 206 | 207 | return FReply::Unhandled(); 208 | } 209 | 210 | bool SCustomNodeEditorWidget::IsAddButtonEnabled() const 211 | { 212 | return NodeNameTextBox.IsValid() && !NodeNameTextBox->GetText().IsEmptyOrWhitespace(); 213 | } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/CustomNodeFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomNodeFactory.h" 2 | #include "EdGraph/EdGraphNode.h" 3 | #include "AssetEditor/EdCustomGraphNode.h" 4 | #include "AssetEditor/SCustomGraphNode.h" 5 | 6 | TSharedPtr FCustomPanelNodeFactory::CreateNode(UEdGraphNode* Node) const 7 | { 8 | if (UEdCustomGraphNode* EdNode = Cast(Node)) 9 | { 10 | return SNew(SCustomGraphNode, EdNode); 11 | } 12 | 13 | return nullptr; 14 | } -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Private/MyCustomPluginEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "MyCustomPluginEditor.h" 2 | #include "AssetToolsModule.h" 3 | #include "CustomDataAsset.h" 4 | #include "CustomDataAssetActions.h" 5 | #include 6 | 7 | #define LOCTEXT_NAMESPACE "FMyCustomPluginEditorModule" 8 | 9 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 10 | void FMyCustomPluginEditorModule::StartupModule() 11 | { 12 | IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); 13 | 14 | // Register the asset category 15 | const EAssetTypeCategories::Type MyCustomAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("MyCustomAsset")), LOCTEXT("ChiefRebelCategory", "Custom Graph Tool")); 16 | 17 | // This code registers the custom panel node factory with the graph editor, allowing the factory to create custom visual nodes within the editor. 18 | CustomPanelNodeFactory = MakeShareable(new FCustomPanelNodeFactory()); 19 | FEdGraphUtilities::RegisterVisualNodeFactory(CustomPanelNodeFactory); 20 | 21 | // Register the asset type actions 22 | CustomDataAssetActions = MakeShareable(new FCustomDataAssetActions(MyCustomAssetCategory)); 23 | AssetTools.RegisterAssetTypeActions(CustomDataAssetActions.ToSharedRef()); 24 | } 25 | 26 | void FMyCustomPluginEditorModule::ShutdownModule() 27 | { 28 | if (FModuleManager::Get().IsModuleLoaded("AssetTools")) 29 | { 30 | IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); 31 | 32 | // Unregister asset type actions associated with your module's class name 33 | TArray ClassesToUnregister; 34 | ClassesToUnregister.Add(UCustomDataAsset::StaticClass()); // Replace UCustomDataAsset with your custom asset class 35 | 36 | for (UClass* Class : ClassesToUnregister) 37 | { 38 | TWeakPtr AssetTypeActions = AssetTools.GetAssetTypeActionsForClass(Class); 39 | if (AssetTypeActions.IsValid()) 40 | { 41 | AssetTools.UnregisterAssetTypeActions(AssetTypeActions.Pin().ToSharedRef()); 42 | } 43 | } 44 | } 45 | 46 | if (CustomPanelNodeFactory.IsValid()) 47 | { 48 | FEdGraphUtilities::UnregisterVisualNodeFactory(CustomPanelNodeFactory); 49 | CustomPanelNodeFactory.Reset(); 50 | } 51 | } 52 | 53 | #undef LOCTEXT_NAMESPACE 54 | 55 | IMPLEMENT_MODULE(FMyCustomPluginEditorModule, MyCustomPluginEditor) -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/AssetEditor/CustomConnectionDrawingPolicy.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "ConnectionDrawingPolicy.h" 5 | 6 | /* 7 | Style policy for the edges between nodes (I added this for hover-effect on an edge) 8 | */ 9 | 10 | class MYCUSTOMPLUGINEDITOR_API FCustomConnectionDrawingPolicy : public FConnectionDrawingPolicy 11 | { 12 | public: 13 | FCustomConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj); 14 | 15 | virtual void DrawConnection(int32 LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) override; 16 | 17 | virtual void DetermineWiringStyle(UEdGraphPin* OutputPin, UEdGraphPin* InputPin, /*inout*/ FConnectionParams& Params) override; 18 | 19 | protected: 20 | UEdGraph* GraphObj; 21 | TMap NodeWidgetMap; 22 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/AssetEditor/CustomDragConnection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Input/DragAndDrop.h" 5 | #include "Input/Reply.h" 6 | #include "Widgets/SWidget.h" 7 | #include "GraphEditorDragDropAction.h" 8 | 9 | class SGraphPanel; 10 | 11 | /* 12 | Responsible of drag / drop events for list items into a graph (creating nodes in a graph when dropped) 13 | */ 14 | 15 | class FCustomDragConnection : public FGraphEditorDragDropAction 16 | { 17 | public: 18 | DRAG_DROP_OPERATOR_TYPE(FDragConnection, FGraphEditorDragDropAction) 19 | 20 | static TSharedRef New(SGraphPanel* GraphPanel, TSharedPtr InCustomNodeData); 21 | 22 | virtual FReply DroppedOnPanel(const TSharedRef& Panel, FVector2D ScreenPosition, FVector2D GraphPosition, UEdGraph& Graph) override; 23 | 24 | protected: 25 | typedef FGraphEditorDragDropAction Super; 26 | 27 | FCustomDragConnection(SGraphPanel* GraphPanelIn, FCustomNodeData* CustomNodeDataIn); 28 | 29 | protected: 30 | SGraphPanel* GraphPanel; 31 | FCustomNodeData* CustomNodeData; 32 | 33 | FVector2D DecoratorAdjust; 34 | }; 35 | -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/AssetEditor/CustomPinDragDropConnection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Input/DragAndDrop.h" 5 | #include "Input/Reply.h" 6 | #include "Widgets/SWidget.h" 7 | #include "SGraphPin.h" 8 | #include "GraphEditorDragDropAction.h" 9 | 10 | /* 11 | Responsible of drag / drop events for the pins in a node (Creates the ins and outs-connections between nodes) 12 | */ 13 | 14 | class FCustomPinDragDropConnection : public FGraphEditorDragDropAction 15 | { 16 | public: 17 | DRAG_DROP_OPERATOR_TYPE(FDragConnection, FGraphEditorDragDropAction) 18 | 19 | typedef TArray FDraggedPinTable; 20 | static TSharedRef New(const TSharedRef& InGraphPanel, const FDraggedPinTable& InStartingPins); 21 | 22 | virtual void OnDrop(bool bDropWasHandled, const FPointerEvent& MouseEvent) override; 23 | 24 | virtual void HoverTargetChanged() override; 25 | virtual FReply DroppedOnPin(FVector2D ScreenPosition, FVector2D GraphPosition) override; 26 | virtual FReply DroppedOnNode(FVector2D ScreenPosition, FVector2D GraphPosition) override; 27 | virtual void OnDragged(const class FDragDropEvent& DragDropEvent) override; 28 | 29 | /* 30 | * Function to check validity of graph pins in the StartPins list. This check helps to prevent processing graph pins which are outdated. 31 | */ 32 | virtual void ValidateGraphPinList(TArray& OutValidPins); 33 | 34 | protected: 35 | typedef FGraphEditorDragDropAction Super; 36 | 37 | // Constructor: Make sure to call Construct() after factorying one of these 38 | FCustomPinDragDropConnection(const TSharedRef& GraphPanel, const FDraggedPinTable& DraggedPins); 39 | 40 | protected: 41 | TSharedPtr GraphPanel; 42 | FDraggedPinTable DraggingPins; 43 | 44 | /** Offset information for the decorator widget */ 45 | FVector2D DecoratorAdjust; 46 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/AssetEditor/EdCustomGraph.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "EdGraph/EdGraph.h" 5 | #include "EdCustomGraph.generated.h" 6 | 7 | class UCustomDataAsset; 8 | 9 | /* 10 | It's responsible for the properties of the custom graph, behavior and serialization data. Right now it doesn't do much 11 | */ 12 | 13 | UCLASS() 14 | class MYCUSTOMPLUGINEDITOR_API UEdCustomGraph : public UEdGraph 15 | { 16 | GENERATED_BODY() 17 | 18 | public: 19 | UEdCustomGraph() = default; 20 | virtual ~UEdCustomGraph() = default; 21 | 22 | UCustomDataAsset* GetCustomData() const; 23 | 24 | virtual bool Modify(bool bAlwaysMarkDirty = true) override; 25 | virtual void PostEditUndo() override; 26 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/AssetEditor/EdCustomGraphNode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "EdGraph/EdGraphNode.h" 5 | #include "CustomDataAsset.h" 6 | #include "SCustomGraphNode.h" 7 | #include "EdCustomGraphNode.generated.h" 8 | 9 | /* 10 | It's responsible for the properties of the custom node, behavior and serialization data. 11 | */ 12 | 13 | UCLASS(MinimalAPI) 14 | class UEdCustomGraphNode : public UEdGraphNode 15 | { 16 | GENERATED_BODY() 17 | 18 | public: 19 | UEdCustomGraphNode(); 20 | virtual ~UEdCustomGraphNode(); 21 | 22 | virtual void AllocateDefaultPins() override; 23 | virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; 24 | virtual FLinearColor GetBackgroundColor() const; 25 | virtual FLinearColor GetBorderColor() const; 26 | virtual FText GetTooltipText() const override; 27 | virtual UEdGraphPin* GetInputPin() const; 28 | virtual UEdGraphPin* GetOutputPin() const; 29 | virtual void AutowireNewNode(UEdGraphPin* FromPin) override; 30 | 31 | public: 32 | UPROPERTY(VisibleAnywhere, Instanced, Category = "Custom Node") 33 | UCustomGraphNodeAsset* GraphNodeAsset; 34 | 35 | SCustomGraphNode* SEdNode; 36 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/AssetEditor/EdCustomGraphSchema.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "CustomDataAsset.h" 5 | #include "EdGraph/EdGraphSchema.h" 6 | #include "EdCustomGraphSchema.generated.h" 7 | 8 | class UEdCustomGraphNode; 9 | 10 | /* 11 | It's responsible for defining the rules, behavior, and structure of a graph within the Unreal Editor 12 | For example: Defining node types, managing connections, handling node actions, validating the graph structure 13 | */ 14 | 15 | UCLASS(MinimalAPI) 16 | class UEdCustomGraphSchema : public UEdGraphSchema 17 | { 18 | GENERATED_BODY() 19 | 20 | public: 21 | virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const override; 22 | 23 | virtual bool TryCreateConnection(UEdGraphPin* A, UEdGraphPin* B) const override; 24 | 25 | virtual void BreakNodeLinks(UEdGraphNode& TargetNode) const override; 26 | 27 | virtual void BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotifcation) const override; 28 | 29 | virtual void BreakSinglePinLink(UEdGraphPin* SourcePin, UEdGraphPin* TargetPin) const override; 30 | 31 | virtual bool SupportsDropPinOnNode(UEdGraphNode* InTargetNode, const FEdGraphPinType& InSourcePinType, EEdGraphPinDirection InSourcePinDirection, FText& OutErrorMessage) const override; 32 | 33 | virtual UEdGraphPin* DropPinOnNode(UEdGraphNode* InTargetNode, const FName& InSourcePinName, const FEdGraphPinType& InSourcePinType, EEdGraphPinDirection InSourcePinDirection) const override; 34 | 35 | virtual bool CreateAutomaticConversionNodeAndConnections(UEdGraphPin* A, UEdGraphPin* B) const override; 36 | 37 | virtual class FConnectionDrawingPolicy* CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const override; 38 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/AssetEditor/SCustomGraphNode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "SGraphNode.h" 5 | 6 | class UEdCustomGraphNode; 7 | 8 | /* 9 | Responsible for how the custom graph node will look like in the graph, but also how to interact with it 10 | */ 11 | 12 | class MYCUSTOMPLUGINEDITOR_API SCustomGraphNode : public SGraphNode 13 | { 14 | public: 15 | SLATE_BEGIN_ARGS(SCustomGraphNode) {} 16 | SLATE_END_ARGS() 17 | 18 | void Construct(const FArguments& InArgs, UEdCustomGraphNode* InNode); 19 | virtual void UpdateGraphNode() override; 20 | virtual void CreatePinWidgets() override; 21 | virtual void AddPin(const TSharedRef& PinToAdd) override; 22 | 23 | FSlateColor GetBorderBackgroundColor() const; 24 | const FSlateBrush* GetNameIcon() const; 25 | UEdCustomGraphNode* Node; 26 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/CustomDataAssetActions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "AssetTypeActions_Base.h" 5 | #include "CustomDataAsset.h" 6 | 7 | /* 8 | This class explains how the content browser is going to interact with the custom asset type 9 | Which category it's going to show up in, what color the icon should have and such 10 | */ 11 | 12 | class MYCUSTOMPLUGINEDITOR_API FCustomDataAssetActions : public FAssetTypeActions_Base 13 | { 14 | public: 15 | // Constructor 16 | FCustomDataAssetActions(const EAssetTypeCategories::Type InAssetCategory) 17 | : AssetCategory(InAssetCategory) { } 18 | 19 | // FAssetTypeActions_Base overrides 20 | virtual FText GetName() const override; 21 | virtual FColor GetTypeColor() const override; 22 | virtual UClass* GetSupportedClass() const override; 23 | virtual uint32 GetCategories() override; 24 | virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) override; 25 | 26 | private: 27 | EAssetTypeCategories::Type AssetCategory; 28 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/CustomDataAssetEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CustomNodeEditorWidget.h" 4 | 5 | class UCustomDataAsset; 6 | 7 | /* 8 | This class is responsible for arranging the layout and adding widgets to the tool window. 9 | It also is resposnible for the commands for the graph widget (should maybe be moved to another place?) 10 | */ 11 | 12 | class FCustomDataAssetEditor : public FAssetEditorToolkit 13 | { 14 | public: 15 | void InitCustomDataAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UCustomDataAsset* InCustomDataAsset); 16 | 17 | virtual void RegisterTabSpawners(const TSharedRef& TabManager) override; 18 | virtual void UnregisterTabSpawners(const TSharedRef& TabManager) override; 19 | 20 | // FAssetEditorToolkit overrides 21 | virtual FName GetToolkitFName() const override; 22 | virtual FText GetBaseToolkitName() const override; 23 | virtual FText GetToolkitName() const override; 24 | virtual FLinearColor GetWorldCentricTabColorScale() const override; 25 | virtual FString GetWorldCentricTabPrefix() const override; 26 | virtual void SaveAsset_Execute() override; 27 | 28 | TSharedRef SpawnTab_RuleList(const FSpawnTabArgs& Args); 29 | TSharedRef SpawnTab_Details(const FSpawnTabArgs& Args); 30 | TSharedRef SpawnTab_NodeList(const FSpawnTabArgs& Args); 31 | // ... additional necessary overrides for FAssetEditorToolkit 32 | 33 | protected: 34 | void OnSelectedNodesChanged(const TSet& NewSelection); 35 | TSharedRef CreateGraphtWidget(); 36 | void CreateInternalWidgets(); 37 | void CreateEdGraph(); 38 | void CreateCommandList(); 39 | 40 | // Delegates for graph editor commands 41 | void SelectAllNodes(); 42 | bool CanSelectAllNodes(); 43 | void DeleteSelectedNodes(); 44 | bool CanDeleteNodes(); 45 | void DeleteSelectedDuplicatableNodes(); 46 | void CutSelectedNodes(); 47 | bool CanCutNodes(); 48 | void CopySelectedNodes(); 49 | bool CanCopyNodes(); 50 | void PasteNodes(); 51 | void PasteNodesHere(const FVector2D& Location); 52 | bool CanPasteNodes(); 53 | void DuplicateNodes(); 54 | bool CanDuplicateNodes(); 55 | 56 | FGraphPanelSelectionSet GetSelectedNodes() const; 57 | 58 | private: 59 | void RebuildCustomData(); 60 | 61 | private: 62 | TSharedPtr CustomNodeEditorWidget; 63 | TSharedPtr CustomGraphWidget; 64 | TSharedPtr PropertyWidget; 65 | TSharedPtr EditorSettingsWidget; 66 | 67 | TSharedPtr GraphEditorCommands; 68 | 69 | UCustomDataAsset* CustomDataAsset; 70 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/CustomDataAssetFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "UObject/ObjectMacros.h" 5 | #include "Factories/Factory.h" 6 | #include "CustomDataAssetFactory.generated.h" 7 | 8 | /* 9 | Creates the custom data type that will hold all the information that the tool needs. 10 | */ 11 | 12 | UCLASS() 13 | class MYCUSTOMPLUGINEDITOR_API UCustomDataAssetFactory : public UFactory 14 | { 15 | GENERATED_BODY() 16 | 17 | public: 18 | UCustomDataAssetFactory(); 19 | 20 | // UFactory interface 21 | virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, 22 | UObject* Context, FFeedbackContext* Warn) override; 23 | // End of UFactory interface 24 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/CustomNodeEditorWidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Widgets/Views/SListView.h" 4 | #include "SlateBasics.h" 5 | #include "CustomNodeData.h" 6 | #include "CustomDataAsset.h" 7 | 8 | /* 9 | This widget displays, creates and removes "unique" node items in a list. 10 | You can drag and drop an item into a graph and it will generate a node in the graph 11 | */ 12 | 13 | class SCustomNodeEditorWidget : public SCompoundWidget 14 | { 15 | public: 16 | SLATE_BEGIN_ARGS(SCustomNodeEditorWidget) 17 | : _CustomDataAsset(nullptr), 18 | _CustomGraphWidget(nullptr), 19 | _PropertyWidget(nullptr) {} 20 | SLATE_ARGUMENT(UCustomDataAsset*, CustomDataAsset) 21 | SLATE_ARGUMENT(SGraphEditor*, CustomGraphWidget) 22 | SLATE_ARGUMENT(IDetailsView*, PropertyWidget) 23 | SLATE_END_ARGS() 24 | 25 | void Construct(const FArguments& InArgs); 26 | void UpdateNodes(const TArray& NewNodes); 27 | 28 | private: 29 | FReply HandleAddButtonClicked(); 30 | FReply HandleDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, TSharedPtr Item); 31 | void HandleNodeNameCommitted(const FText& NewText, ETextCommit::Type CommitType); 32 | void SaveCustomDataAsset(); 33 | void OnSelectedNodesChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectInfo); 34 | bool IsAddButtonEnabled() const; 35 | TSharedRef GenerateRowWidget(TSharedPtr Item, const TSharedRef& OwnerTable); 36 | 37 | private: 38 | TArray> Nodes; 39 | 40 | TSharedPtr>> ListViewWidget; 41 | TSharedPtr DraggedItem; 42 | TSharedPtr NodeNameTextBox; 43 | TSharedPtr AddButton; 44 | 45 | class IDetailsView* PropertyWidget; 46 | SGraphEditor* CustomGraphWidget; 47 | UCustomDataAsset* CustomDataAsset; 48 | 49 | TSharedPtr DragDropOperation; // Declare DragDropOperation as a pointer 50 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/CustomNodeFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "EdGraphUtilities.h" 4 | #include "EdGraph/EdGraphNode.h" 5 | 6 | /* 7 | Creates custom node 8 | */ 9 | 10 | class FCustomPanelNodeFactory : public FGraphPanelNodeFactory 11 | { 12 | virtual TSharedPtr CreateNode(UEdGraphNode* Node) const override; 13 | }; -------------------------------------------------------------------------------- /Source/MyCustomPluginEditor/Public/MyCustomPluginEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Modules/ModuleManager.h" 5 | #include "CustomDataAssetActions.h" 6 | 7 | /* 8 | Initializes and registers various components and actions related to custom asset management 9 | */ 10 | 11 | class FMyCustomPluginEditorModule : public IModuleInterface 12 | { 13 | public: 14 | virtual void StartupModule() override; 15 | virtual void ShutdownModule() override; 16 | 17 | private: 18 | TSharedPtr CustomDataAssetActions; 19 | 20 | TSharedPtr CustomPanelNodeFactory; 21 | }; -------------------------------------------------------------------------------- /graph_tool.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Myggski/graph-tool-plugin-ue5/ae7926d91f8af242a16dbb4701c492ca4d32c583/graph_tool.PNG --------------------------------------------------------------------------------