Compare commits

..

2 Commits

Author SHA1 Message Date
26399a6a15 More work on managing input events 2026-04-16 00:13:48 -04:00
8a3d200247 First step of new focus management system 2026-04-15 22:55:02 -04:00
23 changed files with 407 additions and 14 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,112 @@
////////////////////////////////////////////////////////////
//
// InputEvents.cpp
//
////////////////////////////////////////////////////////////
#include "InputEvents.h"
#include "Common.h"
bool FlxEventRequest::operator==(const FlxEventRequest &Other) const
{
return (Widget == Other.Widget) &&
(UseUIOnly == Other.UseUIOnly) &&
(ShowPointer == Other.ShowPointer) &&
(Hotkeys == Other.Hotkeys);
}
bool FlxEventRequests::SanityCheck(const FlxEventRequest &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)
{
int32 NumHigh = 0;
while ((NumHigh < Requests.Num()) && (Requests[NumHigh].UseUIOnly)) NumHigh++;
int32 NumLow = Requests.Num() - NumHigh;
High = View(Requests.GetData(), NumHigh);
Low = View(Requests.GetData() + NumHigh, NumLow);
}
void FlxEventRequests::Request(const FlxEventRequest &NewRequest)
{
// 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 ((High.Num() > 0) && (High[0] == NewRequest)) return;
}
else
{
if ((Low.Num() > 0) && (Low[0] == NewRequest)) return;
}
// We're going to build a new version of the requests array.
TArray<FlxEventRequest> 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 (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 (Req.Widget != NewRequest.Widget) Updated.Add(Req);
Swap(Requests, Updated);
Dirty = true;
}
void FlxEventRequests::Remove(UUserWidget *Widget)
{
int32 N = Requests.Num();
Requests.RemoveAll([Widget](const FlxEventRequest &Entry)
{
return Entry.Widget == Widget;
});
if (Requests.Num() < N) Dirty = true;
}
void FlxEventRequests::GarbageCollect()
{
int32 N = Requests.Num();
Requests.RemoveAll([](const FlxEventRequest &Entry)
{
UUserWidget *W = Entry.Widget;
return W == nullptr || !IsValid(W) || W->GetParent() == nullptr;
});
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

@@ -0,0 +1,106 @@
////////////////////////////////////////////////////////////
//
// InputEvents.h
//
// Custom input event dispatching system. Uses Unreal's
// built-in input modes (GameOnly / UIOnly) with an
// enhanced input component for GameOnly mode hotkeys.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "Blueprint/UserWidget.h"
#include "InputEvents.generated.h"
////////////////////////////////////////////////////////////
//
// FlxEventRequest
//
// 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.
// 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.
//
////////////////////////////////////////////////////////////
USTRUCT(BlueprintType)
struct FlxEventRequest
{
GENERATED_BODY()
FlxEventRequest() = default;
FlxEventRequest(UUserWidget *InWidget, bool InUseUIOnly, bool InShowPointer, const TArray<FKey> &InHotkeys)
: Widget(InWidget), UseUIOnly(InUseUIOnly), ShowPointer(InShowPointer), Hotkeys(InHotkeys) {}
bool operator == (const FlxEventRequest &Other) const;
UPROPERTY(BlueprintReadWrite)
UUserWidget* Widget = nullptr;
UPROPERTY(BlueprintReadWrite)
bool UseUIOnly = false;
UPROPERTY(BlueprintReadWrite)
bool ShowPointer = false;
UPROPERTY(BlueprintReadWrite)
TArray<FKey> Hotkeys;
};
USTRUCT()
struct FlxEventRequests
{
GENERATED_BODY()
private:
UPROPERTY()
// High priority requests are always before low-priority.
// Otherwise, these are in order of most recent first.
TArray<FlxEventRequest> Requests;
UPROPERTY()
bool Dirty = true;
public:
enum class InputMode { UIOnly, GameOnly };
using View = TArrayView<FlxEventRequest>;
// Get the requests array.
const TArray<FlxEventRequest> &GetRequests() const { return Requests; }
// Sanity check a request to see if it is reasonable.
static bool SanityCheck(const FlxEventRequest &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);
// Remove all requests by the specified widget.
void Remove(UUserWidget *Widget);
// Remove any requests by dead widgets or widgets with no parents.
void GarbageCollect();
// Return true if the requests have changed since the last ClearDirty.
bool IsDirty() { return Dirty; }
// Clear the dirty flag.
void ClearDirty() { Dirty = false; }
// Set the dirty flag.
void SetDirty() { Dirty = true; }
// Get the currently-requested mode.
InputMode GetRequestedMode() const;
};

View File

@@ -169,7 +169,11 @@ void ALuprexGameModeBase::OnWorldPostActorTick(UWorld* InWorld, ELevelTick InLev
UpdateTangibles();
UpdatePossessedTangible();
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(GetWorld()->GetFirstPlayerController());
if (PC != nullptr) PC->UpdateLookAt();
if (PC != nullptr)
{
PC->UpdateLookAt();
PC->UpdateEventDispatch();
}
}
}

View File

