222 lines
7.1 KiB
C++
222 lines
7.1 KiB
C++
// 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<FRadialMenuEntry>& 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<URadialMenuWidget*>(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<FRadialMenuEntry>& 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<URadialSliceWidget>(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<UCanvasPanelSlot>(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);
|
|
}
|
|
}
|
|
} |