More work on focus, and good docs

This commit is contained in:
2026-04-22 22:52:04 -04:00
parent a964211cc8
commit a689d59ea0
11 changed files with 391 additions and 354 deletions

View File

@@ -7,10 +7,11 @@
#include "LuprexViewportClient.h"
#include "Common.h"
#include "PlayerControllerBase.h"
#include "RootCanvas.h"
#include "Engine/GameInstance.h"
#include "Framework/Application/SlateApplication.h"
#include "Layout/WidgetPath.h"
#include "Widgets/SViewport.h"
#include "Slate/SObjectWidget.h"
UlxViewportClient::UlxViewportClient(const FObjectInitializer &ObjectInitializer)
: Super(ObjectInitializer)
@@ -18,44 +19,44 @@ UlxViewportClient::UlxViewportClient(const FObjectInitializer &ObjectInitializer
UE_LOG(LogLuprexIntegration, Display, TEXT("UlxViewportClient constructed"));
}
bool UlxViewportClient::TryBringToFront(const FWidgetPath &Path)
{
UGameInstance *GI = GetGameInstance();
if (!GI) return false;
AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(
GI->GetFirstLocalPlayerController(GetWorld()));
if (!PC) return false;
for (int32 Idx = 0; Idx < Path.Widgets.Num(); ++Idx)
{
SWidget &SW = Path.Widgets[Idx].Widget.Get();
if (SW.GetType() != FName(TEXT("SObjectWidget"))) continue;
UUserWidget *Widget = static_cast<SObjectWidget&>(SW).GetWidgetObject();
if (Widget && Widget->GetParent() == PC->RootCanvas)
{
UlxRootCanvasPanel::BringToFront(Widget);
return true;
}
}
return false;
}
bool UlxViewportClient::InputKey(const FInputKeyEventArgs &EventArgs)
{
UE_LOG(LogLuprexIntegration, Display, TEXT("UlxViewportClient::InputKey key=%s event=%d"),
*EventArgs.Key.ToString(), (int32)EventArgs.Event);
// Only act on left mouse button presses that bubbled up to the
// viewport unhandled. Walk the widget path under the cursor and
// find the nearest focusable ancestor of whatever was hit. If it
// isn't the game viewport itself, hand it to the player controller
// to apply on its next UpdateInputMode pass; that's the point in
// the frame where we can override SViewport's own click-focus
// behaviour without fighting it.
// viewport unhandled. If the click landed on a descendant of a
// top-level widget in the root canvas, bring that top-level widget
// to the front.
if ((EventArgs.Event == IE_Pressed) && (EventArgs.Key == EKeys::LeftMouseButton))
{
FSlateApplication &Slate = FSlateApplication::Get();
FVector2D MousePos = Slate.GetCursorPos();
FWidgetPath Path = Slate.LocateWindowUnderMouse(
MousePos, Slate.GetInteractiveTopLevelWindows());
if (Path.IsValid())
{
TSharedPtr<SViewport> ViewportWidget = GetGameViewportWidget();
for (int32 Idx = Path.Widgets.Num() - 1; Idx >= 0; --Idx)
{
TSharedRef<SWidget> Widget = Path.Widgets[Idx].Widget;
if (!Widget->SupportsKeyboardFocus()) continue;
if (ViewportWidget.IsValid() && Widget == ViewportWidget) break;
if (UGameInstance *GI = GetGameInstance())
{
if (AlxPlayerControllerBase *PC = Cast<AlxPlayerControllerBase>(
GI->GetFirstLocalPlayerController(GetWorld())))
{
PC->ClickToFocus(Widget);
}
}
break;
}
}
if (Path.IsValid() && TryBringToFront(Path)) return true;
}
return Super::InputKey(EventArgs);

View File

@@ -25,4 +25,7 @@ public:
UlxViewportClient(const FObjectInitializer &ObjectInitializer);
virtual bool InputKey(const FInputKeyEventArgs &EventArgs) override;
private:
bool TryBringToFront(const FWidgetPath &Path);
};

View File

@@ -135,7 +135,7 @@ 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;
FromContext(Context)->RootCanvas->LastWidgetGrantedFocus = nullptr;
}
void AlxPlayerControllerBase::AddWidgetToRoot(UUserWidget *Widget)
@@ -319,32 +319,22 @@ void AlxPlayerControllerBase::UpdateInputMode()
// viewport client notifies us of that fact. We then focus the
// widget if possible.
//
if ((!ShowPointer) || (LastWidgetGrantedFocus != Focus))
if ((!ShowPointer) || (RootCanvas->LastWidgetGrantedFocus != Focus))
{
if (Focus)
{
if (TSharedPtr<SWidget> SlateFocus = Focus->GetCachedWidget())
{
SlateOperations.SetUserFocus(SlateFocus.ToSharedRef());
LastWidgetGrantedFocus = Focus;
RootCanvas->LastWidgetGrantedFocus = Focus;
}
}
else
{
SlateOperations.SetUserFocus(ViewportWidgetRef);
LastWidgetGrantedFocus = nullptr;
RootCanvas->LastWidgetGrantedFocus = nullptr;
}
}
else if (TSharedPtr<SWidget> ClickedWidget = ClickToFocusTarget.Pin())
{
SlateOperations.SetUserFocus(ClickedWidget.ToSharedRef());
}
ClickToFocusTarget.Reset();
}
void AlxPlayerControllerBase::ClickToFocus(TSharedRef<SWidget> Widget)
{
ClickToFocusTarget = Widget.ToWeakPtr();
}
void AlxPlayerControllerBase::UpdateLookAt()

View File

@@ -80,9 +80,6 @@ public:
UPROPERTY()
FHitResult CurrentLookAt;
// The last widget whose focus request was granted.
TObjectKey<UWidget> LastWidgetGrantedFocus;
// The single top-level UUserWidget added to the viewport. All
// top-level UI widgets are children of RootCanvas inside it.
UPROPERTY()
@@ -94,14 +91,5 @@ public:
UPROPERTY()
UlxRootCanvasPanel *RootCanvas = nullptr;
// The viewport client uses this to notify us that the user
// clicked on a focusable widget.
void ClickToFocus(TSharedRef<SWidget> Widget);
private:
TWeakPtr<SWidget> ClickToFocusTarget;
public:
bool MustCallLookAtChanged = false;
};

View File

@@ -101,6 +101,8 @@ void UlxRootCanvasPanel::BringToFront(UUserWidget *Widget)
UlxRootCanvasPanel *Panel = Cast<UlxRootCanvasPanel>(Slot->Parent);
if (!Panel) return;
Slot->BringToFrontCount = ++Panel->BringToFrontCounter;
// This refocuses the widget, even if it was already in front.
Panel->LastWidgetGrantedFocus = Panel;
}
void UlxRootCanvasPanel::SetWidgetWindowManagement(class UUserWidget *Widget,

View File

@@ -19,6 +19,7 @@
#include "CoreMinimal.h"
#include "Common.h"
#include "UObject/ObjectKey.h"
#include "Blueprint/UserWidget.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
@@ -128,6 +129,9 @@ public:
meta = (DefaultToSelf = "Widget", ExpandEnumAsExecs = "Result"))
static UlxRootCanvasSlot *GetRootCanvasSlot(class UUserWidget *Widget, ElxSuccessOrWrongType &Result);
// The last widget whose focus request was granted.
TObjectKey<UWidget> LastWidgetGrantedFocus;
protected:
// We inherit most of our code from CanvasPanel. This causes the