Compare commits

..

2 Commits

Author SHA1 Message Date
4420c52b74 Working on new root canvas stuff 2026-04-21 22:28:22 -04:00
8e5d43fd24 Working on new root canvas stuff 2026-04-21 21:26:06 -04:00
14 changed files with 527 additions and 332 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.

View File

@@ -0,0 +1,129 @@
# Keyboard Focus and Input Modes
Luprex provides a "window management system" that takes full ownership of: keyboard focus, mouse capture, pointer visibility, enhanced input routing, and input mode. Top-level widgets don't call `AddToViewport` or configure input modes directly — instead, they are added to a single root canvas, and the player controller arbitrates the per-tick state based on which widget is in front.
## Key types
- **`AlxPlayerControllerBase`** — owns the top-level `RootWidget`, builds the input stack, and reconciles focus / capture / pointer state every tick.
- **`UlxRootCanvasPanel`** — the one `UCanvasPanel` subclass that lives inside `RootWidget`. Every top-level UI widget in the game is a child of this canvas.
- **`UlxRootCanvasSlot`** — the slot type used for children of `UlxRootCanvasPanel`. Extends `UCanvasPanelSlot` with input-mode fields. A widget's slot carries **both** layout config (anchors, offsets, ZOrder) **and** its window-management declarations.
These types collapse what would otherwise be two parallel systems (layout + input-mode requests) into one. Adding a widget to the canvas = declaring its window-management intent.
## The core rule: ZOrder is authoritative
**The widget with the highest ZOrder wins everything.** It dictates:
- Whether the mouse pointer is shown.
- Whether the viewport captures the mouse.
- Which widget (or sub-widget) receives keyboard focus.
- Which input components end up on top of the input stack.
Draw order and input priority are the same fact. A widget you can see on top is the one that gets input.
## Slot fields (`UlxRootCanvasSlot`)
Inherited from `UCanvasPanelSlot`:
- **`ZOrder`** (int32) — Higher = drawn on top / higher input priority. Can be updated at runtime via `Slot->SetZOrder(N)`.
- **`Anchors`, `Offsets`, `Alignment`** — standard UMG canvas layout.
Added by `UlxRootCanvasSlot`:
- **`ShowPointer`** (bool) — When this widget is in front, the mouse cursor is visible and the viewport releases capture. When false, the cursor is hidden and the viewport captures the mouse (game-style input).
- **`BlockInput`** (bool) — When this widget's input component is pushed onto the stack, it blocks lower-priority components from receiving events.
- **`EnableEnhancedInput`** (bool, default true) — Whether this widget's `UInputComponent` is included on the input stack at all. Corresponds to "Enhanced Input events fire on this widget."
One property that lives on the **widget** rather than the slot:
- **`UUserWidget::DesiredFocusWidget`** — the sub-widget to receive keyboard focus when this widget is in front. Null means "focus the viewport."
## API
### Adding a widget
```cpp
// Static, BP-callable with DefaultToSelf + HideSelfPin,
// so in Blueprint it looks like a method on UUserWidget.
AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget);
```
Adds the widget as a child of the player controller's `RootCanvas` at ZOrder 0 with default flags. If the widget is already a child, does nothing.
### Updating a widget's settings
```cpp
UlxRootCanvasPanel::SetWidgetWindowManagement(
UUserWidget *Widget,
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput,
bool BringToFront, UWidget *DesiredFocusWidget);
```
Updates an already-added widget's slot. Writes all the input-mode flags, sets `DesiredFocusWidget`, and optionally bumps ZOrder to `GetMaxZOrder() + 1` if `BringToFront` is true. Logs an error if the widget isn't yet a root widget.
### Forcing a focus re-apply
```cpp
AlxPlayerControllerBase::RestoreFocusToFrontWidget(const UObject *Context);
```
Clears `LastWidgetGrantedFocus`, which causes the next tick of `UpdateInputMode` to re-apply focus based on the current front widget. Use after mutating `DesiredFocusWidget` or when a modal is dismissed and focus should return to the underlying widget.
## Per-tick flow
On every tick, the player controller runs two passes:
### `BuildInputStack`
Assembles the list of `UInputComponent`s that will receive input events this frame:
1. Controlled pawn's input component(s).
2. Level script actors.
3. The player controller's own input component.
4. `CurrentInputStack` (anything pushed via `PushInputComponent`, filtered to exclude input components already managed by a root canvas slot).
5. Root canvas widget input components, pushed in ZOrder-ascending order (so highest ZOrder ends up on top of the stack, which gets processed first).
Each widget-managed IC has its `bBlockInput` flag set from its slot's `BlockInput` before being pushed.
### `UpdateInputMode`
Reads the front widget's slot and applies the declared state:
1. Sorts the canvas slots by ZOrder; picks the front widget.
2. Calls `SetShowMouseCursor(Slot->ShowPointer)`.
3. Configures viewport capture mode based on `ShowPointer`:
- **Pointer on**: releases capture (only if held), unlocks mouse, sets `CaptureDuringMouseDown`.
- **Pointer off**: high-precision mouse movement, locks mouse to viewport, `CapturePermanently`.
4. Resolves focus:
- If `!ShowPointer`, re-applies focus every tick (keeps stealing focus back from anything that steals it).
- If `ShowPointer` and the front widget (identity) hasn't changed since the last grant, lets click-to-focus take over.
- Otherwise grants focus once to the widget's `DesiredFocusWidget` (or the viewport if none).
## Focus arbitration
`LastWidgetGrantedFocus` (a `TObjectKey<UWidget>`) tracks the last focus target the system applied. The comparison is identity-based: if the current front widget's `DesiredFocusWidget` differs from `LastWidgetGrantedFocus`, focus is re-granted.
To force a re-grant without a widget change (e.g., after mutating `DesiredFocusWidget`), call `RestoreFocusToFrontWidget`. This sets the key to null, so the next tick's comparison will always re-grant.
## Click-to-focus
When the user clicks on a focusable widget, the `LuprexViewportClient` notifies the player controller via `ClickToFocus(Widget)`. On the next `UpdateInputMode` tick, if the system isn't going to re-grant focus for other reasons, it forwards focus to the clicked widget.
This only takes effect in pointer mode. In non-pointer mode, focus is continuously re-applied to the front widget's `DesiredFocusWidget`, overriding click behavior.
## What NOT to do
- **Don't call `AddToViewport` on top-level game widgets.** All top-level widgets must be children of `RootCanvas`. Going directly to the viewport bypasses the entire system.
- **Don't set input mode via `SetInputModeGameOnly` / `SetInputModeUIOnly` / `SetInputModeGameAndUI`.** The window management system owns these. Adjust `ShowPointer` / `BlockInput` on the slot instead.
- **Don't call `SetShowMouseCursor` directly.** Same reason — driven by the front widget's `ShowPointer`.
- **Don't set focus manually.** Set `DesiredFocusWidget` on the widget; the system will pick it up.
## Design notes
**Why slot fields, not separate requests?** Earlier versions of this system kept a parallel `FlxInputModeRequests` array of per-widget state. Tying that to slot lifecycle (widget added = entry exists; widget removed = entry gone) eliminated a whole class of stale-entry and lifecycle bugs.
**Why ZOrder as the priority key?** The alternative is a separate priority field, which can diverge from visual order. Using ZOrder means "the widget you see on top is the one with input priority" is true by construction, not by convention.
**Why identity-based focus invalidation?** Tracking a sequence number worked but required callers to call an explicit re-request API whenever `DesiredFocusWidget` changed. Using `TObjectKey<UWidget>` lets the system notice any change in the intended focus target automatically.
**Why `ShowPointer` and not a whole `EInputMode` enum?** Two flags cover the behaviors that actually matter (`ShowPointer`, `BlockInput`). A three-state enum with `GameOnly` / `UIOnly` / `GameAndUI` would force callers into categories that don't map cleanly to how this system layers widgets.

