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..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 @@ -22,6 +22,105 @@ 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. 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, + 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(); + // 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; + } + + 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, @@ -63,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)) @@ -177,16 +200,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 +238,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))