Making A Whole New Editor Node
What Is Editor Node?
Editor Nodes (a.k.a Graph Node) are the type of nodes that are used as interfaces between user interaction on the graph and the node object (Node instance) itself.
What Is The Differences Between Editor Node And Node Instance?
In Joint, we name the actual node object as Node instance, and the graph node as Editor Node.
Node instances are the basic objects that will contain the data that will be utilized on runtime and also some logic runs on the runtime to manipulate the dialogue itself.
On the other hand, Editor Nodes are the objects that will only available on the graph and editor module in the development.
So if you want to make some properties available on the runtime Editor nodes are the objects that rely on the editor graph instance, and they represent the node instances they have in the graph.
Editor nodes will handle some of the important data and process related to the editor feature including slate population of the graph node, and pin data control for the graph, and any additional process to manipulate and pass over the data for the node instances that the editor node represent.
You don't need to make unique editor nodes for each node instance classes on the system since the default editor node that joint will assign to the node instances (fragments, base nodes) already handles most of the logic with quite elegant and decent ways, and also the node instances now have some particular ways to manipulate the data in the editor node (ex, custom pin system).
Then Why Should I Make A New Editor Node Class?
Yet manipulating editor nodes in node instances has some limitations thus knowing how to work with Editor nodes (UDialogueEdGraphNode) is important for such usages:
- Slate Control.
- Complex editing features and data control, better editor transaction and communication with the editor modules.
Slate Control Especially, if you're working on a huge and complex project that requires some unique editing features that enables you to iterate your content faster and better, then knowing how to make a new editor node class is a must.
And also the performance of Simple Property Display Tab is quite bad, so if you want to make a better performing property display on your node, then making a new editor node class and handling the properties with custom slate can help you a lot.
Making a New Editor Node Class
We only support implmenenting a new editor node class via C++, if you are working on the BP only project, then you are out of luck.
You can force to make a new editor node class in BP, but it will not work as intended. Please don't do that.
Start making a new editor node class by overriding UDialogueEdGraphNode in C++.
For fragment editor node class, Please use UDialogueEdGraphNode_Fragment.
Specifying Node Instance Class for Editor Node Class.
Let's start this out by overriding SupportedNodeClass function.
SupportedNodeClass is a function where you can set and tell the system about which class of the node instance object your editor node is designed to be used with. It can be C++ class or BP class as needed.
TSubclassOf<UDialogueNodeBase> UDialogueEdFragment_Sequence::SupportedNodeClass()
{
	return UDF_Sequence::StaticClass();
}
'''note
For BP classes, you can use some convenient function that joint native provide to grab the BP class by its name, GetBPClassWithName in UJointNativeFunctionLibrary but please be careful with it since by any changes on its name can break your code.
'''
Once you overrided this class properly, now the editor will automatically pick and use your new editor node class for the specified node instance type when you newly create a node on the graph editor.
'''warning If multiple editor nodes specify the same node as its supported node class, then it will pick the first one on the class cache in the system singleton, which mean, you can not control or specify it on the editor side as you want. Just avoid doing it so. '''
Basically, it's the only mandatory stuff you need to do when you are implementing a new editor node class to the system. The following instructions are all optional, so please read and use them as you need.
Populating and Manipulating Custom Slates (Widgets)
In Joint, unlike SDS1, now every custom slates that you need for your node class can be created (populated) and controlled via editor node class, and you don't need to make a whole new slate class for the custom widget (still you can if you need) and just patch up the slate fragment on the graph slate's layout.
/**
* Use this function to populate any default widgets on the graph node body.
* Access the slate and change the slate as you want.
*
* If you must change the slate after it is populated, you can access the slate with GetGraphNodeSlate(). Use this function on further customization and instancing.
*
* Note for SDS1 users : You don't need to create new node slate class and mess around the editor to assign the slate anymore because now we have this function.
* See how the new native editor fragments utilize this function.
*/
virtual void ModifyGraphNodeSlate();
Let's start making our custom slates by overriding this function, and check out how the nodes in the Joint Native is using this function to populate its own unique slates.
void UDialogueEdFragment_Participant::ModifyGraphNodeSlate()
{
	//Abort if we can not get the graph node slate instance.
	if (!GetGraphNodeSlate().IsValid()) return;
	const TSharedPtr<SDialogueGraphNodeBase> NodeSlate = GetGraphNodeSlate();
	ParticipantBox = SNew(SVerticalBox);
	NodeSlate->CenterContentBox->AddSlot()
		.HAlign(HAlign_Fill)
		//.VAlign(VAlign_Fill)
		.Padding(FJointEditorStyle::Margin_Frame)
		[
			ParticipantBox.ToSharedRef()
		];
	UpdateSlate();
}
void UDialogueEdFragment_Participant::UpdateSlate()
{
	if (!GetGraphNodeSlate().IsValid()) return;
	const TSharedPtr<SJointGraphNodeSubNodeBase> NodeSlate = StaticCastSharedPtr<SJointGraphNodeSubNodeBase>(
		GetGraphNodeSlate().Pin());
	if(NodeSlate && NodeSlate->CenterContentBox)
	{
		UDF_Participant* CastedNodeInstance = GetCastedNodeInstance<UDF_Participant>();
		if (CastedNodeInstance == nullptr) return;
		TSharedPtr<SWidget> ConditionSlate =
			SNew(SBorder)
			.Visibility(EVisibility::HitTestInvisible)
			.BorderImage(FJointEditorStyle::Get().GetBrush("JointUI.Border.Round"))
			.BorderBackgroundColor(GetNodeBodyTintColor())
			.Padding(FJointEditorStyle::Margin_Normal)
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Center)
			[
				SNew(SBorder)
				.BorderImage(FJointEditorStyle::Get().GetBrush("JointUI.Border.Empty"))
				.Padding(FMargin(0))
				.HAlign(HAlign_Center)
				[
					SNew(SHorizontalBox)
					+ SHorizontalBox::Slot()
					.AutoWidth()
					//.HAlign(HAlign_Right)
					.VAlign(VAlign_Center)
					.Padding(FJointEditorStyle::Margin_Normal)
					[
						SNew(SBox)
						.HeightOverride(16)
						.WidthOverride(16)
						[
							SNew(SImage)
							.Image(FJointEditorStyle::GetUEEditorSlateStyleSet().GetBrush("ShowFlagsMenu.SubMenu.Developer"))
						]
					]
					+ SHorizontalBox::Slot()
					.AutoWidth()
					.VAlign(VAlign_Center)
					[
						SNew(STextBlock)
						.Justification(ETextJustify::Center)
						.Text(FText::FromString(CastedNodeInstance->ParticipantTag.ToString()))
					]
				]
			];
		const UVoltAnimation* Anim = VOLT_MAKE_ANIMATION(UVoltAnimation)
			(
				VOLT_MAKE_MODULE(UVolt_ASM_InterpRenderOpacity)
				.TargetOpacity(1)
				.RateBasedInterpSpeed(10),
				VOLT_MAKE_MODULE(UVolt_ASM_InterpWidgetTransform)
				.StartWidgetTransform(FWidgetTransform(
				FVector2D::ZeroVector,
				FVector2D(0.9, 0.9),
				FVector2D::ZeroVector,
				0))
				.RateBasedInterpSpeed(10)
			);
	
		VOLT_PLAY_ANIM(ConditionSlate, Anim);
		ParticipantBox->ClearChildren();
		ParticipantBox->AddSlot()
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				ConditionSlate.ToSharedRef()
			];
	}
}
The actual result is as following:

