A "MCP" combo button in the top toolbar (LevelEditor.LevelEditorToolBar.User) opens a dropdown with: - Status: live Remote Control reachability (:30010, green/red) + re-check, and a "Start Remote Control server" action. - Active tools (N): submenu listing the catalogue from tools.json (name + desc). - Settings: opens the Setup Wizard tab. The bridge ping is refactored into PingRc(bool) so it can update a cached status silently (menu open) or with the startup notification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
583 lines
20 KiB
C++
583 lines
20 KiB
C++
#include "UEBlueprintMCPEditor.h"
|
|
|
|
#include "HttpModule.h"
|
|
#include "Interfaces/IHttpResponse.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "BlueprintEditor.h"
|
|
#include "BlueprintEditorContext.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "EdGraphNode_Comment.h"
|
|
#include "Framework/Application/IInputProcessor.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Framework/Commands/UIAction.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
#include "ScopedTransaction.h"
|
|
#include "SGraphPanel.h"
|
|
#include "ToolMenu.h"
|
|
#include "ToolMenuEntry.h"
|
|
#include "ToolMenuSection.h"
|
|
#include "ToolMenus.h"
|
|
#include "Framework/Docking/TabManager.h"
|
|
#include "Widgets/Docking/SDockTab.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "Engine/Engine.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Dom/JsonObject.h"
|
|
#include "Dom/JsonValue.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "SMCPWizard.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "UEBlueprintMCPEditor"
|
|
|
|
namespace
|
|
{
|
|
const TCHAR* MCPZoneCommentText = TEXT("MCP Zone");
|
|
const TCHAR* LegacyMCPZoneCommentText = TEXT("MCP Draft Zone");
|
|
const FName MCPWizardTabId(TEXT("MCPSetupWizard"));
|
|
|
|
FString MCPPluginDir()
|
|
{
|
|
TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UEBlueprintMCP"));
|
|
return Plugin.IsValid() ? FPaths::ConvertRelativePathToFull(Plugin->GetBaseDir()) : FString();
|
|
}
|
|
|
|
// Reads the shipped tool catalogue (tools.json). Returns name + description pairs.
|
|
bool MCPReadTools(TArray<TPair<FString, FString>>& Out)
|
|
{
|
|
const FString Dir = MCPPluginDir();
|
|
if (Dir.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
FString Json;
|
|
if (!FFileHelper::LoadFileToString(Json, *(Dir / TEXT("tools.json"))))
|
|
{
|
|
return false;
|
|
}
|
|
TArray<TSharedPtr<FJsonValue>> Items;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Json);
|
|
if (!FJsonSerializer::Deserialize(Reader, Items))
|
|
{
|
|
return false;
|
|
}
|
|
for (const TSharedPtr<FJsonValue>& Item : Items)
|
|
{
|
|
const TSharedPtr<FJsonObject> Obj = Item.IsValid() ? Item->AsObject() : nullptr;
|
|
if (Obj.IsValid())
|
|
{
|
|
Out.Emplace(Obj->GetStringField(TEXT("name")), Obj->GetStringField(TEXT("description")));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool IsMCPZoneComment(const UEdGraphNode* Node)
|
|
{
|
|
const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node);
|
|
return CommentNode
|
|
&& (CommentNode->NodeComment.Equals(MCPZoneCommentText, ESearchCase::IgnoreCase)
|
|
|| CommentNode->NodeComment.Equals(LegacyMCPZoneCommentText, ESearchCase::IgnoreCase));
|
|
}
|
|
}
|
|
|
|
class FMCPZoneInputProcessor : public IInputProcessor
|
|
{
|
|
public:
|
|
explicit FMCPZoneInputProcessor(FUEBlueprintMCPEditorModule& InOwner)
|
|
: Owner(InOwner)
|
|
{
|
|
}
|
|
|
|
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override
|
|
{
|
|
}
|
|
|
|
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
|
{
|
|
if (!Owner.bMCPZoneModeActive || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSharedPtr<FBlueprintEditor> Editor = Owner.ActiveMCPZoneEditor.Pin();
|
|
if (!Editor.IsValid() || !Editor->GetFocusedGraph())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Editor->GetFocusedGraph(), false);
|
|
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
|
if (!GraphPanel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
|
const FVector2f ScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
|
if (!Geometry.IsUnderLocation(ScreenPosition))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bDragging = true;
|
|
StartScreenPosition = ScreenPosition;
|
|
return true;
|
|
}
|
|
|
|
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
|
{
|
|
return bDragging && Owner.bMCPZoneModeActive;
|
|
}
|
|
|
|
virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
|
{
|
|
if (!bDragging || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bDragging = false;
|
|
const FVector2f EndScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
|
Owner.TryCreateMCPZoneFromScreenDrag(StartScreenPosition, EndScreenPosition);
|
|
return true;
|
|
}
|
|
|
|
virtual const TCHAR* GetDebugName() const override
|
|
{
|
|
return TEXT("MCPZoneInputProcessor");
|
|
}
|
|
|
|
private:
|
|
FUEBlueprintMCPEditorModule& Owner;
|
|
bool bDragging = false;
|
|
FVector2f StartScreenPosition = FVector2f::ZeroVector;
|
|
};
|
|
|
|
IMPLEMENT_MODULE(FUEBlueprintMCPEditorModule, UEBlueprintMCPEditor)
|
|
|
|
void FUEBlueprintMCPEditorModule::StartupModule()
|
|
{
|
|
UToolMenus::RegisterStartupCallback(
|
|
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::RegisterMenus));
|
|
|
|
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FUEBlueprintMCPEditorModule::NotifyBridgeStatus);
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::NotifyBridgeStatus()
|
|
{
|
|
// Startup check: ping and show a one-off notification.
|
|
PingRc(true);
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::PingRc(bool bNotify)
|
|
{
|
|
const FString Url = TEXT("http://127.0.0.1:30010/remote/info");
|
|
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
|
|
Request->SetVerb(TEXT("GET"));
|
|
Request->SetURL(Url);
|
|
Request->SetTimeout(3.0f);
|
|
Request->OnProcessRequestComplete().BindLambda(
|
|
[this, bNotify](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSucceeded)
|
|
{
|
|
const bool bOk = bSucceeded && Resp.IsValid() && Resp->GetResponseCode() == 200;
|
|
CachedRcStatus = bOk ? EMcpRcStatus::Up : EMcpRcStatus::Down;
|
|
if (!bNotify)
|
|
{
|
|
return;
|
|
}
|
|
FNotificationInfo Info(bOk
|
|
? LOCTEXT("MCPBridgeUp", "UE Blueprint MCP bridge ready (Remote Control :30010)")
|
|
: LOCTEXT("MCPBridgeDown", "UE Blueprint MCP bridge NOT reachable on :30010 — check Remote Control plugin"));
|
|
Info.ExpireDuration = bOk ? 4.0f : 8.0f;
|
|
Info.bUseSuccessFailIcons = true;
|
|
Info.bFireAndForget = true;
|
|
TSharedPtr<SNotificationItem> Item = FSlateNotificationManager::Get().AddNotification(Info);
|
|
if (Item.IsValid())
|
|
{
|
|
Item->SetCompletionState(bOk ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
|
|
}
|
|
});
|
|
Request->ProcessRequest();
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::ShutdownModule()
|
|
{
|
|
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
|
|
|
|
if (bWizardTabRegistered && FSlateApplication::IsInitialized())
|
|
{
|
|
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(MCPWizardTabId);
|
|
bWizardTabRegistered = false;
|
|
}
|
|
|
|
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
|
{
|
|
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
|
MCPZoneInputProcessor.Reset();
|
|
}
|
|
|
|
if (UToolMenus::IsToolMenuUIEnabled())
|
|
{
|
|
UToolMenus::UnRegisterStartupCallback(this);
|
|
UToolMenus::UnregisterOwner(this);
|
|
}
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::RegisterWizardTabAndMenu()
|
|
{
|
|
// Nomad tab that hosts the wizard.
|
|
if (!bWizardTabRegistered && FSlateApplication::IsInitialized())
|
|
{
|
|
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
|
|
MCPWizardTabId,
|
|
FOnSpawnTab::CreateRaw(this, &FUEBlueprintMCPEditorModule::SpawnWizardTab))
|
|
.SetDisplayName(LOCTEXT("MCPWizardTabTitle", "MCP Setup Wizard"))
|
|
.SetTooltipText(LOCTEXT("MCPWizardTabTooltip", "Set up the UE Blueprint MCP: Remote Control, API keys, and the tool list."))
|
|
.SetMenuType(ETabSpawnerMenuType::Hidden);
|
|
bWizardTabRegistered = true;
|
|
}
|
|
|
|
// Tools menu entry to open it.
|
|
UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools");
|
|
if (ToolsMenu)
|
|
{
|
|
FToolMenuSection& Section = ToolsMenu->FindOrAddSection("MCP", LOCTEXT("MCPMenuSection", "MCP"));
|
|
Section.AddMenuEntry(
|
|
"OpenMCPSetupWizard",
|
|
LOCTEXT("OpenMCPWizard", "MCP Setup Wizard"),
|
|
LOCTEXT("OpenMCPWizardTip", "Open the UE Blueprint MCP setup wizard (Remote Control, API keys, tools)."),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"),
|
|
FUIAction(FExecuteAction::CreateLambda([]()
|
|
{
|
|
FGlobalTabmanager::Get()->TryInvokeTab(MCPWizardTabId);
|
|
})));
|
|
}
|
|
}
|
|
|
|
TSharedRef<SDockTab> FUEBlueprintMCPEditorModule::SpawnWizardTab(const FSpawnTabArgs& Args)
|
|
{
|
|
return SNew(SDockTab)
|
|
.TabRole(ETabRole::NomadTab)
|
|
[
|
|
SNew(SMCPWizard)
|
|
];
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::RegisterMCPToolbarButton()
|
|
{
|
|
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.User");
|
|
if (!Toolbar)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FToolMenuSection& Section = Toolbar->FindOrAddSection("MCP");
|
|
FToolMenuEntry Entry = FToolMenuEntry::InitComboButton(
|
|
"MCPMenu",
|
|
FUIAction(),
|
|
FNewToolMenuChoice(FNewToolMenuDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::BuildMCPStatusMenu)),
|
|
LOCTEXT("MCPToolbarLabel", "MCP"),
|
|
LOCTEXT("MCPToolbarTip", "UE Blueprint MCP — status, active tools and settings"),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"));
|
|
Section.AddEntry(Entry);
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::BuildMCPStatusMenu(UToolMenu* Menu)
|
|
{
|
|
// Kick a fresh (silent) ping so the next open reflects current state.
|
|
PingRc(false);
|
|
|
|
// --- Status ---
|
|
{
|
|
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPStatus", LOCTEXT("MCPStatusHdr", "Status"));
|
|
|
|
FText StatusText;
|
|
FSlateIcon StatusIcon;
|
|
switch (CachedRcStatus)
|
|
{
|
|
case EMcpRcStatus::Up:
|
|
StatusText = LOCTEXT("RcUp", "Remote Control: connected (:30010)");
|
|
StatusIcon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.SuccessWithColor");
|
|
break;
|
|
case EMcpRcStatus::Down:
|
|
StatusText = LOCTEXT("RcDown", "Remote Control: not reachable");
|
|
StatusIcon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.ErrorWithColor");
|
|
break;
|
|
default:
|
|
StatusText = LOCTEXT("RcUnknown", "Remote Control: checking…");
|
|
StatusIcon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.WarningWithColor");
|
|
break;
|
|
}
|
|
|
|
Sec.AddMenuEntry("MCPRcStatus", StatusText,
|
|
LOCTEXT("RcStatusTip", "Click to re-check the Remote Control bridge."),
|
|
StatusIcon,
|
|
FUIAction(FExecuteAction::CreateRaw(this, &FUEBlueprintMCPEditorModule::PingRc, false)));
|
|
|
|
Sec.AddMenuEntry("MCPStartRC",
|
|
LOCTEXT("StartRC", "Start Remote Control server"),
|
|
LOCTEXT("StartRCTip", "Run WebControl.StartServer and enable it on startup."),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateLambda([this]()
|
|
{
|
|
if (GEngine)
|
|
{
|
|
GEngine->Exec(nullptr, TEXT("WebControl.EnableServerOnStartup 1"));
|
|
GEngine->Exec(nullptr, TEXT("WebControl.StartServer"));
|
|
}
|
|
PingRc(false);
|
|
})));
|
|
}
|
|
|
|
// --- Tools ---
|
|
{
|
|
TArray<TPair<FString, FString>> Tools;
|
|
MCPReadTools(Tools);
|
|
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPTools", LOCTEXT("MCPToolsHdr", "Tools"));
|
|
Sec.AddSubMenu("MCPToolsList",
|
|
FText::Format(LOCTEXT("ActiveTools", "Active tools ({0})"), FText::AsNumber(Tools.Num())),
|
|
LOCTEXT("ActiveToolsTip", "The MCP tools this server exposes to your client."),
|
|
FNewToolMenuDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::BuildToolsSubMenu),
|
|
false,
|
|
FSlateIcon());
|
|
}
|
|
|
|
// --- Settings ---
|
|
{
|
|
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPSettings", LOCTEXT("MCPSettingsHdr", "Settings"));
|
|
Sec.AddMenuEntry("MCPOpenWizard",
|
|
LOCTEXT("OpenWizardEntry", "Open Setup Wizard…"),
|
|
LOCTEXT("OpenWizardEntryTip", "Remote Control, API keys and the full tool list."),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"),
|
|
FUIAction(FExecuteAction::CreateLambda([]()
|
|
{
|
|
FGlobalTabmanager::Get()->TryInvokeTab(MCPWizardTabId);
|
|
})));
|
|
}
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::BuildToolsSubMenu(UToolMenu* Menu)
|
|
{
|
|
TArray<TPair<FString, FString>> Tools;
|
|
if (!MCPReadTools(Tools) || Tools.Num() == 0)
|
|
{
|
|
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPToolsNone");
|
|
Sec.AddMenuEntry("MCPNoTools",
|
|
LOCTEXT("NoTools", "tools.json not found"),
|
|
LOCTEXT("NoToolsTip", "Run `npm run dump-tools` in the plugin folder to generate the catalogue."),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateLambda([]() {}), FCanExecuteAction::CreateLambda([]() { return false; })));
|
|
return;
|
|
}
|
|
|
|
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPToolsItems");
|
|
for (const TPair<FString, FString>& Tool : Tools)
|
|
{
|
|
// First line of the description as the tooltip — keeps it readable.
|
|
FString Tip = Tool.Value;
|
|
int32 Dot;
|
|
if (Tip.FindChar(TEXT('.'), Dot) && Dot > 20)
|
|
{
|
|
Tip = Tip.Left(Dot + 1);
|
|
}
|
|
Sec.AddMenuEntry(FName(*Tool.Key),
|
|
FText::FromString(Tool.Key),
|
|
FText::FromString(Tip),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateLambda([]() {})));
|
|
}
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::RegisterMenus()
|
|
{
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
|
|
RegisterWizardTabAndMenu();
|
|
RegisterMCPToolbarButton();
|
|
|
|
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("AssetEditor.BlueprintEditor.ToolBar");
|
|
FToolMenuSection& Section = Toolbar->FindOrAddSection("Settings");
|
|
|
|
Section.AddDynamicEntry("MCPDraftZoneCommands", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection)
|
|
{
|
|
UBlueprintEditorToolMenuContext* Context = InSection.FindContext<UBlueprintEditorToolMenuContext>();
|
|
if (!Context || !Context->BlueprintEditor.IsValid() || !Context->GetBlueprintObj())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TWeakPtr<FBlueprintEditor> WeakEditor = Context->BlueprintEditor;
|
|
FUIAction Action(
|
|
FExecuteAction::CreateLambda([this, WeakEditor]()
|
|
{
|
|
ToggleMCPZoneMode(WeakEditor);
|
|
}),
|
|
FCanExecuteAction::CreateLambda([WeakEditor]()
|
|
{
|
|
return WeakEditor.IsValid() && WeakEditor.Pin()->GetFocusedGraph() != nullptr;
|
|
}),
|
|
FIsActionChecked::CreateLambda([this, WeakEditor]()
|
|
{
|
|
return IsMCPZoneModeActive(WeakEditor);
|
|
}));
|
|
|
|
FToolMenuEntry Entry = FToolMenuEntry::InitToolBarButton(
|
|
FName(TEXT("MCPDraftZone")),
|
|
FToolUIActionChoice(Action),
|
|
LOCTEXT("MCPDraftZone_Label", "MCP Zone"),
|
|
LOCTEXT("MCPDraftZone_Tooltip", "Toggle MCP Zone mode. While active, left-mouse drag in the graph to create a generation zone. Toggle off to remove MCP Zone comments while keeping generated nodes."),
|
|
FSlateIcon(),
|
|
EUserInterfaceActionType::ToggleButton);
|
|
InSection.AddEntry(Entry);
|
|
}));
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::ToggleMCPZoneMode(TWeakPtr<FBlueprintEditor> BlueprintEditor)
|
|
{
|
|
TSharedPtr<FBlueprintEditor> Editor = BlueprintEditor.Pin();
|
|
if (!Editor.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == Editor)
|
|
{
|
|
RemoveMCPZones(Editor->GetBlueprintObj());
|
|
bMCPZoneModeActive = false;
|
|
ActiveMCPZoneEditor.Reset();
|
|
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
|
{
|
|
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
|
MCPZoneInputProcessor.Reset();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (bMCPZoneModeActive)
|
|
{
|
|
if (TSharedPtr<FBlueprintEditor> PreviousEditor = ActiveMCPZoneEditor.Pin())
|
|
{
|
|
RemoveMCPZones(PreviousEditor->GetBlueprintObj());
|
|
}
|
|
}
|
|
|
|
bMCPZoneModeActive = true;
|
|
ActiveMCPZoneEditor = Editor;
|
|
if (!MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
|
{
|
|
MCPZoneInputProcessor = MakeShared<FMCPZoneInputProcessor>(*this);
|
|
FSlateApplication::Get().RegisterInputPreProcessor(MCPZoneInputProcessor);
|
|
}
|
|
}
|
|
|
|
bool FUEBlueprintMCPEditorModule::IsMCPZoneModeActive(TWeakPtr<FBlueprintEditor> BlueprintEditor) const
|
|
{
|
|
return bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == BlueprintEditor.Pin();
|
|
}
|
|
|
|
bool FUEBlueprintMCPEditorModule::TryCreateMCPZoneFromScreenDrag(const FVector2f& StartScreenPosition, const FVector2f& EndScreenPosition)
|
|
{
|
|
TSharedPtr<FBlueprintEditor> Editor = ActiveMCPZoneEditor.Pin();
|
|
if (!Editor.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UBlueprint* Blueprint = Editor->GetBlueprintObj();
|
|
UEdGraph* Graph = Editor->GetFocusedGraph();
|
|
if (!Blueprint || !Graph)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Graph, false);
|
|
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
|
if (!GraphPanel)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
|
const FVector2f LocalStart = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(StartScreenPosition));
|
|
const FVector2f LocalEnd = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(EndScreenPosition));
|
|
const FVector2f GraphStart = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalStart));
|
|
const FVector2f GraphEnd = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalEnd));
|
|
|
|
CreateMCPZone(Graph, Blueprint, GraphStart, GraphEnd);
|
|
return true;
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::CreateMCPZone(UEdGraph* Graph, UBlueprint* Blueprint, const FVector2f& GraphStart, const FVector2f& GraphEnd) const
|
|
{
|
|
if (!Blueprint || !Graph)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FScopedTransaction Transaction(LOCTEXT("CreateMCPDraftZone", "Create MCP Draft Zone"));
|
|
Graph->Modify();
|
|
Blueprint->Modify();
|
|
|
|
constexpr int32 MinWidth = 240;
|
|
constexpr int32 MinHeight = 160;
|
|
const int32 MinX = FMath::FloorToInt(FMath::Min(GraphStart.X, GraphEnd.X));
|
|
const int32 MinY = FMath::FloorToInt(FMath::Min(GraphStart.Y, GraphEnd.Y));
|
|
const int32 MaxX = FMath::CeilToInt(FMath::Max(GraphStart.X, GraphEnd.X));
|
|
const int32 MaxY = FMath::CeilToInt(FMath::Max(GraphStart.Y, GraphEnd.Y));
|
|
|
|
UEdGraphNode_Comment* ZoneNode = NewObject<UEdGraphNode_Comment>(Graph);
|
|
ZoneNode->CreateNewGuid();
|
|
ZoneNode->NodeComment = MCPZoneCommentText;
|
|
ZoneNode->bCommentBubbleVisible = false;
|
|
ZoneNode->CommentColor = FLinearColor(0.05f, 0.32f, 0.85f, 0.35f);
|
|
ZoneNode->NodePosX = MinX;
|
|
ZoneNode->NodePosY = MinY;
|
|
ZoneNode->NodeWidth = FMath::Max(MinWidth, MaxX - MinX);
|
|
ZoneNode->NodeHeight = FMath::Max(MinHeight, MaxY - MinY);
|
|
|
|
Graph->AddNode(ZoneNode, true, false);
|
|
ZoneNode->PostPlacedNewNode();
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
|
}
|
|
|
|
void FUEBlueprintMCPEditorModule::RemoveMCPZones(UBlueprint* Blueprint) const
|
|
{
|
|
if (!Blueprint)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FScopedTransaction Transaction(LOCTEXT("RemoveMCPDraftZones", "Remove MCP Draft Zones"));
|
|
Blueprint->Modify();
|
|
|
|
TArray<UEdGraph*> Graphs;
|
|
Blueprint->GetAllGraphs(Graphs);
|
|
for (UEdGraph* Graph : Graphs)
|
|
{
|
|
if (!Graph)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Graph->Modify();
|
|
TArray<UEdGraphNode*> Nodes = Graph->Nodes;
|
|
for (UEdGraphNode* Node : Nodes)
|
|
{
|
|
if (IsMCPZoneComment(Node))
|
|
{
|
|
Graph->RemoveNode(Node);
|
|
}
|
|
}
|
|
}
|
|
|
|
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|