View File

@@ -1,117 +0,0 @@
////////////////////////////////////////////////////////////
//
// InputModeRequest.cpp
//
////////////////////////////////////////////////////////////
#include "InputModeRequest.h"
#include "Common.h"
bool FlxInputModeRequest::operator<(const FlxInputModeRequest &Other) const
{
// The highest priority request goes to the front of the array.
// Therefore, in this context, 'less than' means 'higher priority'.
// It's a little confusing.
if (ShowPointer != Other.ShowPointer) return ShowPointer > Other.ShowPointer;
return SequenceNumber > Other.SequenceNumber;
}
bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const
{
return (Widget == Other.Widget) &&
(Focus == Other.Focus) &&
(ShowPointer == Other.ShowPointer) &&
(BlockInput == Other.BlockInput) &&
(EnableInputComponent == Other.EnableInputComponent);
}
bool FlxInputModeRequests::SanityCheck(const FlxInputModeRequest &Request)
{
if (Request.Widget == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("RequestEvents called with null widget."));
return false;
}
return true;
}
int32 FlxInputModeRequests::FindWidget(UUserWidget *Widget)
{
for (const FlxInputModeRequest &Req : Requests)
{
if (Req.Widget == Widget) return &Req - Requests.GetData();
}
return Requests.Num();
}
void FlxInputModeRequests::BubbleItem(int32 Index)
{
while ((Index > 0) && (Requests[Index] < Requests[Index - 1]))
{
Swap(Requests[Index], Requests[Index - 1]);
--Index;
}
while ((Index < Requests.Num() - 1) && (Requests[Index + 1] < Requests[Index]))
{
Swap(Requests[Index], Requests[Index + 1]);
++Index;
}
}
void FlxInputModeRequests::Request(const FlxInputModeRequest &NewRequest, bool UpdateSequence)
{
int32 Index = FindWidget(NewRequest.Widget);
if (Index == Requests.Num())
{
Requests.Emplace(NewRequest);
Requests[Index].SequenceNumber = ++NextSequenceNumber;
}
else
{
int32 SequenceNumber = Requests[Index].SequenceNumber;
if (UpdateSequence) SequenceNumber = ++NextSequenceNumber;
Requests[Index] = NewRequest;
Requests[Index].SequenceNumber = SequenceNumber;
}
BubbleItem(Index);
}
void FlxInputModeRequests::SetEnableInputComponent(UUserWidget *Widget, bool EnableInputComponent)
{
int32 Index = FindWidget(Widget);
if (Index == Requests.Num())
{
FlxInputModeRequest NewReq;
NewReq.Widget = Widget;
NewReq.EnableInputComponent = EnableInputComponent;
NewReq.SequenceNumber = ++NextSequenceNumber;
Requests.Emplace(NewReq);
}
else
{
Requests[Index].EnableInputComponent = EnableInputComponent;
}
BubbleItem(Index);
}
void FlxInputModeRequests::Remove(UUserWidget *Widget)
{
Requests.RemoveAll([Widget](const FlxInputModeRequest &Entry)
{
return Entry.Widget == Widget;
});
}
void FlxInputModeRequests::GarbageCollect()
{
Requests.RemoveAll([](const FlxInputModeRequest &Entry)
{
return !IsValid(Entry.Widget);
});
}