In ModifyGraphNodeSlate(), you can populate the initial slates you can use to decorate your graph node. One thing that you must know is that this function will not be triggered again after the graph refreshing request. thus you can not use this function to update your slate, but only initializing.
if you need a slate that doesn't change over time and change on the other propertie's modifications, it will do anything, but if you need to make your slate be updated according to some property modification, then you have to manually update your slate on somewhere else.
/**
* Callback for the node instance's property change event. Override and use this function to implement actions for the property change.
* By default, it is used to call UpdatePins, RequestPopulationOfNodeTagWidgets and NodeConnectionListChanged.
*/
virtual void OnNodeInstancePropertyChanged(const FPropertyChangedEvent& PropertyChangedEvent, const FString& PropertyName);
OnNodeInstancePropertyChanged() is one of the best places you can update your slate as you need. This function will be executed whenever any properties on the node instance have been changed, so you can do whatever you need for your slate on here.
For example, In Joint Native, UDialogueEdFragment_SpeakerAndListener updates its slate on OnNodeInstancePropertyChanged like this.
void UDialogueEdFragment_SpeakerAndListener::OnNodeInstancePropertyChanged(
	const FPropertyChangedEvent& PropertyChangedEvent,
	const FString& PropertyName)
{
	Super::OnNodeInstancePropertyChanged(PropertyChangedEvent, PropertyName);
	UpdateSlate();
}
void UDialogueEdFragment_SpeakerAndListener::UpdateSlate()
{
	if (!GetGraphNodeSlate().IsValid()) return;
	const TSharedPtr<SDialogueGraphNodeBase> NodeSlate = GetGraphNodeSlate();
	SpeakersBox->ClearChildren();
	ListenersBox->ClearChildren();
	UDF_SpeakerAndListener* SpeakerAndListener = GetCastedNodeInstance<UDF_SpeakerAndListener>();
	if (SpeakerAndListener == nullptr) return;
	for (int i = 0; i < SpeakerAndListener->Listeners.Num(); ++i)
	{
		if (!SpeakerAndListener->Listeners.IsValidIndex(i)) continue;
		FDialogueNodePointer& Listener = SpeakerAndListener->Listeners[i];
		const TAttribute<FText> DisplayText_Attr = TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateLambda(
			[Listener]
			{
				if (Listener.Node == nullptr) return LOCTEXT("NoParticipant", "No Participant Specified");
				if (const UDF_Participant* CastedNode = Cast<UDF_Participant>(Listener.Node.Get()))
					return FText::FromString(
						CastedNode->ParticipantTag.ToString());
				return FText::GetEmpty();
			}));
		ListenersBox->AddSlot()
		[
			SNew(SDialogueNodePointerSlate)
			.GraphContextObject(this)
			.DisplayName(DisplayText_Attr)
			.PointerToStructure(&Listener)
			.bShouldShowDisplayName(true)
			.bShouldShowNodeName(true)
		];
	}
	for (int i = 0; i < SpeakerAndListener->Speakers.Num(); ++i)
	{
		if (!SpeakerAndListener->Speakers.IsValidIndex(i)) continue;
		FDialogueNodePointer& Speaker = SpeakerAndListener->Speakers[i];
		const TAttribute<FText> DisplayText_Attr = TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateLambda(
			[Speaker]
			{
				if (Speaker.Node == nullptr) return LOCTEXT("NoParticipant", "No Participant Specified");
				if (const UDF_Participant* CastedNode = Cast<UDF_Participant>(Speaker.Node.Get()))
					return FText::FromString(
						CastedNode->ParticipantTag.ToString());
				return FText::GetEmpty();
			}));
		SpeakersBox->AddSlot()
		[
			SNew(SDialogueNodePointerSlate)
			.GraphContextObject(this)
			.DisplayName(DisplayText_Attr)
			.PointerToStructure(&Speaker)
			.bShouldShowDisplayName(true)
			.bShouldShowNodeName(true)
		];
	}
}
In UDialogueEdFragment_SpeakerAndListener, it populates two custom vertical box on its layout and use that box to shows the widget that indicates each speakers and listeners that are assigend to the node, and update the content of the box in OnNodeInstancePropertyChanged() with a function named UpdateSlate().
You can see that we are accessing the graph node slate via GetGraphNodeSlate and populating our slates on a predefined layout on the slate named CenterContentBox.
SDialogueGraphNodeBasehas following layout slates :
TSharedPtr<SBorder> NodeBody;
  
