// RadialMenuWidget.cpp #include "RadialMenuWidget.h" #include "RadialSliceWidget.h" #include "Components/CanvasPanel.h" #include "Components/CanvasPanelSlot.h" #include "Input/CommonUIInputTypes.h" // FBindUIActionArgs #include "InputCoreTypes.h" // EKeys::LeftMouseButton void URadialMenuWidget::InitializeFromController( URadialMenuController* InController, const TArray& InEntries) { OwningController = InController; BuildSlices(InEntries); } void URadialMenuWidget::NativeOnActivated() { Super::NativeOnActivated(); // Make sure the widget can actually hold focus; UCanvasPanel can't, so the // activatable widget itself is our focus target (see GetDesiredFocusTarget). SetIsFocusable(true); // Must be hit-testable for NativeOnMouseButtonDown (left-click confirm) to // fire. SelfHitTestInvisible/HitTestInvisible would let the click fall // through and never reach us. SetVisibility(ESlateVisibility::Visible); // Bind Confirm. FBindUIActionArgs accepts the row handle directly. // NOTE: Confirm must be mapped to a KEY (e.g. spacebar / face button), not // left mouse button. CommonUI routes left-click through its separate Default // Click Action path, so a click-mapped Confirm never reaches this binding. // To confirm via trackpad/left-click instead, handle the pointer click // directly (see NativeOnMouseButtonDown) rather than binding it here. if (!ConfirmAction.IsNull()) { FBindUIActionArgs ConfirmArgs( ConfirmAction, FSimpleDelegate::CreateUObject(this, &URadialMenuWidget::HandleConfirm)); // Show in the on-screen action bar so the player sees the prompt. ConfirmArgs.bDisplayInActionBar = true; ConfirmHandle = RegisterUIActionBinding(ConfirmArgs); } if (!BackAction.IsNull()) { FBindUIActionArgs BackArgs( BackAction, FSimpleDelegate::CreateUObject(this, &URadialMenuWidget::HandleBack)); BackArgs.bDisplayInActionBar = true; BackHandle = RegisterUIActionBinding(BackArgs); } // Reinforce: explicitly route focus to this widget for the owning player so // gamepad/keyboard input reaches the bindings even if the layer didn't. if (APlayerController* PC = GetOwningPlayer()) { SetUserFocus(PC); } } void URadialMenuWidget::NativeOnDeactivated() { // Clean up bindings so they don't linger if the widget is pooled/reused. if (ConfirmHandle.IsValid()) { ConfirmHandle.Unregister(); } if (BackHandle.IsValid()) { BackHandle.Unregister(); } Super::NativeOnDeactivated(); } UWidget* URadialMenuWidget::NativeGetDesiredFocusTarget() const { // A UCanvasPanel is NOT focusable by default, so returning it here would // silently fail and the bound actions wouldn't receive input. The radial // menu has no single "default" child to land on (there's no list to // highlight), so we focus the activatable widget itself, which participates // in focus correctly. Requires bIsFocusable = true (set in NativeConstruct). return const_cast(this); } FReply URadialMenuWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { // Left-click anywhere on the menu confirms the currently hovered slice. // The controller already tracks the hovered index each tick from the cursor // angle, so the click itself carries no position logic — it just triggers // the confirm. This bypasses CommonUI's Default Click Action entirely. if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { if (OwningController.IsValid()) { OwningController->CloseAndConfirm(); } // Handle the event so it doesn't fall through to the click system. return FReply::Handled(); } return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); } FReply URadialMenuWidget::NativeOnAnalogValueChanged(const FGeometry& InGeometry, const FAnalogInputEvent& InAnalogEvent) { // Analog events arrive one axis per event, so we cache the latest value of // each right-stick axis and recompute direction whenever either changes. const FKey Key = InAnalogEvent.GetKey(); bool bRightStick = false; if (Key == EKeys::Gamepad_RightX) { RightStickX = InAnalogEvent.GetAnalogValue(); bRightStick = true; } else if (Key == EKeys::Gamepad_RightY) { RightStickY = InAnalogEvent.GetAnalogValue(); bRightStick = true; } if (bRightStick && OwningController.IsValid()) { // Stick Y is +up already (unlike screen space), so pass it straight // through. The controller applies its own dead zone. const float Mag = FVector2D(RightStickX, RightStickY).Size(); OwningController->UpdateHoverFromDirection(RightStickX, RightStickY, Mag); // Consume so CommonUI's analog cursor doesn't also act on it. return FReply::Handled(); } return Super::NativeOnAnalogValueChanged(InGeometry, InAnalogEvent); } void URadialMenuWidget::HandleConfirm() { // Route to the controller, which owns the state, time dilation, and the // OnSelectionConfirmed broadcast. The controller also tears down the widget. if (OwningController.IsValid()) { OwningController->CloseAndConfirm(); } } void URadialMenuWidget::HandleBack() { if (OwningController.IsValid()) { OwningController->CloseAndCancel(); } } void URadialMenuWidget::BuildSlices(const TArray& InEntries) { if (!SliceCanvas || !SliceWidgetClass) { return; } SliceCanvas->ClearChildren(); Slices.Reset(); HoveredIndex = -1; const int32 Count = InEntries.Num(); if (Count == 0) { return; } const float AngleSize = 360.f / Count; for (int32 i = 0; i < Count; ++i) { URadialSliceWidget* Slice = CreateWidget(GetOwningPlayer(), SliceWidgetClass); if (!Slice) { continue; } const float StartAngle = i * AngleSize - AngleSize * 0.5f; Slice->Setup(SliceMaterial, StartAngle, AngleSize, InEntries[i].Icon, InEntries[i].bEnabled); SliceCanvas->AddChild(Slice); if (UCanvasPanelSlot* CanvasSlot = Cast(Slice->Slot)) { CanvasSlot->SetAnchors(FAnchors(0.5f, 0.5f)); CanvasSlot->SetAlignment(FVector2D(0.5f, 0.5f)); CanvasSlot->SetPosition(FVector2D::ZeroVector); CanvasSlot->SetSize(FVector2D(MenuDiameter, MenuDiameter)); } Slices.Add(Slice); } } void URadialMenuWidget::SetHoveredSegment(int32 Index) { HoveredIndex = Index; } void URadialMenuWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) { Super::NativeTick(MyGeometry, InDeltaTime); const float RealDelta = FApp::GetDeltaTime(); // undilated for (int32 i = 0; i < Slices.Num(); ++i) { if (Slices[i]) { Slices[i]->UpdateHighlight(i == HoveredIndex, RealDelta); } } }