View File

@@ -1,98 +0,0 @@
////////////////////////////////////////////////////////////
//
// InputModeRequest.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 "Blueprint/UserWidget.h"
#include "InputModeRequest.generated.h"
////////////////////////////////////////////////////////////
//
// FlxInputModeRequest
//
// Using this struct, a Widget can express a need for a
// particular input mode. These requests go to the player
// controller, which arbitrates.
//
////////////////////////////////////////////////////////////
USTRUCT(BlueprintType)
struct FlxInputModeRequest
{
GENERATED_BODY()
FlxInputModeRequest() = default;
bool operator == (const FlxInputModeRequest &Other) const;
bool operator < (const FlxInputModeRequest &Other) const;
UPROPERTY(BlueprintReadWrite)
UUserWidget* Widget = nullptr;
UPROPERTY(BlueprintReadWrite)
UWidget* Focus = nullptr;
UPROPERTY(BlueprintReadWrite)
bool ShowPointer = false;
UPROPERTY(BlueprintReadWrite)
bool BlockInput = false;
UPROPERTY(BlueprintReadWrite)
bool EnableInputComponent = true;
UPROPERTY()
int32 SequenceNumber = 0;
};
USTRUCT()
struct FlxInputModeRequests
{
GENERATED_BODY()
private:
UPROPERTY()
// Sorted by highest priority first, then most recent first.
TArray<FlxInputModeRequest> Requests;
UPROPERTY()
int32 NextSequenceNumber = 0;
public:
// Get the requests array.
const TArray<FlxInputModeRequest> &GetRequests() const { return Requests; }
// Sanity check a request to see if it is reasonable.
static bool SanityCheck(const FlxInputModeRequest &Request);
// Apply a request. Replaces any previous request by the same widget.
void Request(const FlxInputModeRequest &NewRequest, bool UpdateSequence = true);
// Find the specified widget, and modify the 'EnableInputComponent'
// flag. Adds the widget if it's not already present.
void SetEnableInputComponent(UUserWidget *Widget, bool EnableInputComponent);
// Remove all requests by the specified widget.
void Remove(UUserWidget *Widget);
// Remove any requests by dead widgets or widgets with no parents.
void GarbageCollect();
private:
// Find specified widget. If not present, returns Requests.Num()
int32 FindWidget(UUserWidget *Widget);
// Move item at Index to its proper place in the array by priority.
void BubbleItem(int32 Index);
};

View File

