Much work on input mode switching

This commit is contained in:
2026-04-17 23:43:28 -04:00
parent 7a09da8a4e
commit 6388de9b39
12 changed files with 251 additions and 101 deletions

View File

@@ -7,52 +7,44 @@
#include "InputEvents.h"
#include "Common.h"
bool FlxEventRequest::operator==(const FlxEventRequest &Other) const
bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const
{
return (Widget == Other.Widget) &&
(UseUIOnly == Other.UseUIOnly) &&
(Focus == Other.Focus) &&
(ShowPointer == Other.ShowPointer) &&
(Hotkeys == Other.Hotkeys);
(BlockInput == Other.BlockInput);
}
bool FlxEventRequests::SanityCheck(const FlxEventRequest &Request)
bool FlxInputModeRequests::SanityCheck(const FlxInputModeRequest &Request)
{
if (Request.Widget == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget."));
return false;
}
if (Request.ShowPointer && !Request.UseUIOnly)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: ShowPointer requires UseUIOnly."));
return false;
}
if (Request.UseUIOnly && !Request.Hotkeys.IsEmpty())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents: Widget asked for all events, and also, specific events"));
return false;
}
return true;
}
void FlxEventRequests::SplitHighLow(View &High, View &Low)
void FlxInputModeRequests::SplitHighLow(View &High, View &Low)
{
int32 NumHigh = 0;
while ((NumHigh < Requests.Num()) && (Requests[NumHigh].UseUIOnly)) NumHigh++;
while ((NumHigh < Requests.Num()) && (Requests[NumHigh].IsHighPrio())) NumHigh++;
int32 NumLow = Requests.Num() - NumHigh;
High = View(Requests.GetData(), NumHigh);
Low = View(Requests.GetData() + NumHigh, NumLow);
}
void FlxEventRequests::Request(const FlxEventRequest &NewRequest)
void FlxInputModeRequests::Request(const FlxInputModeRequest &NewRequest)
{
bool IsHigh = NewRequest.IsHighPrio();
// Divide the array into a high-priority slice and a low-priority slice.
View High, Low;
SplitHighLow(High, Low);
// This is a simple test to see if anything is going to change.
// If not, we return early and avoid setting the dirty bit.
if (NewRequest.UseUIOnly)
if (IsHigh)
{
if ((High.Num() > 0) && (High[0] == NewRequest)) return;
}
@@ -62,36 +54,45 @@ void FlxEventRequests::Request(const FlxEventRequest &NewRequest)
}
// We're going to build a new version of the requests array.
TArray<FlxEventRequest> Updated;
TArray<FlxInputModeRequest> Updated;
// Add all high priority requests to the updated array, new request first.
if (NewRequest.UseUIOnly) Updated.Add(NewRequest);
for (const FlxEventRequest &Req : High)
if (IsHigh) Updated.Add(NewRequest);
for (const FlxInputModeRequest &Req : High)
if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
// Add all low priority requests to the updated array, new request first.
if (!NewRequest.UseUIOnly) Updated.Add(NewRequest);
for (const FlxEventRequest &Req : Low)
if (!IsHigh) Updated.Add(NewRequest);
for (const FlxInputModeRequest &Req : Low)
if (Req.Widget != NewRequest.Widget) Updated.Add(Req);
Swap(Requests, Updated);
Dirty = true;
}
void FlxEventRequests::Remove(UUserWidget *Widget)
void FlxInputModeRequests::EnsureWidget(UUserWidget *Widget)
{
for (const FlxInputModeRequest &Req : Requests)
{
if (Req.Widget == Widget) return;
}
Request(FlxInputModeRequest(Widget, nullptr, false, false));
}
void FlxInputModeRequests::Remove(UUserWidget *Widget)
{
int32 N = Requests.Num();
Requests.RemoveAll([Widget](const FlxEventRequest &Entry)
Requests.RemoveAll([Widget](const FlxInputModeRequest &Entry)
{
return Entry.Widget == Widget;
});
if (Requests.Num() < N) Dirty = true;
}
void FlxEventRequests::GarbageCollect()
void FlxInputModeRequests::GarbageCollect()
{
int32 N = Requests.Num();
Requests.RemoveAll([](const FlxEventRequest &Entry)
Requests.RemoveAll([](const FlxInputModeRequest &Entry)
{
UUserWidget *W = Entry.Widget;
return W == nullptr || !IsValid(W) || W->GetParent() == nullptr;
@@ -99,14 +100,3 @@ void FlxEventRequests::GarbageCollect()
if (Requests.Num() < N) Dirty = true;
}
FlxEventRequests::InputMode FlxEventRequests::GetRequestedMode() const
{
if ((Requests.Num() > 0) && (Requests[0].UseUIOnly))
{
return InputMode::UIOnly;
}
else
{
return InputMode::GameOnly;
}
}

View File

@@ -11,52 +11,55 @@
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "Blueprint/UserWidget.h"
#include "InputEvents.generated.h"
////////////////////////////////////////////////////////////
//
// FlxEventRequest
// FlxInputModeRequest
//
// A widget's declaration of interest in input events.
//
// Widget: The widget that wants to receive events.
// UseUIOnly: If true, activating this request puts
// the system into UIOnly mode.
// Focus: The widget that should receive keyboard focus
// while this request is active.
// ShowPointer: If true, the mouse pointer should be
// visible when this widget has control.
// Hotkeys: Keys that go to this widget when the
// player is in GameOnly mode.
// BlockInput: If true, input actions should be blocked from
// reaching lower-priority consumers (e.g. the pawn).
//
////////////////////////////////////////////////////////////
USTRUCT(BlueprintType)
struct FlxEventRequest
struct FlxInputModeRequest
{
GENERATED_BODY()
FlxEventRequest() = default;
FlxEventRequest(UUserWidget *InWidget, bool InUseUIOnly, bool InShowPointer, const TArray<FKey> &InHotkeys)
: Widget(InWidget), UseUIOnly(InUseUIOnly), ShowPointer(InShowPointer), Hotkeys(InHotkeys) {}
FlxInputModeRequest() = default;
FlxInputModeRequest(UUserWidget *InWidget, UWidget *InFocus, bool InShowPointer, bool InBlockInput)
: Widget(InWidget), Focus(InFocus), ShowPointer(InShowPointer), BlockInput(InBlockInput) {}
bool operator == (const FlxEventRequest &Other) const;
bool operator == (const FlxInputModeRequest &Other) const;
// True if this request wants any high-priority resource:
// keyboard focus, the mouse pointer, or input blocking.
bool IsHighPrio() const { return Focus != nullptr || ShowPointer || BlockInput; }
UPROPERTY(BlueprintReadWrite)
UUserWidget* Widget = nullptr;
UPROPERTY(BlueprintReadWrite)
bool UseUIOnly = false;
UWidget* Focus = nullptr;
UPROPERTY(BlueprintReadWrite)
bool ShowPointer = false;
UPROPERTY(BlueprintReadWrite)
TArray<FKey> Hotkeys;
bool BlockInput = false;
};
USTRUCT()
struct FlxEventRequests
struct FlxInputModeRequests
{
GENERATED_BODY()
@@ -64,27 +67,29 @@ private:
UPROPERTY()
// High priority requests are always before low-priority.
// Otherwise, these are in order of most recent first.
TArray<FlxEventRequest> Requests;
TArray<FlxInputModeRequest> Requests;
UPROPERTY()
bool Dirty = true;
public:
enum class InputMode { UIOnly, GameOnly };
using View = TArrayView<FlxEventRequest>;
using View = TArrayView<FlxInputModeRequest>;
// Get the requests array.
const TArray<FlxEventRequest> &GetRequests() const { return Requests; }
const TArray<FlxInputModeRequest> &GetRequests() const { return Requests; }
// Sanity check a request to see if it is reasonable.
static bool SanityCheck(const FlxEventRequest &Request);
static bool SanityCheck(const FlxInputModeRequest &Request);
// Divide Requests into a high-priority slice and a low-priority slice.
void SplitHighLow(View &High, View &Low);
// Apply a request. Replaces any previous request by the same widget.
void Request(const FlxEventRequest &NewRequest);
void Request(const FlxInputModeRequest &NewRequest);
// Ensure the specified widget has a request in the array.
// If it isn't already present, register a low-priority request for it.
void EnsureWidget(UUserWidget *Widget);
// Remove all requests by the specified widget.
void Remove(UUserWidget *Widget);
@@ -100,7 +105,4 @@ public:
// Set the dirty flag.
void SetDirty() { Dirty = true; }
// Get the currently-requested mode.
InputMode GetRequestedMode() const;
};

View File

@@ -172,6 +172,7 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev
if (PC != nullptr)
{
PC->UpdateLookAt();
PC->UpdateInputMode();
}
}
}

View File

@@ -2,10 +2,14 @@
#include "Common.h"
#include "Tangible.h"
#include "TangibleManager.h"
#include "Blueprint/UserWidget.h"
#include "Components/InputComponent.h"
#include "Engine/LevelScriptActor.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h"
#include "Engine/LevelScriptActor.h"
#include "Engine/LocalPlayer.h"
#include "Kismet/GameplayStatics.h"
#include "Widgets/SViewport.h"
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
{
@@ -55,10 +59,53 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
return ScreenPosition;
}
UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *Widget)
{
if (!IsValid(Widget)) return nullptr;
// Cache the FProperty on first call. FProperties are owned by
// the native UUserWidget UClass, which lives for the process
// lifetime, so the pointer stays valid without us needing to
// root anything against GC. Static local init is thread-safe.
static FObjectProperty *InputComponentProp = FindFProperty<FObjectProperty>(
UUserWidget::StaticClass(), TEXT("InputComponent"));
check(InputComponentProp);
UObject *Value = InputComponentProp->GetObjectPropertyValue_InContainer(Widget);
return Cast<UInputComponent>(Value);
}
void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool ShowPointer, bool BlockInput, UWidget *Focus)
{
if (!IsValid(Widget))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("WidgetRequestInputMode called with an invalid widget."));
return;
}
APlayerController *OwningPC = Widget->GetOwningPlayer();
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(OwningPC);
if (PC == nullptr)
{
UE_LOG(LogLuprexIntegration, Error,
TEXT("WidgetRequestInputMode: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
*Widget->GetName(), *GetNameSafe(OwningPC));
return;
}
PC->InputModeRequests.Request(FlxInputModeRequest(Widget, Focus, ShowPointer, BlockInput));
}
void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent)
{
if (InInputComponent)
{
// Widgets don't go on the current input stack. Instead, they're
// tracked in InputModeRequests, where the PlayerController can arbitrate
// focus, pointer visibility, and input blocking across them.
if (UUserWidget *Widget = Cast<UUserWidget>(InInputComponent->GetOuter()))
{
InputModeRequests.EnsureWidget(Widget);
return;
}
CurrentInputStack.RemoveSingle(InInputComponent);
CurrentInputStack.Add(InInputComponent);
}
@@ -121,31 +168,103 @@ void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputSta
InputStack.Push(InputComponent);
}
// Sort the components first by bBlockInput, then by
// priority. We don't touch the original array, because
// we want to preserve information about which component
// was pushed last.
TArray<UInputComponent*, TInlineAllocator<20>> Pushed;
for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx)
// The current input stack is unlikely to have anything,
// given that we've moved widgets to their own mechanism.
// But, if there's anything here, sort it and push it.
if (!CurrentInputStack.IsEmpty())
{
UInputComponent* IC = CurrentInputStack[Idx].Get();
if (IsValid(IC))
TArray<UInputComponent*, TInlineAllocator<20>> Pushed;
for (int32 Idx = 0; Idx < CurrentInputStack.Num(); ++Idx)
{
Pushed.Add(IC);
UInputComponent* IC = CurrentInputStack[Idx].Get();
if (IsValid(IC)) Pushed.Add(IC);
else CurrentInputStack.RemoveAt(Idx--);
}
else
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B)
{
CurrentInputStack.RemoveAt(Idx--);
}
if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput;
return A.Priority < B.Priority;
});
InputStack.Append(Pushed);
}
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B)
// Push the widget input components from InputModeRequests. Requests
// are ordered high-priority first; the input stack is processed
// from top (last index) down, so we push back-to-front: low
// priority first, high priority last (ending up on top).
const TArray<FlxInputModeRequest> &Requests = InputModeRequests.GetRequests();
for (int32 Idx = Requests.Num() - 1; Idx >= 0; --Idx)
{
if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput;
return A.Priority < B.Priority;
});
if (UInputComponent *IC = GetWidgetInputComponent(Requests[Idx].Widget))
{
IC->bBlockInput = Requests[Idx].BlockInput;
InputStack.Push(IC);
}
}
}
InputStack.Append(Pushed);
void AlxPlayerControllerBase::UpdateInputMode()
{
InputModeRequests.GarbageCollect();
// Get all the various objects we need to be able to manipulate
// the input mode.
UGameViewportClient *GameViewportClient = GetWorld()->GetGameViewport();
ULocalPlayer *LocalPlayer = Cast<ULocalPlayer>(Player);
if (GameViewportClient == nullptr || LocalPlayer == nullptr) return;
TSharedPtr<SViewport> ViewportWidget = GameViewportClient->GetGameViewportWidget();
if (!ViewportWidget.IsValid()) return;
TSharedRef<SViewport> ViewportWidgetRef = ViewportWidget.ToSharedRef();
FReply &SlateOperations = LocalPlayer->GetSlateOperations();
// The first entry in InputModeRequests (highest priority) dictates the
// pointer / capture / focus state. If there are no requests at all,
// fall back to a static default-constructed request.
static const FlxInputModeRequest EmptyRequest;
const TArray<FlxInputModeRequest> &Requests = InputModeRequests.GetRequests();
const FlxInputModeRequest &Top = Requests.IsEmpty() ? EmptyRequest : Requests[0];
SetShowMouseCursor(Top.ShowPointer);
if (Top.ShowPointer)
{
// Pointer-visible mode: full macroexpansion of SetInputModeGameAndUI
// — the BP wrapper, APlayerController::SetInputMode, and
// FInputModeGameAndUI::ApplyInputMode all inlined. Defaults:
// MouseLockMode=DoNotLock, WidgetToFocus=null, bHideCursorDuringCapture=true.
SlateOperations.ReleaseMouseLock();
SlateOperations.ReleaseMouseCapture();
GameViewportClient->SetMouseLockMode(EMouseLockMode::DoNotLock);
GameViewportClient->SetHideCursorDuringCapture(false);
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CaptureDuringMouseDown);
}
else
{
// Pointer-invisible mode: full macroexpansion of SetInputModeGameOnly
// — the BP wrapper, APlayerController::SetInputMode, and
// FInputModeGameOnly::ApplyInputMode all inlined.
SlateOperations.UseHighPrecisionMouseMovement(ViewportWidgetRef);
SlateOperations.LockMouseToWidget(ViewportWidgetRef);
GameViewportClient->SetMouseLockMode(EMouseLockMode::LockOnCapture);
GameViewportClient->SetMouseCaptureMode(EMouseCaptureMode::CapturePermanently);
}
// UWidget holds its live SWidget in a cached TSharedPtr; we need a
// TSharedRef for SetUserFocus. Fall back to the viewport if the
// target has never been constructed (cached widget is null).
TSharedPtr<SWidget> SlateFocus = Top.Focus ? Top.Focus->GetCachedWidget() : nullptr;
if (SlateFocus.IsValid())
{
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
}
else
{
SlateOperations.SetUserFocus(ViewportWidgetRef);
}
GameViewportClient->SetIgnoreInput(false);
}
void AlxPlayerControllerBase::UpdateLookAt()