@@ -4,6 +4,24 @@
#include "TangibleManager.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/SViewport.h"
#include "Slate/SObjectWidget.h"
FString AlxPlayerControllerBase::GetUserWidgetName(SWidget *W)
{
while (W)
{
if (W->GetType() == FName("SObjectWidget"))
{
UUserWidget *UW = static_cast<SObjectWidget*>(W)->GetWidgetObject();
if (UW) return UW->GetClass()->GetName();
}
W = W->GetParentWidget().Get();
}
return TEXT("Unknown Widget");
}
AlxPlayerControllerBase *AlxPlayerControllerBase::FromContext(const UObject *Context)
{
@@ -53,6 +71,96 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
return ScreenPosition;
}
void AlxPlayerControllerBase::BeginPlay()
{
Super::BeginPlay();
HotkeyInputComponent = NewObject<UInputComponent>(this);
HotkeyInputComponent->bBlockInput = false;
PushInputComponent(HotkeyInputComponent);
}
void AlxPlayerControllerBase::UpdateEventDispatch()
{
EventRequests.GarbageCollect();
// If we're in GameOnly mode, check that focus is still on the viewport.
if (CurrentInputMode == InputMode::GameOnly)
{
UGameViewportClient *GVC = GetWorld() ? GetWorld()->GetGameViewport() : nullptr;
if (GVC)
{
TSharedPtr<SViewport> ViewportWidget = GVC->GetGameViewportWidget();
if (ViewportWidget.IsValid())
{
TSharedPtr<SWidget> Focused = FSlateApplication::Get().GetKeyboardFocusedWidget();
if (Focused.Get() != ViewportWidget.Get())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, keyboard focus must stay on viewport, but was stolen by: %s. Restoring."), *GetUserWidgetName(Focused.Get()));
EventRequests.SetDirty();
}
if (!ViewportWidget->HasMouseCapture())
{
UE_LOG(LogLuprexIntegration, Error, TEXT("In GameOnly mode, viewport must have mouse capture, but lost it. Restoring."));
EventRequests.SetDirty();
}
}
}
}
if (!EventRequests.IsDirty()) return;
EventRequests.ClearDirty();
CurrentInputMode = EventRequests.GetRequestedMode();
const TArray<FlxEventRequest> &Requests = EventRequests.GetRequests();
if (CurrentInputMode == InputMode::UIOnly)
{
SetInputMode(FInputModeUIOnly().SetWidgetToFocus(Requests[0].Widget->GetCachedWidget()));
}
else
{
SetInputMode(FInputModeGameOnly());
HotkeyInputComponent->KeyBindings.Empty();
TSet<FKey> BoundKeys;
for (const FlxEventRequest &Req : Requests)
{
for (const FKey &Key : Req.Hotkeys)
{
if (!BoundKeys.Contains(Key))
{
BoundKeys.Add(Key);
HotkeyInputComponent->BindKey(Key, IE_Pressed, this, &AlxPlayerControllerBase::ForwardKeyEvent);
}
}
}
}
}
void AlxPlayerControllerBase::ForwardKeyEvent(FKey Key)
{
// TODO: implement
}
void AlxPlayerControllerBase::RequestEvents(const FlxEventRequest &Request)
{
if (!FlxEventRequests::SanityCheck(Request)) return;
AlxPlayerControllerBase *PC = FromContext(Request.Widget);
PC->EventRequests.Request(Request);
}
void AlxPlayerControllerBase::UnRequestEvents(UUserWidget *Widget)
{
if (Widget == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("UnRequestEvents called with null widget."));
return;
}
AlxPlayerControllerBase *PC = FromContext(Widget);
PC->EventRequests.Remove(Widget);
}
void AlxPlayerControllerBase::UpdateLookAt()
{
UlxTangibleManager *TM = GetGameInstance()->GetSubsystem<UlxTangibleManager>();

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)
@@ -11,6 +12,8 @@ class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
GENERATED_BODY()
public:
using InputMode = FlxEventRequests::InputMode;
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
static void SetLookAt(const UObject *Context, const FHitResult &HitResult);
@@ -26,6 +29,12 @@ public:
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Look-At Detection")
static void SetLookAtChanged(const UObject *Context);
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
static void RequestEvents(const FlxEventRequest &Request);
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Events")
static void UnRequestEvents(UUserWidget *Widget);
// Blueprint events
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
void CalculateLookAt();
@@ -33,14 +42,35 @@ public:
UFUNCTION(BlueprintImplementableEvent, Category = "Luprex|Look-At Detection")
void LookAtChanged();
virtual void BeginPlay() override;
// Called by GameMode each tick.
void UpdateLookAt();
// Rebuild input component and switch input mode.
void UpdateEventDispatch();
// Handler for GameOnly mode hotkey presses.
void ForwardKeyEvent(FKey Key);
// Walk up from a Slate widget to find the nearest UMG widget class name.
static FString GetUserWidgetName(SWidget *W);
// Get the player controller, cast to AlxPlayerControllerBase.
static AlxPlayerControllerBase *FromContext(const UObject *Context);
UPROPERTY()
FHitResult CurrentLookAt;
UPROPERTY()
FlxEventRequests EventRequests;
// Input component for GameOnly mode: catches hotkeys only.
UPROPERTY()
UInputComponent *HotkeyInputComponent = nullptr;
// Current input mode.
InputMode CurrentInputMode = InputMode::GameOnly;
bool MustCallLookAtChanged = false;
};