@@ -1,8 +1,10 @@
#include "PlayerControllerBase.h" #include "PlayerControllerBase.h"
#include "Common.h" #include "Common.h"
#include "RootCanvas.h"
#include "Tangible.h" #include "Tangible.h"
#include "TangibleManager.h" #include "TangibleManager.h"
#include "Blueprint/UserWidget.h" #include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/InputComponent.h" #include "Components/InputComponent.h"
#include "Engine/GameInstance.h" #include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h" #include "Engine/GameViewportClient.h"
@@ -63,7 +65,17 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context)
void AlxPlayerControllerBase::BeginPlay() void AlxPlayerControllerBase::BeginPlay()
{ {
// Build the root UMG stack BEFORE Super::BeginPlay. Super calls
// ReceiveBeginPlay, which fires the Blueprint Event BeginPlay;
// BP code there may immediately try to add widgets to RootCanvas,
// so the canvas must already exist.
RootWidget = CreateWidget<UlxRootWidget>(this);
RootCanvas = RootWidget->WidgetTree->ConstructWidget<UlxRootCanvasPanel>();
RootWidget->WidgetTree->RootWidget = RootCanvas;
RootWidget->AddToViewport(0);
Super::BeginPlay(); Super::BeginPlay();
if (FSlateApplication::IsInitialized()) if (FSlateApplication::IsInitialized())
{ {
FocusChangingHandle = FSlateApplication::Get().OnFocusChanging().AddUObject( FocusChangingHandle = FSlateApplication::Get().OnFocusChanging().AddUObject(
@@ -78,6 +90,14 @@ void AlxPlayerControllerBase::EndPlay(const EEndPlayReason::Type EndPlayReason)
FSlateApplication::Get().OnFocusChanging().Remove(FocusChangingHandle); FSlateApplication::Get().OnFocusChanging().Remove(FocusChangingHandle);
FocusChangingHandle.Reset(); FocusChangingHandle.Reset();
} }
if (IsValid(RootWidget))
{
RootWidget->RemoveFromParent();
}
RootWidget = nullptr;
RootCanvas = nullptr;
Super::EndPlay(EndPlayReason); Super::EndPlay(EndPlayReason);
} }
@@ -111,11 +131,18 @@ UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *W
return Cast<UInputComponent>(Value); return Cast<UInputComponent>(Value);
} }
void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool ShowPointer, bool BlockInput, UWidget *Focus, bool EnableInputComponent) void AlxPlayerControllerBase::RestoreFocusToFrontWidget(const UObject *Context)
{
// This will trigger UpdateInputMode to shift focus back to
// the front window, if the front window wants focus.
FromContext(Context)->LastWidgetGrantedFocus = nullptr;
}
void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget)
{ {
if (!IsValid(Widget)) if (!IsValid(Widget))
{ {
UE_LOG(LogLuprexIntegration, Error, TEXT("WidgetRequestInputMode called with an invalid widget.")); UE_LOG(LogLuprexIntegration, Error, TEXT("AddWidgetToRoot called with an invalid widget."));
return; return;
} }
APlayerController *OwningPC = Widget->GetOwningPlayer(); APlayerController *OwningPC = Widget->GetOwningPlayer();
@@ -123,53 +150,21 @@ void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool S
if (PC == nullptr) if (PC == nullptr)
{ {
UE_LOG(LogLuprexIntegration, Error, UE_LOG(LogLuprexIntegration, Error,
TEXT("WidgetRequestInputMode: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."), TEXT("AddWidgetToRoot: widget '%s' owning player is not an AlxPlayerControllerBase (got %s)."),
*Widget->GetName(), *GetNameSafe(OwningPC)); *Widget->GetName(), *GetNameSafe(OwningPC));
return; return;
} }
FlxInputModeRequest Req; if (PC->RootCanvas == nullptr)
Req.Widget = Widget;
Req.Focus = Focus;
Req.ShowPointer = ShowPointer;
Req.BlockInput = BlockInput;
Req.EnableInputComponent = EnableInputComponent;
PC->InputModeRequests.Request(Req);
}
void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent)
{ {
if (InInputComponent) UE_LOG(LogLuprexIntegration, Error,
{ TEXT("AddWidgetToRoot: root canvas is not initialized, this is probably an initialization order issue"));
// 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.SetEnableInputComponent(Widget, true);
return; return;
} }
CurrentInputStack.RemoveSingle(InInputComponent);
CurrentInputStack.Add(InInputComponent);
}
}
bool AlxPlayerControllerBase::PopInputComponent(UInputComponent* InInputComponent) if (Widget->GetParent() == PC->RootCanvas) return;
{
if (InInputComponent) UlxRootCanvasSlot *Slot = PC->RootCanvas->AddChildToRootCanvas(Widget);
{ Slot->SetZOrder(0);
if (UUserWidget *Widget = Cast<UUserWidget>(InInputComponent->GetOuter()))
{
InputModeRequests.SetEnableInputComponent(Widget, false);
InInputComponent->ClearBindingValues();
return true;
}
if (CurrentInputStack.RemoveSingle(InInputComponent) > 0)
{
InInputComponent->ClearBindingValues();
return true;
}
}
return false;
} }
void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack) void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputStack)
@@ -216,50 +211,54 @@ void AlxPlayerControllerBase::BuildInputStack(TArray<UInputComponent*>& InputSta
InputStack.Push(InputComponent); InputStack.Push(InputComponent);
} }
// The current input stack is unlikely to have anything, // Get the widget slots.
// given that we've moved widgets to their own mechanism. TArray<UlxRootCanvasSlot*> WidgetSlots = RootCanvas->GetSortedUserWidgets();
// But, if there's anything here, sort it and push it.
if (!CurrentInputStack.IsEmpty()) // Generate a set of input components that are being managed by the WidgetSlots.
TSet<UInputComponent*> WidgetManagedComponents;
for (UlxRootCanvasSlot *Slot : WidgetSlots)
{ {
TArray<UInputComponent*, TInlineAllocator<20>> Pushed; UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
UInputComponent *IC = GetWidgetInputComponent(Widget);
if (IC) WidgetManagedComponents.Add(IC);
}
// Add components in the CurrentInputStack, *unless* they are being managed
// by widgets. If they're being managed by widgets, they get added later.
for (int32 Idx=0; Idx<CurrentInputStack.Num(); ++Idx) for (int32 Idx=0; Idx<CurrentInputStack.Num(); ++Idx)
{ {
UInputComponent* IC = CurrentInputStack[Idx].Get(); UInputComponent* IC = CurrentInputStack[Idx].Get();
if (IsValid(IC)) Pushed.Add(IC); if (IsValid(IC))
else CurrentInputStack.RemoveAt(Idx--); {
if (!WidgetManagedComponents.Contains(IC)) InputStack.Push(IC);
}
else
{
CurrentInputStack.RemoveAt(Idx--);
}
} }
Pushed.StableSort([](const UInputComponent& A, const UInputComponent& B) // Now add the widget-managed input components.
for (UlxRootCanvasSlot *Slot : ReverseIterate(WidgetSlots))
{ {
if (A.bBlockInput != B.bBlockInput) return !A.bBlockInput; if (Slot->EnableEnhancedInput)
return A.Priority < B.Priority;
});
InputStack.Append(Pushed);
}
// 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)
{ {
const FlxInputModeRequest &Req = Requests[Idx]; UUserWidget *Widget = Cast<UUserWidget>(Slot->GetContent());
if (!Req.EnableInputComponent) continue; UInputComponent *IC = GetWidgetInputComponent(Widget);
UUserWidget *Widget = Req.Widget; if (IC)
if (!Widget->IsInViewport()) continue;
if (UInputComponent *IC = GetWidgetInputComponent(Widget))
{ {
IC->bBlockInput = Req.BlockInput; IC->bBlockInput = Slot->BlockInput;
InputStack.Push(IC); InputStack.Push(IC);
} }
} }
} }
}
void AlxPlayerControllerBase::UpdateInputMode() void AlxPlayerControllerBase::UpdateInputMode()
{ {
InputModeRequests.GarbageCollect(); // Drain any deferred ZOrder writes from SetWidgetWindowManagement
// before we read the front-most slot.
RootCanvas->PropagateZOrderToSlate();
// Get all the various objects we need to be able to manipulate // Get all the various objects we need to be able to manipulate
// the input mode. // the input mode.
@@ -273,19 +272,24 @@ void AlxPlayerControllerBase::UpdateInputMode()
TSharedPtr<FSlateUser> SlateUser = LocalPlayer->GetSlateUser(); TSharedPtr<FSlateUser> SlateUser = LocalPlayer->GetSlateUser();
if (!SlateUser.IsValid()) return; if (!SlateUser.IsValid()) return;
// The first active entry in InputModeRequests dictates the
// pointer / capture / focus state. If there are no requests at all, // Get the desired configuration from the first widget.
// fall back to a static default-constructed request. // TODO: Maybe we don't have to sort the whole array.
static const FlxInputModeRequest EmptyRequest; TArray<UlxRootCanvasSlot*> WidgetSlots = RootCanvas->GetSortedUserWidgets();
const FlxInputModeRequest *Top = &EmptyRequest; UUserWidget *Widget = nullptr;
for (const FlxInputModeRequest &Req : InputModeRequests.GetRequests()) UWidget *Focus = nullptr;
bool ShowPointer = false;
if (!WidgetSlots.IsEmpty())
{ {
if (Req.Widget->IsInViewport()) { Top = &Req; break; } UlxRootCanvasSlot *Top = WidgetSlots[0];
Widget = Cast<UUserWidget>(Top->GetContent());
Focus = Widget->GetDesiredFocusWidget();
ShowPointer = Top->ShowPointer;
} }
SetShowMouseCursor(Top->ShowPointer); SetShowMouseCursor(ShowPointer);
if (Top->ShowPointer) if (ShowPointer)
{ {
// Only release capture if the viewport is currently holding it // Only release capture if the viewport is currently holding it
// (e.g. we just came from GameOnly). A blanket ReleaseMouseCapture // (e.g. we just came from GameOnly). A blanket ReleaseMouseCapture
@@ -321,20 +325,20 @@ void AlxPlayerControllerBase::UpdateInputMode()
// viewport client notifies us of that fact. We then focus the // viewport client notifies us of that fact. We then focus the
// widget if possible. // widget if possible.
// //
if ((!Top->ShowPointer) || (LastRequestGrantedFocus != Top->SequenceNumber)) if ((!ShowPointer) || (LastWidgetGrantedFocus != Focus))
{ {
if (Top->Focus) if (Focus)
{ {
if (TSharedPtr<SWidget> SlateFocus = Top->Focus->GetCachedWidget()) if (TSharedPtr<SWidget> SlateFocus = Focus->GetCachedWidget())
{ {
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef()); SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
LastRequestGrantedFocus = Top->SequenceNumber; LastWidgetGrantedFocus = Focus;
} }
} }
else else
{ {
SlateOperations.SetUserFocus(ViewportWidgetRef); SlateOperations.SetUserFocus(ViewportWidgetRef);
LastRequestGrantedFocus = Top->SequenceNumber; LastWidgetGrantedFocus = nullptr;
} }
} }
else if (TSharedPtr<SWidget> ClickedWidget = ClickToFocusTarget.Pin()) else if (TSharedPtr<SWidget> ClickedWidget = ClickToFocusTarget.Pin())

View File

@@ -3,9 +3,12 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Engine/HitResult.h" #include "Engine/HitResult.h"
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "InputModeRequest.h" #include "UObject/ObjectKey.h"
#include "PlayerControllerBase.generated.h" #include "PlayerControllerBase.generated.h"
class UlxRootCanvasPanel;
class UWidget;
UCLASS(BlueprintType, Blueprintable) UCLASS(BlueprintType, Blueprintable)
class INTEGRATION_API AlxPlayerControllerBase : public APlayerController class INTEGRATION_API AlxPlayerControllerBase : public APlayerController
{ {
@@ -55,9 +58,6 @@ private:
public: public:
// 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; virtual void BuildInputStack(TArray<UInputComponent*>& InputStack) override;
// Read UUserWidget::InputComponent via reflection. The field is // Read UUserWidget::InputComponent via reflection. The field is
@@ -65,12 +65,14 @@ public:
// FProperty so we always see the current value without caching it. // FProperty so we always see the current value without caching it.
static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget); static class UInputComponent* GetWidgetInputComponent(class UUserWidget *Widget);
// Blueprint-facing entry point. Looks like a method on UUserWidget // Restore focus back to the window that is in front, if it wants focus.
// (thanks to DefaultToSelf + HideSelfPin): the widget self-binds, UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context"), Category = "Luprex|Root Canvas")
// we find its owning PlayerController, and register the request. static void RestoreFocusToFrontWidget(const UObject *Context);
UFUNCTION(BlueprintCallable, Category = "Luprex|Input Mode",
meta = (DefaultToSelf = "Widget", HideSelfPin = "true", EnableInputComponent = "true")) // Add a widget to the root canvas at ZOrder 0 with default slot flags.
static void WidgetRequestInputMode(class UUserWidget *Widget, bool ShowPointer, bool BlockInput, class UWidget *Focus, bool EnableInputComponent); UFUNCTION(BlueprintCallable, Category = "Luprex|Root Canvas",
meta = (DefaultToSelf = "Widget", HideSelfPin = "true"))
static void AddWidgetToRoot(class UUserWidget *Widget);
// Get the player controller, cast to AlxPlayerControllerBase. // Get the player controller, cast to AlxPlayerControllerBase.
static AlxPlayerControllerBase *FromContext(const UObject *Context); static AlxPlayerControllerBase *FromContext(const UObject *Context);
@@ -78,13 +80,19 @@ public:
UPROPERTY() UPROPERTY()
FHitResult CurrentLookAt; FHitResult CurrentLookAt;
// Input mode requests - see InputModeRequest.h for an explanation. // The last widget whose focus request was granted.
UPROPERTY() TObjectKey<UWidget> LastWidgetGrantedFocus;
FlxInputModeRequests InputModeRequests;
// The last input mode request whose focus request was granted. // The single top-level UUserWidget added to the viewport. All
// top-level UI widgets are children of RootCanvas inside it.
UPROPERTY() UPROPERTY()
int32 LastRequestGrantedFocus = 0; UUserWidget *RootWidget = nullptr;
// The root canvas panel inside RootWidget. Children of this
// canvas are the top-level widgets; their slots carry both
// layout and input-mode configuration.
UPROPERTY()
UlxRootCanvasPanel *RootCanvas = nullptr;
// The viewport client uses this to notify us that the user // The viewport client uses this to notify us that the user
// clicked on a focusable widget. // clicked on a focusable widget.

View File

@@ -0,0 +1,121 @@
////////////////////////////////////////////////////////////
//
// RootCanvas.cpp
//
////////////////////////////////////////////////////////////
#include "RootCanvas.h"
#include "Common.h"
#include "Blueprint/UserWidget.h"
UlxRootCanvasSlot::UlxRootCanvasSlot(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Children of the root canvas default to filling the parent,
// matching the viewport's "stretch" behavior. Individual widgets
// can still override anchors/offsets after being added.
SetAnchors(FAnchors(0.0f, 0.0f, 1.0f, 1.0f));
SetOffsets(FMargin(0.0f, 0.0f, 0.0f, 0.0f));
}
UClass* UlxRootCanvasPanel::GetSlotClass() const
{
return UlxRootCanvasSlot::StaticClass();
}
UlxRootCanvasSlot *UlxRootCanvasPanel::GetRootCanvasSlot(UUserWidget *Widget, ElxSuccessOrWrongType &Result)
{
if (IsValid(Widget))
{
if (UlxRootCanvasSlot *Slot = Cast<UlxRootCanvasSlot>(Widget->Slot))
{
Result = ElxSuccessOrWrongType::Success;
return Slot;
}
}
Result = ElxSuccessOrWrongType::WrongType;
return nullptr;
}
UlxRootCanvasSlot* UlxRootCanvasPanel::AddChildToRootCanvas(UWidget* Content)
{
return Cast<UlxRootCanvasSlot>(Super::AddChild(Content));
}
int32 UlxRootCanvasPanel::GetMaxZOrder() const
{
int32 MaxZOrder = 0;
for (UPanelSlot *PanelSlot : Slots)
{
UlxRootCanvasSlot *TypedSlot = Cast<UlxRootCanvasSlot>(PanelSlot);
check(TypedSlot);
MaxZOrder = FMath::Max(MaxZOrder, TypedSlot->GetZOrder());
}
return MaxZOrder;
}
void UlxRootCanvasPanel::PropagateZOrderToSlate()
{
if (!MustPropagateZOrderToSlate) return;
MustPropagateZOrderToSlate = false;
for (UPanelSlot *PanelSlot : Slots)
{
UlxRootCanvasSlot *TypedSlot = Cast<UlxRootCanvasSlot>(PanelSlot);
check(TypedSlot);
TypedSlot->SetZOrder(TypedSlot->GetZOrder());
}
}
TArray<UlxRootCanvasSlot*> UlxRootCanvasPanel::GetSortedUserWidgets()
{
TArray<UlxRootCanvasSlot*> Result;
Result.Reserve(Slots.Num());
for (UPanelSlot *PanelSlot : Slots)
{
UlxRootCanvasSlot *TypedSlot = Cast<UlxRootCanvasSlot>(PanelSlot);
check(TypedSlot);
if (Cast<UUserWidget>(TypedSlot->Content) == nullptr) continue;
Result.Add(TypedSlot);
}
Result.StableSort([](const UlxRootCanvasSlot &A, const UlxRootCanvasSlot &B)
{
return A.GetZOrder() > B.GetZOrder();
});
return Result;
}
void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget,
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput, bool BringToFront, UWidget *DesiredFocusWidget)
{
if (!IsValid(Widget))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("ManageRootWidget called with an invalid widget."));
return;
}
UlxRootCanvasSlot *Slot = Cast<UlxRootCanvasSlot>(Widget->Slot);
if (Slot == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("Widget is not yet a root widget, use 'AddWidgetToRoot' first"));
return;
}
UlxRootCanvasPanel *Panel = Cast<UlxRootCanvasPanel>(Slot->Parent);
check(Panel);
Slot->ShowPointer = ShowPointer;
Slot->BlockInput = BlockInput;
Slot->EnableEnhancedInput = EnableEnhancedInput;
Widget->SetDesiredFocusWidget(DesiredFocusWidget);
if (BringToFront)
{
// Write the int32 ZOrder UPROPERTY directly and defer the
// Slate-side push to PropagateZOrderToSlate. Going through SetZOrder
// here is unsafe when called from Event Construct: the FSlot
// has been Exposed but its Owner isn't set yet, which fires
// an ensure in SConstraintCanvas::FSlot::SetZOrder.
PRAGMA_DISABLE_DEPRECATION_WARNINGS
Slot->ZOrder = Panel->GetMaxZOrder() + 1;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
Panel->MustPropagateZOrderToSlate = true;
}
}

