Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/WingToolMenu.cpp
2026-03-18 10:17:58 -04:00

279 lines
8.5 KiB
C++

#include "WingToolMenu.h"
#include "ToolMenuEntry.h"
#include "ToolMenuDelegates.h"
#include "ToolMenuContext.h"
#include "ToolMenus.h"
#include "WingUtils.h"
#include "EdGraph/EdGraphSchema.h"
#include "EdGraphSchema_K2.h"
#include "Framework/Commands/UIAction.h"
// ============================================================
// Private member access via template explicit-instantiation loophole.
//
// The C++ standard says "the usual access checking rules do not
// apply to names used to specify explicit instantiations." So
// &FToolMenuEntry::Action is legal as a template argument in an
// explicit instantiation, even though Action is private.
//
// The WingPrivateAccessor template captures the member pointer and exposes it
// through a friend function that we can call from normal code.
//
// See: https://bloglitb.blogspot.com/2011/12/access-to-private-members-safer.html
// ============================================================
template<typename Tag, typename Tag::type M>
struct WingPrivateAccessor
{
friend typename Tag::type GetPtr(Tag) { return M; }
};
// ----- FToolMenuEntry::Action -----
struct Tag_FToolMenuEntry_Action
{
using type = FToolUIActionChoice FToolMenuEntry::*;
friend type GetPtr(Tag_FToolMenuEntry_Action);
};
template struct WingPrivateAccessor<Tag_FToolMenuEntry_Action, &FToolMenuEntry::Action>;
static const FToolUIActionChoice& GetAction(const FToolMenuEntry& Entry)
{
return Entry.*GetPtr(Tag_FToolMenuEntry_Action());
}
// ----- FToolMenuEntry::Command -----
struct Tag_FToolMenuEntry_Command
{
using type = TSharedPtr<const FUICommandInfo> FToolMenuEntry::*;
friend type GetPtr(Tag_FToolMenuEntry_Command);
};
template struct WingPrivateAccessor<Tag_FToolMenuEntry_Command, &FToolMenuEntry::Command>;
static bool HasCommand(const FToolMenuEntry& Entry)
{
return Entry.*GetPtr(Tag_FToolMenuEntry_Command()) != nullptr;
}
// ============================================================
// Given a menu entry label, and a pin name (possibly empty),
// generate a better label for an LLM to type. The goal here
// is to have consistent spacing, consistent casing, so that
// the LLM can easily remember what to type.
// ============================================================
FText WingToolMenu::MakeBetterLabel(const UEdGraphPin *Pin, const FText &EntryLabel)
{
FString Sanitized = EntryLabel.ToString();
int32 Dst = 0;
bool Upper = true;
for (int32 Src = 0; Src < Sanitized.Len(); Src++)
{
TCHAR c = Sanitized[Src];
if (FChar::IsAlnum(c))
{
if (Upper) c = FChar::ToUpper(c);
Sanitized[Dst++] = c;
Upper = false;
}
else
{
Upper = true;
if ((c <= 0x20)||(c == 0x7F)) continue;
if (c == ':') c = '-';
Sanitized[Dst++] = c;
}
}
Sanitized.LeftInline(Dst);
if (Pin)
{
Sanitized = FString::Printf(TEXT("Pin:%s:%s"), *WingUtils::FormatName(Pin), *Sanitized);
}
return FText::FromString(Sanitized);
}
// ============================================================
// Check if an array of entries contains a specific label.
// ============================================================
bool WingToolMenu::ContainsText(const TArray<FText> &Texts, const FText &Value)
{
for (const FText &Text : Texts)
{
if (Value.IdenticalTo(Text))
{
return true;
}
}
return false;
}
// ============================================================
// AddEntry — create a synthetic menu entry with a direct action.
// ============================================================
void WingToolMenu::AddEntry(TArray<FToolMenuEntry>& Entries, UEdGraphPin* Pin,
const TCHAR* Label, FCanExecuteAction CanExec, FExecuteAction Exec)
{
if (!CanExec.Execute())
return;
FToolMenuEntry Entry = FToolMenuEntry::InitMenuEntry(
NAME_None,
MakeBetterLabel(Pin, FText::FromString(Label)),
FText::GetEmpty(),
FSlateIcon(),
FUIAction(MoveTemp(Exec), MoveTemp(CanExec)));
Entries.Add(MoveTemp(Entry));
}
void WingToolMenu::AddSyntheticEntries(TArray<FToolMenuEntry> &Entries, UEdGraphNode *NodePtr)
{
const UEdGraphSchema_K2 *K2Schema = Cast<UEdGraphSchema_K2>(NodePtr->GetSchema());
if (K2Schema == nullptr) return;
// TWeakObjectPtr<UEdGraphNode> Node(NodePtr);
for (UEdGraphPin *PinPtr : NodePtr->Pins)
{
if (PinPtr->bHidden) continue;
FEdGraphPinReference Pin(PinPtr);
AddEntry(Entries, PinPtr, TEXT("SplitStructPin"),
[=](){ UEdGraphPin *P=Pin.Get(); return P && K2Schema->CanSplitStructPin(*P); },
[=](){ UEdGraphPin *P=Pin.Get(); if (P) K2Schema->SplitPin(P); });
AddEntry(Entries, PinPtr, TEXT("RecombineStructPin"),
[=](){ UEdGraphPin *P=Pin.Get(); return P && K2Schema->CanRecombineStructPin(*P); },
[=](){ UEdGraphPin *P=Pin.Get(); if (P) K2Schema->RecombinePin(P); });
}
}
// ============================================================
// Get the Menu Items for a given node and pin. This doesn't
// do anything with the labels yet.
// ============================================================
TArray<FToolMenuEntry> WingToolMenu::GetMenuItems(
UGraphNodeContextMenuContext* GNC, const FToolMenuContext &TMC)
{
TArray<FToolMenuEntry> Result;
UToolMenu* Menu = NewObject<UToolMenu>();
GNC->Node->GetNodeContextMenuActions(Menu, GNC);
//GNC->Node->GetSchema()->GetContextMenuActions(Menu, GNC);
for (FToolMenuSection& Section : Menu->Sections)
{
Result.Append(Section.Blocks);
}
return Result;
}
TArray<FToolMenuEntry> WingToolMenu::GetMenuItems(UEdGraphNode *Node, const FToolMenuContext &Context)
{
// Create the two context objects.
TArray<FToolMenuEntry> Result;
UGraphNodeContextMenuContext* GNC = NewObject<UGraphNodeContextMenuContext>();
// Fetch the menu items for the node.
GNC->Init(Node->GetGraph(), Node, nullptr, false);
TArray<FToolMenuEntry> NodeEntries = GetMenuItems(GNC, Context);
// Improve the labels for the node entries, and also
// record the original labels.
TArray<FText> OriginalLabels;
for (FToolMenuEntry &Entry: NodeEntries)
{
FText Label = Entry.Label.Get();
OriginalLabels.Add(Label);
if (!CanExecute(Entry, Context)) continue;
Entry.Label = MakeBetterLabel(nullptr, Label);
Result.Add(Entry);
}
// Fetch the Menu items for the pins. Discard
// pins whose original label exactly matches the
// original label of a node entry.
for (const UEdGraphPin *Pin : Node->Pins)
{
if (Pin->bHidden) continue;
FString PinName = WingUtils::FormatName(Pin);
GNC->Init(Node->GetGraph(), Node, Pin, false);
TArray<FToolMenuEntry> PinEntries = GetMenuItems(GNC, Context);
for (FToolMenuEntry &PinEntry : PinEntries)
{
FText Label = PinEntry.Label.Get();
if (!ContainsText(OriginalLabels, Label))
{
if (CanExecute(PinEntry, Context))
{
PinEntry.Label = MakeBetterLabel(Pin, Label);
Result.Add(PinEntry);
}
}
}
}
AddSyntheticEntries(Result, Node);
return Result;
}
// ============================================================
// Menu entry resolution
//
// We only handle the three Action-based callback mechanisms:
// 1. FToolUIAction — delegates that take a FToolMenuContext
// 2. FToolDynamicUIAction — same, Blueprint-friendly variant
// 3. FUIAction — plain delegates, no context needed
//
// Command-based entries are skipped — they depend on editor
// selection/focus state which we can't reliably provide.
// ============================================================
bool WingToolMenu::CanExecute(const FToolMenuEntry& Entry, const FToolMenuContext& Context)
{
if (HasCommand(Entry))
return false;
const FToolUIActionChoice& Choice = GetAction(Entry);
if (const FToolUIAction* ToolAction = Choice.GetToolUIAction())
{
if (ToolAction->CanExecuteAction.IsBound())
return ToolAction->CanExecuteAction.Execute(Context);
return ToolAction->ExecuteAction.IsBound();
}
if (const FToolDynamicUIAction* DynamicAction = Choice.GetToolDynamicUIAction())
{
if (DynamicAction->CanExecuteAction.IsBound())
return DynamicAction->CanExecuteAction.Execute(Context);
return DynamicAction->ExecuteAction.IsBound();
}
if (const FUIAction* Action = Choice.GetUIAction())
return Action->IsBound() && Action->CanExecute();
return false;
}
bool WingToolMenu::Execute(const FToolMenuEntry& Entry, const FToolMenuContext& Context)
{
if (HasCommand(Entry))
return false;
const FToolUIActionChoice& Choice = GetAction(Entry);
if (const FToolUIAction* ToolAction = Choice.GetToolUIAction())
{
ToolAction->ExecuteAction.ExecuteIfBound(Context);
return true;
}
if (const FToolDynamicUIAction* DynamicAction = Choice.GetToolDynamicUIAction())
{
DynamicAction->ExecuteAction.ExecuteIfBound(Context);
return true;
}
if (const FUIAction* Action = Choice.GetUIAction())
return Action->Execute();
return false;
}