From 3cae682bef3a02e37136955f0285e3c030792590 Mon Sep 17 00:00:00 2001 From: SoloGorilla <10895928+SoloGorilla@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:30:02 +0200 Subject: [PATCH 1/2] fix(umg): bind_on_hovered and bind_on_value_changed author real bound-event nodes bind_on_hovered and bind_on_value_changed returned an instruction string ("Bind '...' to ...'s ... event.") instead of authoring a node, unlike bind_on_clicked which creates a real UK2Node_ComponentBoundEvent. Extracted the working bind_on_clicked path into a shared BindComponentDelegateEvent helper and routed both actions through it: - bind_on_hovered -> Button OnHovered. - bind_on_value_changed -> per widget type: Slider/SpinBox OnValueChanged, CheckBox OnCheckStateChanged, ComboBoxString OnSelectionChanged; unsupported widgets return UNSUPPORTED_WIDGET instead of a misleading success. Both now return a real node (nodeId + compileSucceeded), are idempotent (reuse an existing bound event), and only dirty/recompile when something changed. create_property_binding is left unchanged. Verified live in UE 5.8: bind_on_hovered and bind_on_value_changed (Slider) both produce real K2Node_ComponentBoundEvent nodes with compileSucceeded:true. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ionBridge_WidgetAuthoringEventBindings.cpp | 139 ++++++++++++++---- 1 file changed, 112 insertions(+), 27 deletions(-) diff --git a/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp b/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp index 219de6929..ef73b1bbd 100644 --- a/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp +++ b/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp @@ -22,6 +22,99 @@ namespace WidgetAuthoringHandlers { using namespace WidgetAuthoringHelpers; +// Create a real UK2Node_ComponentBoundEvent for an arbitrary multicast delegate on a +// widget component — the same path the Designer's "+ " button uses. Mirrors the +// bind_on_clicked implementation so OnHovered / OnValueChanged author genuine nodes +// (nodeId + compileSucceeded) instead of returning an instruction string. Sends the +// response itself and always returns true (request handled). +static bool BindComponentDelegateEvent( + UMcpAutomationBridgeSubsystem& Subsystem, + const FString& RequestId, + TSharedPtr RequestingSocket, + TSharedPtr ResultJson, + UWidgetBlueprint* WidgetBP, + UWidget* TargetWidget, + const FString& SlotName, + UClass* DelegateOwnerClass, + const FName DelegateName, + const FString& EventTypeLabel, + const FString& FunctionName) +{ + bool bBlueprintChanged = false; + if (!TargetWidget->bIsVariable) + { + TargetWidget->Modify(); + TargetWidget->bIsVariable = true; + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WidgetBP); + bBlueprintChanged = true; + } + + FMulticastDelegateProperty* DelegateProp = + FindFProperty(DelegateOwnerClass, DelegateName); + if (!DelegateProp) + { + Subsystem.SendAutomationError(RequestingSocket, RequestId, + FString::Printf(TEXT("%s delegate not found on %s"), *DelegateName.ToString(), *DelegateOwnerClass->GetName()), + TEXT("DELEGATE_NOT_FOUND")); + return true; + } + + FObjectProperty* CompProp = + FindFProperty(WidgetBP->SkeletonGeneratedClass, FName(*SlotName)); + if (!CompProp) + { + Subsystem.SendAutomationError(RequestingSocket, RequestId, + FString::Printf(TEXT("Component variable '%s' not found on widget skeleton class"), *SlotName), + TEXT("COMPONENT_PROPERTY_NOT_FOUND")); + return true; + } + + UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(WidgetBP); + if (!EventGraph) + { + Subsystem.SendAutomationError(RequestingSocket, RequestId, TEXT("Event graph not found on widget blueprint"), TEXT("EVENT_GRAPH_NOT_FOUND")); + return true; + } + + // Idempotent: reuse an existing bound event for this delegate+component. + const UK2Node_ComponentBoundEvent* Existing = + FKismetEditorUtilities::FindBoundEventForComponent(WidgetBP, DelegateProp->GetFName(), CompProp->GetFName()); + + UK2Node_ComponentBoundEvent* BoundNode = const_cast(Existing); + bool bCreatedNew = false; + if (!BoundNode) + { + EventGraph->Modify(); + FGraphNodeCreator Creator(*EventGraph); + BoundNode = Creator.CreateNode(false); + BoundNode->InitializeComponentBoundEventParams(CompProp, DelegateProp); + Creator.Finalize(); + bCreatedNew = true; + bBlueprintChanged = true; + } + + bool bCompiled = true; + if (bBlueprintChanged) + { + FBlueprintEditorUtils::MarkBlueprintAsModified(WidgetBP); + bCompiled = McpSafeCompileBlueprint(WidgetBP); + } + + ResultJson->SetBoolField(TEXT("success"), true); + ResultJson->SetStringField(TEXT("slotName"), SlotName); + ResultJson->SetStringField(TEXT("eventType"), EventTypeLabel); + ResultJson->SetStringField(TEXT("functionName"), FunctionName); + ResultJson->SetBoolField(TEXT("bound"), true); + ResultJson->SetBoolField(TEXT("createdNew"), bCreatedNew); + ResultJson->SetBoolField(TEXT("compileSucceeded"), bCompiled); + ResultJson->SetStringField(TEXT("nodeId"), BoundNode->NodeGuid.ToString()); + ResultJson->SetStringField(TEXT("eventFunctionName"), BoundNode->CustomFunctionName.ToString()); + + Subsystem.SendAutomationResponse(RequestingSocket, RequestId, true, + FString::Printf(TEXT("%s event bound"), *EventTypeLabel), ResultJson); + return true; +} + bool HandleWidgetAuthoringEventBindings( UMcpAutomationBridgeSubsystem& Subsystem, const FString& RequestId, @@ -177,16 +270,9 @@ bool HandleWidgetAuthoringEventBindings( return true; } - ResultJson->SetBoolField(TEXT("success"), true); - ResultJson->SetStringField(TEXT("slotName"), SlotName); - ResultJson->SetStringField(TEXT("eventType"), TEXT("OnHovered")); - ResultJson->SetStringField(TEXT("functionName"), FunctionName); - ResultJson->SetStringField(TEXT("instruction"), FString::Printf(TEXT("Bind '%s' to %s's OnHovered event."), *FunctionName, *SlotName)); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WidgetBP); - - Subsystem.SendAutomationResponse(RequestingSocket, RequestId, true, TEXT("OnHovered binding info provided"), ResultJson); - return true; + return BindComponentDelegateEvent(Subsystem, RequestId, RequestingSocket, ResultJson, + WidgetBP, ButtonWidget, SlotName, UButton::StaticClass(), FName(TEXT("OnHovered")), + TEXT("OnHovered"), FunctionName); } if (SubAction.Equals(TEXT("bind_on_value_changed"), ESearchCase::IgnoreCase)) @@ -222,26 +308,25 @@ bool HandleWidgetAuthoringEventBindings( return true; } - // Determine widget type for appropriate binding info - FString WidgetType = TargetWidget->GetClass()->GetName(); - FString EventName = TEXT("OnValueChanged"); + // Resolve the value-changed multicast delegate per widget type. + const FString WidgetType = TargetWidget->GetClass()->GetName(); + FName DelegateName = NAME_None; + if (Cast(TargetWidget) || Cast(TargetWidget)) DelegateName = FName(TEXT("OnValueChanged")); + else if (Cast(TargetWidget)) DelegateName = FName(TEXT("OnCheckStateChanged")); + else if (Cast(TargetWidget)) DelegateName = FName(TEXT("OnSelectionChanged")); - if (Cast(TargetWidget)) EventName = TEXT("OnValueChanged (float)"); - else if (Cast(TargetWidget)) EventName = TEXT("OnCheckStateChanged (bool)"); - else if (Cast(TargetWidget)) EventName = TEXT("OnValueChanged (float)"); - else if (Cast(TargetWidget)) EventName = TEXT("OnSelectionChanged (FString)"); + if (DelegateName.IsNone()) + { + Subsystem.SendAutomationError(RequestingSocket, RequestId, + FString::Printf(TEXT("Widget '%s' (%s) has no supported value-changed delegate. Supported: Slider, SpinBox, CheckBox, ComboBoxString."), *SlotName, *WidgetType), + TEXT("UNSUPPORTED_WIDGET")); + return true; + } - ResultJson->SetBoolField(TEXT("success"), true); - ResultJson->SetStringField(TEXT("slotName"), SlotName); ResultJson->SetStringField(TEXT("widgetType"), WidgetType); - ResultJson->SetStringField(TEXT("eventType"), EventName); - ResultJson->SetStringField(TEXT("functionName"), FunctionName); - ResultJson->SetStringField(TEXT("instruction"), FString::Printf(TEXT("Bind '%s' to %s's %s event."), *FunctionName, *SlotName, *EventName)); - - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WidgetBP); - - Subsystem.SendAutomationResponse(RequestingSocket, RequestId, true, TEXT("OnValueChanged binding info provided"), ResultJson); - return true; + return BindComponentDelegateEvent(Subsystem, RequestId, RequestingSocket, ResultJson, + WidgetBP, TargetWidget, SlotName, TargetWidget->GetClass(), DelegateName, + DelegateName.ToString(), FunctionName); } if (SubAction.Equals(TEXT("create_property_binding"), ESearchCase::IgnoreCase)) From 46b79fb835d292a70bfe8cea0c76b98bb5888cab Mon Sep 17 00:00:00 2001 From: SoloGorilla Date: Mon, 6 Jul 2026 16:39:35 +0200 Subject: [PATCH 2/2] refactor(umg): route bind_on_clicked through the shared bound-event helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up, both findings: - bind_on_clicked now delegates to BindComponentDelegateEvent after its Button lookup, removing the duplicated binding path so future fixes land in one place. Response contract is unchanged (same fields, same "OnClicked event bound" message). - BindComponentDelegateEvent marks the Blueprint structurally modified when it creates a new bound-event node — the same call the Designer's FKismetEditorUtilities::CreateNewBoundEventForComponent path makes — which now covers all three bind actions. --- ...ionBridge_WidgetAuthoringEventBindings.cpp | 96 +++---------------- 1 file changed, 13 insertions(+), 83 deletions(-) diff --git a/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp b/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp index ef73b1bbd..1ddc47091 100644 --- a/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp +++ b/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/WidgetAuthoring/Bindings/McpAutomationBridge_WidgetAuthoringEventBindings.cpp @@ -23,10 +23,13 @@ namespace WidgetAuthoringHandlers using namespace WidgetAuthoringHelpers; // Create a real UK2Node_ComponentBoundEvent for an arbitrary multicast delegate on a -// widget component — the same path the Designer's "+ " button uses. Mirrors the -// bind_on_clicked implementation so OnHovered / OnValueChanged author genuine nodes -// (nodeId + compileSucceeded) instead of returning an instruction string. Sends the -// response itself and always returns true (request handled). +// widget component — the same path the Designer's "+ " button uses. Shared by +// bind_on_clicked / bind_on_hovered / bind_on_value_changed so all three author genuine +// nodes (nodeId + compileSucceeded) through one code path. Idempotent: a repeat call +// reuses the existing bound event and leaves the asset untouched (no dirty / recompile). +// Response contract: functionName echoes the request input; the real handler is +// engine-generated and returned as eventFunctionName (CustomFunctionName). +// Sends the response itself and always returns true (request handled). static bool BindComponentDelegateEvent( UMcpAutomationBridgeSubsystem& Subsystem, const FString& RequestId, @@ -89,6 +92,9 @@ static bool BindComponentDelegateEvent( BoundNode = Creator.CreateNode(false); BoundNode->InitializeComponentBoundEventParams(CompProp, DelegateProp); Creator.Finalize(); + // Adding a bound-event node is a structural change (new function entry on the class) — same call the + // Designer's FKismetEditorUtilities::CreateNewBoundEventForComponent path makes after node creation. + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WidgetBP); bCreatedNew = true; bBlueprintChanged = true; } @@ -156,85 +162,9 @@ bool HandleWidgetAuthoringEventBindings( return true; } - // Create a real UK2Node_ComponentBoundEvent bound to the button's OnClicked - // multicast delegate — the exact node the Designer's "+ OnClicked" adds. - // Track whether we actually mutate the blueprint so the reuse path stays - // side-effect free (no dirty / recompile when nothing changed). - bool bBlueprintChanged = false; - if (!ButtonWidget->bIsVariable) - { - ButtonWidget->Modify(); - ButtonWidget->bIsVariable = true; - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(WidgetBP); - bBlueprintChanged = true; - } - - FMulticastDelegateProperty* DelegateProp = - FindFProperty(UButton::StaticClass(), FName(TEXT("OnClicked"))); - if (!DelegateProp) - { - Subsystem.SendAutomationError(RequestingSocket, RequestId, TEXT("OnClicked delegate not found on UButton"), TEXT("DELEGATE_NOT_FOUND")); - return true; - } - - FObjectProperty* CompProp = - FindFProperty(WidgetBP->SkeletonGeneratedClass, FName(*SlotName)); - if (!CompProp) - { - Subsystem.SendAutomationError(RequestingSocket, RequestId, - FString::Printf(TEXT("Component variable '%s' not found on widget skeleton class"), *SlotName), - TEXT("COMPONENT_PROPERTY_NOT_FOUND")); - return true; - } - - UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(WidgetBP); - if (!EventGraph) - { - Subsystem.SendAutomationError(RequestingSocket, RequestId, TEXT("Event graph not found on widget blueprint"), TEXT("EVENT_GRAPH_NOT_FOUND")); - return true; - } - - // Idempotent: reuse an existing bound event for this delegate+component. - const UK2Node_ComponentBoundEvent* Existing = - FKismetEditorUtilities::FindBoundEventForComponent(WidgetBP, DelegateProp->GetFName(), CompProp->GetFName()); - - UK2Node_ComponentBoundEvent* BoundNode = const_cast(Existing); - bool bCreatedNew = false; - if (!BoundNode) - { - EventGraph->Modify(); - FGraphNodeCreator Creator(*EventGraph); - BoundNode = Creator.CreateNode(false); - BoundNode->InitializeComponentBoundEventParams(CompProp, DelegateProp); - Creator.Finalize(); - bCreatedNew = true; - bBlueprintChanged = true; - } - - // Only dirty + recompile when something actually changed — a repeat - // bind_on_clicked that reuses the existing node leaves the asset untouched. - bool bCompiled = true; - if (bBlueprintChanged) - { - FBlueprintEditorUtils::MarkBlueprintAsModified(WidgetBP); - bCompiled = McpSafeCompileBlueprint(WidgetBP); - } - - ResultJson->SetBoolField(TEXT("success"), true); - ResultJson->SetStringField(TEXT("slotName"), SlotName); - ResultJson->SetStringField(TEXT("eventType"), TEXT("OnClicked")); - // Response contract: functionName echoes the request input (default "OnButtonClicked"). - // The real handler is engine-generated -- returned as eventFunctionName - // (UK2Node_ComponentBoundEvent::CustomFunctionName). Reference the handler via that. - ResultJson->SetStringField(TEXT("functionName"), FunctionName); - ResultJson->SetBoolField(TEXT("bound"), true); - ResultJson->SetBoolField(TEXT("createdNew"), bCreatedNew); - ResultJson->SetBoolField(TEXT("compileSucceeded"), bCompiled); - ResultJson->SetStringField(TEXT("nodeId"), BoundNode->NodeGuid.ToString()); - ResultJson->SetStringField(TEXT("eventFunctionName"), BoundNode->CustomFunctionName.ToString()); - - Subsystem.SendAutomationResponse(RequestingSocket, RequestId, true, TEXT("OnClicked event bound"), ResultJson); - return true; + return BindComponentDelegateEvent(Subsystem, RequestId, RequestingSocket, ResultJson, + WidgetBP, ButtonWidget, SlotName, UButton::StaticClass(), FName(TEXT("OnClicked")), + TEXT("OnClicked"), FunctionName); } if (SubAction.Equals(TEXT("bind_on_hovered"), ESearchCase::IgnoreCase))