View File

@@ -0,0 +1,147 @@
////////////////////////////////////////////////////////////
//
// RootCanvas.h
//
// UlxRootCanvasPanel is a UCanvasPanel subclass whose
// slots (UlxRootCanvasSlot) carry input-mode configuration
// in addition to layout. The PlayerController scans these
// slots, sorted by ZOrder, to arbitrate pointer visibility,
// capture, focus, and input-component blocking for
// top-level widgets. ZOrder therefore serves double duty:
// it determines draw order AND input priority.
//
////////////////////////////////////////////////////////////
#pragma once
#include "CoreMinimal.h"
#include "Common.h"
#include "Blueprint/UserWidget.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "RootCanvas.generated.h"
class UWidget;
////////////////////////////////////////////////////////////
//
// UlxRootCanvasSlot
//
// Luprex provides a "window management system" for root widgets.
// This system is documented in Docs/Keyboard-Focus-and-Input-Modes.md
// The Root Canvas Slot is how widgets ask the window management system
// to engage certain behaviors.
//
////////////////////////////////////////////////////////////
UCLASS()
class INTEGRATION_API UlxRootCanvasSlot : public UCanvasPanelSlot
{
GENERATED_BODY()
public:
UlxRootCanvasSlot(const FObjectInitializer& ObjectInitializer);
// When this window is in front, the mouse pointer is shown and the
// viewport does not capture the mouse.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode")
bool ShowPointer = false;
// When this window is in front, this window's input component blocks
// lower-priority input components in the input stack.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode")
bool BlockInput = false;
// If true, this widget's input component is enabled, which is to say,
// that enhanced input events are enabled. If false, enhanced input
// events in the Event Graph are deactivated.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Luprex|Input Mode")
bool EnableEnhancedInput = true;
};
////////////////////////////////////////////////////////////
//
// UlxRootCanvasPanel
//
// A UCanvasPanel that uses UlxRootCanvasSlot for its
// children instead of the plain UCanvasPanelSlot. Layout
// behavior is identical to UCanvasPanel; only the slot
// type differs.
//
////////////////////////////////////////////////////////////
UCLASS()
class INTEGRATION_API UlxRootCanvasPanel : public UCanvasPanel
{
GENERATED_BODY()
public:
// Convenience wrapper around AddChild that returns the
// derived slot type, so callers don't have to cast.
UFUNCTION()
UlxRootCanvasSlot* AddChildToRootCanvas(UWidget* Content);
// Find children of type UserWidget. Return them in a sorted
// order, with the highest Zorder first.
TArray<UlxRootCanvasSlot*> GetSortedUserWidgets();
// Return the largest ZOrder across all slots, or 0 if empty.
// Used as the basis for placing new widgets on top.
int32 GetMaxZOrder() const;
// SetZOrder is buggy: if you call it inside Event Construct,
// it crashes. So we've developed a hack to make it reliable.
// To set the zorder, you store the value in the slot property,
// without calling SetZorder. This routine, which is used later,
// propagates that value into the slate widgets.
void PropagateZOrderToSlate();
// This function updates several window-management-related properties
// which are stored in the UserWidget and the lxRootCanvasSlot. Note that
// it is perfectly legal to edit these properties by other means: this
// function is one of many valid setters. See the documentation in
// Docs/Keyboard-Focus-and-Input-Modes.md for information about how Luprex
// window management works.
UFUNCTION(BlueprintCallable, Category = "Luprex|Window Management",
meta = (DefaultToSelf = "Widget", EnableEnhancedInput = "true"))
static void SetWidgetWindowManagement(class UUserWidget *Widget,
bool ShowPointer, bool BlockInput, bool EnableEnhancedInput,
bool BringToFront, UWidget *DesiredFocusWidget);
// Fetch the UlxRootCanvasSlot for a widget that is parented to a
// UlxRootCanvasPanel. Returns nullptr via the WrongType exec pin
// if the widget isn't a root widget (no slot, or slot is not a
// UlxRootCanvasSlot).
UFUNCTION(BlueprintCallable, Category = "Luprex|Window Management",
meta = (DefaultToSelf = "Widget", ExpandEnumAsExecs = "Result"))
static UlxRootCanvasSlot *GetRootCanvasSlot(class UUserWidget *Widget, ElxSuccessOrWrongType &Result);
protected:
// UPanelWidget
virtual UClass* GetSlotClass() const override;
public:
bool MustPropagateZOrderToSlate = false;
};
////////////////////////////////////////////////////////////
//
// UlxRootWidget
//
// A trivial concrete UUserWidget subclass used by the
// PlayerController to host the root UlxRootCanvasPanel.
// UUserWidget itself is marked Abstract in UMG, so we need
// a non-abstract class to instantiate.
//
////////////////////////////////////////////////////////////
UCLASS()
class INTEGRATION_API UlxRootWidget : public UUserWidget
{
GENERATED_BODY()
};

View File

@@ -168,6 +168,7 @@ def UEFStringSummaryProvider(valobj, dict):
############################################################ ############################################################
def UEFNameSummaryProvider(valobj, dict): def UEFNameSummaryProvider(valobj, dict):
valobj = valobj.GetNonSyntheticValue()
target = valobj.GetTarget() target = valobj.GetTarget()
process = target.GetProcess() process = target.GetProcess()
entry_id = valobj.GetChildMemberWithName('DisplayIndex') entry_id = valobj.GetChildMemberWithName('DisplayIndex')