279 lines
8.5 KiB
C++
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;
|
|
}
|