Editor Utility : Extract the good bits from a copied blueprint node
In the editor, copying a blueprint node gives a really long string listing all the info about that node in its graph, and the long string's format varies a fair amount between node types. I often look up Print String info to find where things went wrong in a blueprint execution (from the Output Log).
So I want to log particular info without having to type every time all the details that let me backtrack to the relevant spot in the graph (and to go to the right log location). You might notice if you search for just a node's name that you get results where that node is used all through the engine and plugins of your project, not just your own stuff.
What I wanted was a way to supply Print String or Log String an easy reference to the class , function or eventgraph, to append to any extra notes of my own. I don't like typing the info out everytime. So I wrote an editor utility widget to grab the clipboard, check it's a single node's copied content, then extract the details I want, show it to me, then provide a copy-to-clipboard button. This didn't take very long, but I got stuck on trying to find exceptions, given the formatted info in the various blueprint nodes isn't all the same.
A SpawnActorFromClass (K2Node) is different from a PrintString node, which has its name in MemberName="PrintString", so the code to negotiate the copied string, while it finally works, took a lot of tweaking.
This post is all about this output log, so what do we have here? Class [4P_Char::EventGraph] Node reached: SpawnActorFromClass_4' <Arrow that reflects>
The pattern is actually from an array with a few appended parts so i can read it:
Class | Location | Node Name | Node Comment
4P_Char | Eventgraph | SpawnActorFromClass_4 | Arrow that reflects is all I'm actually getting from the copied SpawnActor node in the blueprint. It's decorated a little using Append String pins in the widget blueprint. Sorry it isn't the clearest image, but the point is that the parts of the copied SpawnActor node can be pulled out of the clipboard string.
To do this in my C++ function, I already had a function library function that converts the clipboard to a string that another function can use. It's pretty easy to make a function library. Here is a guide by Chris McCole: https://www.chrismccole.com/blog/blueprint-function-libraries-in-unreal-engine-ue4ue5#:~:text=Making%20Blueprint%20Function%20Libraries%20in%20C%2B%2B&text=Creating%20the%20cpp%20file%20is,across%20all%20of%20your%20blueprints. Snippet of clipboard paste function in my project's functionlibrary.h
UFUNCTION(BlueprintCallable, Category = "FunFunctions", meta = (DisplayName = "StringToClipboard", ToolTip = "This function sends a String to Clipboard", Keywords = "funf,string,copy,clipboard"))
static void StringToClipboard(const FString& String);
UFUNCTION(BlueprintCallable, Category = "FunFunctions", BlueprintPure, meta = (DisplayName = "ClipboardToString", ToolTip = "This function returns the Clipboard as a String.", Keywords = "funf,get,clipboard,string"))
static FString ClipboardToString();
Snippet of clipboard paste function in my project's functionlibrary.cpp
// String clipboard function require the project's .Build.cs for the project to be edited to include ApplicationCore, as follows:
// PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "ApplicationCore" });
void UFunFunctionLibrary::StringToClipboard(const FString& String)
{
FPlatformApplicationMisc::ClipboardCopy(*String);
}
FString UFunFunctionLibrary::ClipboardToString()
{
FString ClipboardContent;
FPlatformApplicationMisc::ClipboardPaste(ClipboardContent);
return ClipboardContent;
}
The next step was to create an Editor Utility Widget C++ class from inside unreal, then (haha) get Chatgpt to write the method to parse the clipboard string. I told it I already had my get clipboard function, and it understood to include my functionlibrary and call it, but it was really iffy about handling the string, until I realised the rules for parsing the string weren't straightforward (the structure of the BP info differs across nodes types).
First, I need to make ChatGPT understand the required elements of a copied single node (in case what was in the clipboard was something else entirely, nothing, or maybe many copied nodes).
Next, I had to identify exactly what I wanted to return in an array: 0 - Class 1 - Eventgraph or function the node is found in 2 - The node name (of what was actually copied from unreal) 3 - Node comments (which I later used a widget Tickbox to make optional in the returned results).
As in pretty clear, in the image, the widget design is awesome, no real formatting style, just raw info.
The C++ function of the utility can figure out if the clipboard isn't a blueprint node or is empty and so on, so there's a little bit of user experience feedback. [Even while writing this, I made it hide the copy-to-clipboard UI Button if the output string is not valid, which shows the value of reviewing work I guess]. Unsurprisingly, because blueprint nodes come in many forms, the copied clipboard info from each node differs, especially between standard unreal functions (like SpawnActorFromClass) and custom or collapsed nodes. Timelines, for instance, follow a different information formatting. Very likely, I missed some other exceptional cases. So far though, testing things out, it's giving good results. There's probably even a better way to do it ; I thought it would be nice to RMB copy the class name or path from those big header texts at the top of each blueprint graph, like you can with classes in Visual Studio.
But no, they do highlight though... is there an easy way to get that text into clipboard?
While writing this, I noticed that I'd specified a custom project folder to look for the BP class in, and decided it's better to search from /Game/ forwards the left of the .Class part of the copied string. However, for nodes that are /Engine/ classes, that also has to be handled. Thus the hours of string untangling stretch on. Class[StandardMacros::Create and Assign MID] Node reached: MacroInstance'/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:Create and Assign MID.K2Node_MacroInstance_2' <> Having fixed this, the code checks for both /Game/ and /Engine/ classes, though at a glance it's probably more info that you'd want to log; logging a Standard Macro is kind of unusual. Still, a good test.
Well, now I have my widget UI, I just copy a node then ding the 'Get Node Details" button, and I get the string I want to log in my LogString nodes. You could adjust what info the utility returns, I just had a certain set of things I want to put into my logging. I know hardly any C++ in the proper sense, but I'm getting better at understanding what I'm seeing. The first part of the function handles the copied clipboard. The middle part (blue) extracts the array entries I mentioned earlier:
0 Class the node appears in
1 Graph location the node that was copied appears in (eventgraph or function)
2 Node that was copied
3 Comments on the node, if any.
And the last part of the function is forming the output array and opting out if something went wrong.
Don't worry about this image, the code is expandable below...
Code for the function (if you are following this, you can probably figure it out).
#include "FunBPHelperUtility.h"
#include "FunFunctionLibrary.h"
#include "Misc/OutputDeviceDebug.h"
#include "Misc/Paths.h"
void UFunBPHelperUtility::GetClipboardNodeInfo(bool& Success, TArray<FString>& NodeInfo)
{
Success = false;
NodeInfo.Empty();
// Get the clipboard content using the FunFunctionLibrary function
FString ClipboardContent = UFunFunctionLibrary::ClipboardToString();
GLog->Logf(ELogVerbosity::Warning, TEXT("Clipboard Content: %s"), *ClipboardContent);
if (ClipboardContent.IsEmpty())
{
GLog->Logf(ELogVerbosity::Warning, TEXT("Clipboard is empty"));
return;
}
// Extract Blueprint Class Name using ExportPath
FString BlueprintClassName;
int32 ExportPathStart = ClipboardContent.Find(TEXT("ExportPath="));
if (ExportPathStart != INDEX_NONE)
{
ExportPathStart += FString(TEXT("ExportPath=")).Len();
// Check if path is in /Game/ or /Engine/
int32 PathStart = INDEX_NONE;
FString PathIdentifier;
if (ClipboardContent.Find(TEXT("/Game/"), ESearchCase::IgnoreCase, ESearchDir::FromStart, ExportPathStart) != INDEX_NONE)
{
PathIdentifier = TEXT("/Game/");
PathStart = ClipboardContent.Find(PathIdentifier, ESearchCase::IgnoreCase, ESearchDir::FromStart, ExportPathStart);
}
else if (ClipboardContent.Find(TEXT("/Engine/"), ESearchCase::IgnoreCase, ESearchDir::FromStart, ExportPathStart) != INDEX_NONE)
{
PathIdentifier = TEXT("/Engine/");
PathStart = ClipboardContent.Find(PathIdentifier, ESearchCase::IgnoreCase, ESearchDir::FromStart, ExportPathStart);
}
if (PathStart != INDEX_NONE)
{
int32 ClassPathEnd = ClipboardContent.Find(TEXT("."), ESearchCase::IgnoreCase, ESearchDir::FromStart, PathStart);
if (ClassPathEnd != INDEX_NONE)
{
int32 ClassPathStart = INDEX_NONE;
for (int32 i = ClassPathEnd - 1; i >= PathStart; --i)
{
if (ClipboardContent[i] == '/' || ClipboardContent[i] == '\'')
{
ClassPathStart = i + 1;
break;
}
}
if (ClassPathStart != INDEX_NONE)
{
BlueprintClassName = ClipboardContent.Mid(ClassPathStart, ClassPathEnd - ClassPathStart);
}
}
}
}
// Extract Function or Event Graph Name
FString FunctionOrEventGraphName;
int32 FunctionOrEventGraphStart = ClipboardContent.Find(TEXT(":"), ESearchCase::IgnoreCase, ESearchDir::FromStart, ExportPathStart) + 1;
int32 FunctionOrEventGraphEnd = ClipboardContent.Find(TEXT("."), ESearchCase::IgnoreCase, ESearchDir::FromStart, FunctionOrEventGraphStart);
if (FunctionOrEventGraphEnd != INDEX_NONE)
{
FunctionOrEventGraphName = ClipboardContent.Mid(FunctionOrEventGraphStart, FunctionOrEventGraphEnd - FunctionOrEventGraphStart);
}
// Check for Timeline node and extract TimelineName if present
if (ClipboardContent.Contains(TEXT("K2Node_Timeline")))
{
int32 TimelineNameStart = ClipboardContent.Find(TEXT("TimelineName=\""), ESearchCase::IgnoreCase, ESearchDir::FromStart);
if (TimelineNameStart != INDEX_NONE)
{
TimelineNameStart += FString(TEXT("TimelineName=\"")).Len();
int32 TimelineNameEnd = ClipboardContent.Find(TEXT("\""), ESearchCase::IgnoreCase, ESearchDir::FromStart, TimelineNameStart);
if (TimelineNameEnd != INDEX_NONE)
{
FunctionOrEventGraphName = ClipboardContent.Mid(TimelineNameStart, TimelineNameEnd - TimelineNameStart);
}
}
}
// Extract Node Name
FString NodeName;
int32 NodeNameStart = ClipboardContent.Find(TEXT("MemberName="), ESearchCase::IgnoreCase, ESearchDir::FromStart);
if (NodeNameStart != INDEX_NONE)
{
NodeNameStart += FString(TEXT("MemberName=")).Len();
int32 NodeNameEnd = ClipboardContent.Find(TEXT(")"), ESearchCase::IgnoreCase, ESearchDir::FromStart, NodeNameStart);
if (NodeNameEnd != INDEX_NONE)
{
NodeName = ClipboardContent.Mid(NodeNameStart, NodeNameEnd - NodeNameStart);
}
}
else
{
// Fallback if MemberName is not found
int32 K2NodeStart = ClipboardContent.Find(TEXT(".K2Node_"), ESearchCase::IgnoreCase, ESearchDir::FromStart, ExportPathStart);
if (K2NodeStart != INDEX_NONE)
{
K2NodeStart += FString(TEXT(".K2Node_")).Len();
int32 K2NodeEnd = ClipboardContent.Find(TEXT("\""), ESearchCase::IgnoreCase, ESearchDir::FromStart, K2NodeStart);
if (K2NodeEnd != INDEX_NONE)
{
NodeName = ClipboardContent.Mid(K2NodeStart, K2NodeEnd - K2NodeStart);
}
}
}
// Extract Node Comment
FString NodeComment;
int32 NodeCommentStart = ClipboardContent.Find(TEXT("NodeComment=\""), ESearchCase::IgnoreCase, ESearchDir::FromStart);
if (NodeCommentStart != INDEX_NONE)
{
NodeCommentStart += FString(TEXT("NodeComment=\"")).Len();
int32 NodeCommentEnd = ClipboardContent.Find(TEXT("\""), ESearchCase::IgnoreCase, ESearchDir::FromStart, NodeCommentStart);
if (NodeCommentEnd != INDEX_NONE)
{
NodeComment = ClipboardContent.Mid(NodeCommentStart, NodeCommentEnd - NodeCommentStart);
}
}
// Clean up the extracted data
BlueprintClassName = BlueprintClassName.TrimStartAndEnd();
FunctionOrEventGraphName = FunctionOrEventGraphName.TrimStartAndEnd();
NodeName = NodeName.TrimStartAndEnd();
NodeComment = NodeComment.TrimStartAndEnd();
// Add the extracted information to the NodeInfo array
if (!BlueprintClassName.IsEmpty())
{
NodeInfo.Add(BlueprintClassName);
}
if (!FunctionOrEventGraphName.IsEmpty())
{
NodeInfo.Add(FunctionOrEventGraphName);
}
if (!NodeName.IsEmpty())
{
NodeInfo.Add(NodeName);
}
if (!NodeComment.IsEmpty())
{
NodeInfo.Add(NodeComment);
}
else
{
NodeInfo.Add(TEXT(""));
}
Success = true;
}
To do something with the array that this function produces, we have to actually make the UI widget and tell the buttons and text in the widget what to do. This is way easier than kicking chatgpt to make 100 lines of code.
This is the UE Pastebin of the above editor utility widget blueprint... in case viewing the image above is too small. https://blueprintue.com/blueprint/o9fm6ugj/
Button click events in the UI:
An extremely simple UI layout
What was all that in aid of? If it is faster to format information, it's going to make it more loggable, and then easier to look up.
Even though the unreliable, computationally expensive ChatGPT was used to figure out some string expressions, we end up with a reusable script and easy to access UI to run as often as required.
Comments