diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index 702005ea..a3f1cdb4 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -4,7 +4,7 @@ [/Script/Engine.Engine] -GameViewportClientClassName=/Script/CommonUI.CommonGameViewportClient +GameViewportClientClassName=/Script/Integration.lxViewportClient [/Script/EngineSettings.GameMapsSettings] GameDefaultMap=/Game/LpxLevel.LpxLevel diff --git a/Content/Widgets/WB_Console.uasset b/Content/Widgets/WB_Console.uasset index 22d2a44a..83366b9e 100644 --- a/Content/Widgets/WB_Console.uasset +++ b/Content/Widgets/WB_Console.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c47f30006c2587f576ad3d9648cab923229c8a3f837410b6e72c231bff1052 -size 213148 +oid sha256:deea0bc6b6d7e176b78f5f031ab3baea187807f5500c7e14ba9626f533357c74 +size 219967 diff --git a/Source/Integration/InputEvents.cpp b/Source/Integration/InputModeRequest.cpp similarity index 71% rename from Source/Integration/InputEvents.cpp rename to Source/Integration/InputModeRequest.cpp index 2b11877e..5027e12c 100644 --- a/Source/Integration/InputEvents.cpp +++ b/Source/Integration/InputModeRequest.cpp @@ -1,10 +1,10 @@ //////////////////////////////////////////////////////////// // -// InputEvents.cpp +// InputModeRequest.cpp // //////////////////////////////////////////////////////////// -#include "InputEvents.h" +#include "InputModeRequest.h" #include "Common.h" bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const @@ -12,7 +12,8 @@ bool FlxInputModeRequest::operator==(const FlxInputModeRequest &Other) const return (Widget == Other.Widget) && (Focus == Other.Focus) && (ShowPointer == Other.ShowPointer) && - (BlockInput == Other.BlockInput); + (BlockInput == Other.BlockInput) && + (EnableInputComponent == Other.EnableInputComponent); } bool FlxInputModeRequests::SanityCheck(const FlxInputModeRequest &Request) @@ -42,60 +43,55 @@ void FlxInputModeRequests::Request(const FlxInputModeRequest &NewRequest) 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 (IsHigh) - { - if ((High.Num() > 0) && (High[0] == NewRequest)) return; - } - else - { - if ((Low.Num() > 0) && (Low[0] == NewRequest)) return; - } + // Stamp this request with a fresh sequence number. + FlxInputModeRequest Stamped = NewRequest; + Stamped.SequenceNumber = ++NextSequenceNumber; // We're going to build a new version of the requests array. TArray Updated; // Add all high priority requests to the updated array, new request first. - if (IsHigh) Updated.Add(NewRequest); + if (IsHigh) Updated.Add(Stamped); 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 (!IsHigh) Updated.Add(NewRequest); + if (!IsHigh) Updated.Add(Stamped); for (const FlxInputModeRequest &Req : Low) if (Req.Widget != NewRequest.Widget) Updated.Add(Req); Swap(Requests, Updated); - Dirty = true; } -void FlxInputModeRequests::EnsureWidget(UUserWidget *Widget) +void FlxInputModeRequests::SetEnableInputComponent(UUserWidget *Widget, bool EnableInputComponent) { - for (const FlxInputModeRequest &Req : Requests) + for (FlxInputModeRequest &Req : Requests) { - if (Req.Widget == Widget) return; + if (Req.Widget == Widget) + { + Req.EnableInputComponent = EnableInputComponent; + return; + } } - Request(FlxInputModeRequest(Widget, nullptr, false, false)); + FlxInputModeRequest NewReq; + NewReq.Widget = Widget; + NewReq.EnableInputComponent = EnableInputComponent; + Request(NewReq); } void FlxInputModeRequests::Remove(UUserWidget *Widget) { - int32 N = Requests.Num(); Requests.RemoveAll([Widget](const FlxInputModeRequest &Entry) { return Entry.Widget == Widget; }); - if (Requests.Num() < N) Dirty = true; } void FlxInputModeRequests::GarbageCollect() { - int32 N = Requests.Num(); Requests.RemoveAll([](const FlxInputModeRequest &Entry) { return !IsValid(Entry.Widget); }); - if (Requests.Num() < N) Dirty = true; } diff --git a/Source/Integration/InputEvents.h b/Source/Integration/InputModeRequest.h similarity index 61% rename from Source/Integration/InputEvents.h rename to Source/Integration/InputModeRequest.h index cc5bbe0b..245fe4b1 100644 --- a/Source/Integration/InputEvents.h +++ b/Source/Integration/InputModeRequest.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////// // -// InputEvents.h +// InputModeRequest.h // // Custom input event dispatching system. Uses Unreal's // built-in input modes (GameOnly / UIOnly) with an @@ -12,21 +12,17 @@ #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" -#include "InputEvents.generated.h" +#include "InputModeRequest.generated.h" + + //////////////////////////////////////////////////////////// // // FlxInputModeRequest // -// A widget's declaration of interest in input events. -// -// Widget: The widget that wants to receive events. -// 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. -// BlockInput: If true, input actions should be blocked from -// reaching lower-priority consumers (e.g. the pawn). +// Using this struct, a Widget can express a need for a +// particular input mode. These requests go to the player +// controller, which arbitrates. // //////////////////////////////////////////////////////////// @@ -36,8 +32,6 @@ struct FlxInputModeRequest GENERATED_BODY() FlxInputModeRequest() = default; - FlxInputModeRequest(UUserWidget *InWidget, UWidget *InFocus, bool InShowPointer, bool InBlockInput) - : Widget(InWidget), Focus(InFocus), ShowPointer(InShowPointer), BlockInput(InBlockInput) {} bool operator == (const FlxInputModeRequest &Other) const; @@ -56,6 +50,13 @@ struct FlxInputModeRequest UPROPERTY(BlueprintReadWrite) bool BlockInput = false; + + UPROPERTY(BlueprintReadWrite) + bool EnableInputComponent = true; + + UPROPERTY() + int32 SequenceNumber = 0; + }; USTRUCT() @@ -65,12 +66,11 @@ struct FlxInputModeRequests private: UPROPERTY() - // High priority requests are always before low-priority. - // Otherwise, these are in order of most recent first. + // Sorted by highest priority first, then most recent first. TArray Requests; UPROPERTY() - bool Dirty = true; + int32 NextSequenceNumber = 0; public: using View = TArrayView; @@ -87,22 +87,13 @@ public: // Apply a request. Replaces any previous request by the same widget. 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); + // 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(); - - // 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; } }; diff --git a/Source/Integration/LuprexViewportClient.cpp b/Source/Integration/LuprexViewportClient.cpp new file mode 100644 index 00000000..b860eae1 --- /dev/null +++ b/Source/Integration/LuprexViewportClient.cpp @@ -0,0 +1,62 @@ +//////////////////////////////////////////////////////////// +// +// LuprexViewportClient.cpp +// +//////////////////////////////////////////////////////////// + +#include "LuprexViewportClient.h" +#include "Common.h" +#include "PlayerControllerBase.h" +#include "Engine/GameInstance.h" +#include "Framework/Application/SlateApplication.h" +#include "Layout/WidgetPath.h" +#include "Widgets/SViewport.h" + +UlxViewportClient::UlxViewportClient(const FObjectInitializer &ObjectInitializer) + : Super(ObjectInitializer) +{ + UE_LOG(LogLuprexIntegration, Display, TEXT("UlxViewportClient constructed")); +} + +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. + 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 ViewportWidget = GetGameViewportWidget(); + for (int32 Idx = Path.Widgets.Num() - 1; Idx >= 0; --Idx) + { + TSharedRef Widget = Path.Widgets[Idx].Widget; + if (!Widget->SupportsKeyboardFocus()) continue; + if (ViewportWidget.IsValid() && Widget == ViewportWidget) break; + if (UGameInstance *GI = GetGameInstance()) + { + if (AlxPlayerControllerBase *PC = Cast( + GI->GetFirstLocalPlayerController(GetWorld()))) + { + PC->ClickToFocus(Widget); + } + } + break; + } + } + } + + return Super::InputKey(EventArgs); +} diff --git a/Source/Integration/LuprexViewportClient.h b/Source/Integration/LuprexViewportClient.h new file mode 100644 index 00000000..d0d1f5a0 --- /dev/null +++ b/Source/Integration/LuprexViewportClient.h @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////// +// +// LuprexViewportClient.h +// +// Custom game viewport client. Implements a project-wide +// click-to-focus rule: when a left-mouse-button click is +// not handled by any widget and bubbles up to the viewport, +// we hit-test under the cursor and focus the nearest +// focusable ancestor of whatever was hit. +// +//////////////////////////////////////////////////////////// + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/GameViewportClient.h" +#include "LuprexViewportClient.generated.h" + +UCLASS() +class INTEGRATION_API UlxViewportClient : public UGameViewportClient +{ + GENERATED_BODY() + +public: + UlxViewportClient(const FObjectInitializer &ObjectInitializer); + + virtual bool InputKey(const FInputKeyEventArgs &EventArgs) override; +}; diff --git a/Source/Integration/PlayerControllerBase.cpp b/Source/Integration/PlayerControllerBase.cpp index fe864df7..c20e8370 100644 --- a/Source/Integration/PlayerControllerBase.cpp +++ b/Source/Integration/PlayerControllerBase.cpp @@ -8,6 +8,8 @@ #include "Engine/GameViewportClient.h" #include "Engine/LevelScriptActor.h" #include "Engine/LocalPlayer.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/Application/SlateUser.h" #include "Kismet/GameplayStatics.h" #include "Widgets/SViewport.h" @@ -59,6 +61,40 @@ FVector2D AlxPlayerControllerBase::GetLookAtPixel(const UObject *Context) return ScreenPosition; } +void AlxPlayerControllerBase::BeginPlay() +{ + Super::BeginPlay(); + if (FSlateApplication::IsInitialized()) + { + FocusChangingHandle = FSlateApplication::Get().OnFocusChanging().AddUObject( + this, &AlxPlayerControllerBase::HandleFocusChanging); + } +} + +void AlxPlayerControllerBase::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (FocusChangingHandle.IsValid() && FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().OnFocusChanging().Remove(FocusChangingHandle); + FocusChangingHandle.Reset(); + } + Super::EndPlay(EndPlayReason); +} + +void AlxPlayerControllerBase::HandleFocusChanging( + const FFocusEvent &FocusEvent, + const FWeakWidgetPath &OldPath, + const TSharedPtr &OldFocusedWidget, + const FWidgetPath &NewPath, + const TSharedPtr &NewFocusedWidget) +{ + UE_LOG(LogLuprexIntegration, Display, + TEXT("Focus changing: '%s' -> '%s' (cause: %s)"), + OldFocusedWidget.IsValid() ? *OldFocusedWidget->GetTypeAsString() : TEXT(""), + NewFocusedWidget.IsValid() ? *NewFocusedWidget->GetTypeAsString() : TEXT(""), + *UEnum::GetValueAsString(FocusEvent.GetCause())); +} + UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *Widget) { if (!IsValid(Widget)) return nullptr; @@ -75,7 +111,7 @@ UInputComponent* AlxPlayerControllerBase::GetWidgetInputComponent(UUserWidget *W return Cast(Value); } -void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool ShowPointer, bool BlockInput, UWidget *Focus) +void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool ShowPointer, bool BlockInput, UWidget *Focus, bool EnableInputComponent) { if (!IsValid(Widget)) { @@ -91,7 +127,13 @@ void AlxPlayerControllerBase::WidgetRequestInputMode(UUserWidget *Widget, bool S *Widget->GetName(), *GetNameSafe(OwningPC)); return; } - PC->InputModeRequests.Request(FlxInputModeRequest(Widget, Focus, ShowPointer, BlockInput)); + FlxInputModeRequest Req; + Req.Widget = Widget; + Req.Focus = Focus; + Req.ShowPointer = ShowPointer; + Req.BlockInput = BlockInput; + Req.EnableInputComponent = EnableInputComponent; + PC->InputModeRequests.Request(Req); } void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputComponent) @@ -103,7 +145,7 @@ void AlxPlayerControllerBase::PushInputComponent(UInputComponent* InInputCompone // focus, pointer visibility, and input blocking across them. if (UUserWidget *Widget = Cast(InInputComponent->GetOuter())) { - InputModeRequests.EnsureWidget(Widget); + InputModeRequests.SetEnableInputComponent(Widget, true); return; } CurrentInputStack.RemoveSingle(InInputComponent); @@ -115,6 +157,12 @@ bool AlxPlayerControllerBase::PopInputComponent(UInputComponent* InInputComponen { if (InInputComponent) { + if (UUserWidget *Widget = Cast(InInputComponent->GetOuter())) + { + InputModeRequests.SetEnableInputComponent(Widget, false); + InInputComponent->ClearBindingValues(); + return true; + } if (CurrentInputStack.RemoveSingle(InInputComponent) > 0) { InInputComponent->ClearBindingValues(); @@ -197,11 +245,13 @@ void AlxPlayerControllerBase::BuildInputStack(TArray& InputSta const TArray &Requests = InputModeRequests.GetRequests(); for (int32 Idx = Requests.Num() - 1; Idx >= 0; --Idx) { - UUserWidget *Widget = Requests[Idx].Widget; + const FlxInputModeRequest &Req = Requests[Idx]; + if (!Req.EnableInputComponent) continue; + UUserWidget *Widget = Req.Widget; if (!Widget->IsInViewport()) continue; if (UInputComponent *IC = GetWidgetInputComponent(Widget)) { - IC->bBlockInput = Requests[Idx].BlockInput; + IC->bBlockInput = Req.BlockInput; InputStack.Push(IC); } } @@ -220,6 +270,8 @@ void AlxPlayerControllerBase::UpdateInputMode() if (!ViewportWidget.IsValid()) return; TSharedRef ViewportWidgetRef = ViewportWidget.ToSharedRef(); FReply &SlateOperations = LocalPlayer->GetSlateOperations(); + TSharedPtr SlateUser = LocalPlayer->GetSlateUser(); + if (!SlateUser.IsValid()) return; // The first active entry in InputModeRequests dictates the // pointer / capture / focus state. If there are no requests at all, @@ -235,41 +287,66 @@ void AlxPlayerControllerBase::UpdateInputMode() 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. + // Only release capture if the viewport is currently holding it + // (e.g. we just came from GameOnly). A blanket ReleaseMouseCapture + // every tick would yank capture away from widgets mid-gesture + // (scrollbar drags, slider drags, etc.). + if (SlateUser->DoesWidgetHaveAnyCapture(ViewportWidget)) + { + SlateOperations.ReleaseMouseCapture(); + } 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. + // Also captures the mouse to the viewport. 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 SlateFocus = Top->Focus ? Top->Focus->GetCachedWidget() : nullptr; - if (SlateFocus.IsValid()) - { - SlateOperations.SetUserFocus(SlateFocus.ToSharedRef()); - } - else - { - SlateOperations.SetUserFocus(ViewportWidgetRef); - } - GameViewportClient->SetIgnoreInput(false); + + // How we handle focus depends on whether we're showing the pointer. + // In pointer mode, we set focus to the desired state just once, + // and then we let the pointer control it from there on. In + // no-pointer mode, we set focus to the desired state and + // keep putting it back forever. + // + // If the user clicks the mouse on a focusable widget, the + // viewport client notifies us of that fact. We then focus the + // widget if possible. + // + if ((!Top->ShowPointer) || (LastRequestGrantedFocus != Top->SequenceNumber)) + { + if (Top->Focus) + { + if (TSharedPtr SlateFocus = Top->Focus->GetCachedWidget()) + { + SlateOperations.SetUserFocus(SlateFocus.ToSharedRef()); + LastRequestGrantedFocus = Top->SequenceNumber; + } + } + else + { + SlateOperations.SetUserFocus(ViewportWidgetRef); + LastRequestGrantedFocus = Top->SequenceNumber; + } + } + else if (TSharedPtr ClickedWidget = ClickToFocusTarget.Pin()) + { + SlateOperations.SetUserFocus(ClickedWidget.ToSharedRef()); + } + ClickToFocusTarget.Reset(); +} + +void AlxPlayerControllerBase::ClickToFocus(TSharedRef Widget) +{ + ClickToFocusTarget = Widget.ToWeakPtr(); } void AlxPlayerControllerBase::UpdateLookAt() diff --git a/Source/Integration/PlayerControllerBase.h b/Source/Integration/PlayerControllerBase.h index 176cf854..6dc011e9 100644 --- a/Source/Integration/PlayerControllerBase.h +++ b/Source/Integration/PlayerControllerBase.h @@ -3,7 +3,7 @@ #include "CoreMinimal.h" #include "Engine/HitResult.h" #include "GameFramework/PlayerController.h" -#include "InputEvents.h" +#include "InputModeRequest.h" #include "PlayerControllerBase.generated.h" UCLASS(BlueprintType, Blueprintable) @@ -41,6 +41,20 @@ public: // eventually reconcile focus, pointer, and capture state. void UpdateInputMode(); + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + +private: + FDelegateHandle FocusChangingHandle; + void HandleFocusChanging( + const struct FFocusEvent &FocusEvent, + const class FWeakWidgetPath &OldPath, + const TSharedPtr &OldFocusedWidget, + const class FWidgetPath &NewPath, + const TSharedPtr &NewFocusedWidget); + +public: + // Input stack overrides: unsorted, append-on-push. virtual void PushInputComponent(UInputComponent* InInputComponent) override; virtual bool PopInputComponent(UInputComponent* InInputComponent) override; @@ -55,8 +69,8 @@ public: // (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); + meta = (DefaultToSelf = "Widget", HideSelfPin = "true", EnableInputComponent = "true")) + static void WidgetRequestInputMode(class UUserWidget *Widget, bool ShowPointer, bool BlockInput, class UWidget *Focus, bool EnableInputComponent); // Get the player controller, cast to AlxPlayerControllerBase. static AlxPlayerControllerBase *FromContext(const UObject *Context); @@ -64,8 +78,22 @@ public: UPROPERTY() FHitResult CurrentLookAt; + // Input mode requests - see InputModeRequest.h for an explanation. UPROPERTY() FlxInputModeRequests InputModeRequests; + // The last input mode request whose focus request was granted. + UPROPERTY() + int32 LastRequestGrantedFocus = 0; + + // The viewport client uses this to notify us that the user + // clicked on a focusable widget. + void ClickToFocus(TSharedRef Widget); + +private: + TWeakPtr ClickToFocusTarget; + +public: + bool MustCallLookAtChanged = false; };