View File

@@ -3,6 +3,7 @@
#include "CoreMinimal.h"
#include "Engine/HitResult.h"
#include "GameFramework/PlayerController.h"
#include "InputEvents.h"
#include "PlayerControllerBase.generated.h"
UCLASS(BlueprintType, Blueprintable)
@@ -36,16 +37,35 @@ public:
// Called by GameMode each tick.
void UpdateLookAt();
// Called by GameMode each tick. GCs dead requests and will
// eventually reconcile focus, pointer, and capture state.
void UpdateInputMode();
// Input stack overrides: unsorted, append-on-push.
virtual void PushInputComponent(UInputComponent* InInputComponent) override;
virtual bool PopInputComponent(UInputComponent* InInputComponent) override;
virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override;
// Read UUserWidget::InputComponent via reflection. The field is
// protected and has no public accessor; this reaches through the
// FProperty so we always see the current value without caching it.
static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget);
// Blueprint-facing entry point. Looks like a method on UUserWidget
// (thanks to DefaultToSelf + HideSelfPin): the widget self-binds,
// we find its owning PlayerController, and register the request.
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode",
meta = (DefaultToSelf = "Widget", HideSelfPin = "true"))
static void WidgetRequestInputMode(class UUserWidget *Widget, bool ShowPointer, bool BlockInput, class UWidget *Focus);
// Get the player controller, cast to AlxPlayerControllerBase.
static AlxPlayerControllerBase *FromContext(const UObject *Context);
UPROPERTY()
FHitResult CurrentLookAt;
UPROPERTY()
FlxInputModeRequests InputModeRequests;
bool MustCallLookAtChanged = false;
};