TSharedPtr<SHorizontalBox> NameBox;
TSharedPtr<SVerticalBox> CenterWholeBox;
TSharedPtr<SVerticalBox> CenterContentBox;
TSharedPtr<SWrapBox> SubNodeBox;
TSharedPtr<SVerticalBox> NodeTagBox;
TSharedPtr<SVerticalBox> PropertyDisplayBox;
They are all guaranteed to be available after the graph node slate initalization stage, which means the editor initialization.
You can grab a layout slate as you want and attach a slate from there.
'''note We are not going to go through all the steps related to the slate system here. Check out the other materials for your need.
And if you need, you can check out some of the editor nodes in Joint Native to start off how to make your own editor node slate with. '''
Using Volt on Your Node Slates
Joint provide a strong slate animation library called Volt and every animation effects on the editor and graph are made with and powered by Volt,
You can also use that in your graph node slates too! In fact, Volt is licensed under MIT license, so if you need, then it is okay to fork it and use it on your own product. (See the official GitHub repository. (https://github.com/GGgRain/Unreal-Volt))
'''note Volt is completely independent module from the Joint and JointEditor Module. You need to include the Volt, VoltCore module to your module if you are working on the other module than Joint, JointNative, JointEditor modules.
Here is the brief explanation for each modules, please check out and use these as you need:
VoltCore : The framework itself. it includes all the classes to animate your slates. This is necessary if you want to work with Volt. Volt : The content module for the animations. It only includes the animation modules and animations themselves. Use this module when you need templete classes for Volt and slate animations. This module is dependent on VoltCore. '''
We are not going to take a look at all the details you have to know about Volt to use it, instead we will learn how to quick-start it from the beginning. Check out our official documentation about Volt on the official Github repository.
Let's assume that you are going to use Volt on your slates on your custom graph nodes. GraphNode slates will have its own UVoltAnimationManager inside, so as long as your slate sticks with the node slate, you don't need to implement a whole new VoltAnimationManager.
Try accessing the existing animation manager via SDialogueGraphNodeBase::GetAnimationManager().
/**
* Get the Volt animation manager that handles the slate animations on the node.
* @return The Volt animation manager of the graph node slate.
*/
UVoltAnimationManager* GetAnimationManager() const;
It is guaranteed to be accessible on almost every stage of the lifecycle of the slate, and even if it is not present yet, it will try to create a new instance and handle you the reference of that newly created instance once you try to access that manager via SDialogueGraphNodeBase::GetAnimationManager().
Please let us know if it was not accessible via this function, we still can not guarantee it 10/10 because there might be some unfortunate occasions
Once you grab that instance, then now you can animate your slates with that instance.
if (UVolt_ASA_Expand* Animation = VOLT_GET_ANIMATION<UVolt_ASA_Expand>(UVolt_ASA_Expand::StaticClass()))
{
	VOLT_PLAY_ANIM(GetAnimationManager(), NodeBody, Animation); // In header; TSharedPtr<SBorder> NodeBody; 
}
Here is an example that you can find in Joint Editor, which is actually the part of the code that we animates the graph node slate with expanding animation when the drag-drop operation moves onto the slate.
Joint's Built-In Graph Node Slates
JointEditor module involves various helpful slate classes that you can use on your fragment to boost your productivity.
SDialogueNodePointerSlate
SDialogueNodePointerSlate is a slate class that can be used to indicate and edit a single node pointer structure (FDialogueNodePointer).
//In DialogueEdFragment_SpeakerAndListener.cpp...
SNew(SDialogueNodePointerSlate)
			.GraphContextObject(this)
			.DisplayName(DisplayText_Attr)
			.PointerToStructure(&Speaker)
			.bShouldShowDisplayName(true)
			.bShouldShowNodeName(true)
			
You can populate it on the layout and feed structure's pointer to the slate to make the slate display that structure.
Check out Joint native's DialogueEdFragment_SpeakerAndListener.cpp for the further details.
SContextTextEditor (WYSIWYG editor for text properties)
Joint provides simple WYSIWYG editor for the text editing.
SAssignNew(ContextTextEditor, SContextTextEditor)
		.Text(ContextText_Attr)
		.TableToEdit(TableToEdit_Attr)
		.bUseStyling(bUseStyler_Attr)
		.OnTextChanged_UObject(this, &UDialogueEdFragment_Text::OnTextChange)
		.OnTextCommitted_UObject(this, &UDialogueEdFragment_Text::OnTextCommitted)
It will take the text attribute for the text being displayed on the editor, and take another attribute that refers to the textstyle table for the styling.
It will not work as a WYSIWYG editor if you don't set bUseStyling true even if you provided a valid textstyle table to the slate, if it is false, it will work just same as a simple text box for the text editing.
Please notice that the update on the text on the slate will not be replicated back to the original property! You must manually update it via ``OnTextChanged, OnTextCommitted` instead.
Check how UDialogueEdFragment_Text is handling the update on the slate.
It is also utilizing editor transactions here to notify the modifications on the objects to the editor and record them. It will be a good, solid example for the transactions on the editor nodes either.
void UDialogueEdFragment_Text::OnTextChange(const FText& Text)
{
	UDF_Text* CastedNode = GetCastedNodeInstance<UDF_Text>();
	if (!bHasTransaction)
	{
		GEditor->BeginTransaction(FText::FromString("Modify Context Text"));
		bHasTransaction = true;
	}
	CastedNode->Modify();
	
	const FText SavedText = CastedNode->Text;
	
	FString OutKey, OutNamespace;
	const FString Namespace = FTextInspector::GetNamespace(SavedText).Get(FString());
	const FString Key = FTextInspector::GetKey(SavedText).Get(FString());
	
	//Holds new text and let engine caches this text in.
	FJointEdUtils::DialogueText_StaticStableTextIdWithObj(
		this,
		IEditableTextProperty::ETextPropertyEditAction::EditedSource,
		Text.ToString(),
		Namespace,
		Key,
		OutNamespace,
		OutKey
		);
	CastedNode->Text = FText::ChangeKey(FTextKey(OutNamespace),FTextKey(OutKey),Text);
	
}
void UDialogueEdFragment_Text::OnTextCommitted(const FText& Text, ETextCommit::Type Arg)
{
	UDF_Text* CastedNode = GetCastedNodeInstance<UDF_Text>();
	if (!bHasTransaction)
	{
		GEditor->BeginTransaction(FText::FromString("Modify Context Text"));
		bHasTransaction = true;
	}
	CastedNode->Modify();
	const FText SavedText = CastedNode->Text;
	
	FString OutKey, OutNamespace;
	const FString Namespace = FTextInspector::GetNamespace(SavedText).Get(FString());
	const FString Key = FTextInspector::GetKey(SavedText).Get(FString());
	//Holds new text and let engine caches this text in. This is fundamental when it comes to the 
	FJointEdUtils::DialogueText_StaticStableTextIdWithObj(
		this,
		IEditableTextProperty::ETextPropertyEditAction::EditedSource,
		Text.ToString(),
		Namespace,
		Key,
		OutNamespace,
		OutKey
		);
	CastedNode->Text = FText::ChangeKey(FTextKey(OutNamespace),FTextKey(OutKey),Text);
	if (bHasTransaction)
	{
		GEditor->EndTransaction();
		bHasTransaction = false;
	}
}