// Copyright Epic Games, Inc. All Rights Reserved. #include "CommonPlayerInputKey.h" #include "CommonInputSubsystem.h" #include "CommonLocalPlayer.h" #include "CommonPlayerController.h" #include "Components/SlateWrapperTypes.h" #include "Containers/UnrealString.h" #include "Delegates/Delegate.h" #include "Engine/World.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" #include "Internationalization/Internationalization.h" #include "Layout/Geometry.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Materials/Material.h" #include "Materials/MaterialInstanceDynamic.h" #include "Materials/MaterialInterface.h" #include "Math/Color.h" #include "Math/UnrealMathSSE.h" #include "Misc/AssertionMacros.h" #include "Rendering/DrawElements.h" #include "Rendering/RenderingCommon.h" #include "Rendering/SlateRenderer.h" #include "Styling/SlateBrush.h" #include "Styling/WidgetStyle.h" #include "Templates/Casts.h" #include "Templates/SharedPointer.h" #include "TimerManager.h" #include "Trace/Detail/Channel.h" #include "UObject/UnrealNames.h" #include "Widgets/InvalidateWidgetReason.h" class FPaintArgs; class FSlateRect; #define LOCTEXT_NAMESPACE "CommonKeybindWidget" DECLARE_LOG_CATEGORY_EXTERN(LogCommonPlayerInput, Log, All); DEFINE_LOG_CATEGORY(LogCommonPlayerInput); struct FSlateDrawUtil { static void DrawBrushCenterFit( FSlateWindowElementList& ElementList, uint32 InLayer, const FGeometry& InAllottedGeometry, const FSlateBrush* InBrush, const FLinearColor& InTint = FLinearColor::White) { DrawBrushCenterFitWithOffset ( ElementList, InLayer, InAllottedGeometry, InBrush, InTint, FVector2D(0, 0) ); } static void DrawBrushCenterFitWithOffset( FSlateWindowElementList& ElementList, uint32 InLayer, const FGeometry& InAllottedGeometry, const FSlateBrush* InBrush, const FLinearColor& InTint, const FVector2D InOffset) { if (!InBrush) { return; } const FVector2D AreaSize = InAllottedGeometry.GetLocalSize(); const FVector2D ProgressSize = InBrush->GetImageSize(); const float FitScale = FMath::Min(FMath::Min(AreaSize.X / ProgressSize.X, AreaSize.Y / ProgressSize.Y), 1.0f); const FVector2D FinalSize = FitScale * ProgressSize; const FVector2D Offset = (InAllottedGeometry.GetLocalSize() * 0.5f) - (FinalSize * 0.5f) + InOffset; FSlateDrawElement::MakeBox ( ElementList, InLayer, InAllottedGeometry.ToPaintGeometry(Offset, FinalSize), InBrush, ESlateDrawEffect::None, InTint ); } }; void FMeasuredText::SetText(const FText& InText) { CachedText = InText; bTextDirty = true; } FVector2D FMeasuredText::UpdateTextSize(const FSlateFontInfo &InFontInfo, float FontScale) const { if (bTextDirty) { bTextDirty = false; CachedTextSize = FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure(CachedText, InFontInfo, FontScale); } return CachedTextSize; } UCommonPlayerInputKey::UCommonPlayerInputKey(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , BoundKeyFallback(EKeys::Invalid) , InputTypeOverride(ECommonInputType::Count) { FrameSize = FVector2D(0, 0); } void UCommonPlayerInputKey::NativePreConstruct() { Super::NativePreConstruct(); UpdateKeybindWidget(); if (IsDesignTime()) { ShowHoldBackPlate(); RecalculateDesiredSize(); } } void UCommonPlayerInputKey::NativeConstruct() { Super::NativeConstruct(); } void UCommonPlayerInputKey::NativeDestruct() { if (ProgressPercentageMID) { // Need to restore the material on the brush before we kill off the MID. HoldProgressBrush.SetResourceObject(ProgressPercentageMID->GetMaterial()); ProgressPercentageMID->MarkAsGarbage(); ProgressPercentageMID = nullptr; } Super::NativeDestruct(); } int32 UCommonPlayerInputKey::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { int32 MaxLayer = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); if (bDrawProgress) { FSlateDrawUtil::DrawBrushCenterFit ( OutDrawElements, ++MaxLayer, AllottedGeometry, &HoldProgressBrush, FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * HoldProgressBrush.GetTint(InWidgetStyle)) ); } if (bDrawCountdownText) { const FVector2D CountdownTextOffset = (AllottedGeometry.GetLocalSize() - CountdownText.GetTextSize()) * 0.5f; FSlateDrawElement::MakeText ( OutDrawElements, ++MaxLayer, AllottedGeometry.ToOffsetPaintGeometry(CountdownTextOffset), CountdownText.GetText(), CountdownTextFont, ESlateDrawEffect::None, FLinearColor(InWidgetStyle.GetColorAndOpacityTint()) ); } else if (bDrawBrushForKey) { // Draw Shadow FSlateDrawUtil::DrawBrushCenterFitWithOffset ( OutDrawElements, ++MaxLayer, AllottedGeometry, &CachedKeyBrush, FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * FLinearColor::Black), FVector2D(1, 1) ); FSlateDrawUtil::DrawBrushCenterFit ( OutDrawElements, ++MaxLayer, AllottedGeometry, &CachedKeyBrush, FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * CachedKeyBrush.GetTint(InWidgetStyle)) ); } else if (KeybindText.GetTextSize().X > 0) { const FVector2D FrameOffset = (AllottedGeometry.GetLocalSize() - FrameSize) * 0.5f; FSlateDrawElement::MakeBox ( OutDrawElements, ++MaxLayer, AllottedGeometry.ToPaintGeometry(FrameOffset, FrameSize), &KeyBindTextBorder, ESlateDrawEffect::None, FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * KeyBindTextBorder.GetTint(InWidgetStyle)) ); const FVector2D ActionTextOffset = (AllottedGeometry.GetLocalSize() - KeybindText.GetTextSize()) * 0.5f; FSlateDrawElement::MakeText ( OutDrawElements, ++MaxLayer, AllottedGeometry.ToOffsetPaintGeometry(ActionTextOffset), KeybindText.GetText(), KeyBindTextFont, ESlateDrawEffect::None, FLinearColor(InWidgetStyle.GetColorAndOpacityTint()) ); } return MaxLayer; } void UCommonPlayerInputKey::StartHoldProgress(FName HoldActionName, float HoldDuration) { if (HoldActionName == BoundAction && ensureMsgf(HoldDuration > 0.0f, TEXT("Trying to perform hold action \"%s\" with no HoldDuration"), *BoundAction.ToString())) { HoldKeybindDuration = HoldDuration; HoldKeybindStartTime = GetWorld()->GetRealTimeSeconds(); UpdateHoldProgress(); } } void UCommonPlayerInputKey::StopHoldProgress(FName HoldActionName, bool bCompletedSuccessfully) { if (HoldActionName == BoundAction) { HoldKeybindStartTime = 0.f; HoldKeybindDuration = 0.f; if (ensure(ProgressPercentageMID)) { ProgressPercentageMID->SetScalarParameterValue(PercentageMaterialParameterName, 0.f); } if (bDrawCountdownText) { bDrawCountdownText = false; Invalidate(EInvalidateWidget::Paint); RecalculateDesiredSize(); } } } void UCommonPlayerInputKey::SyncHoldProgress() { // If we had an active hold action, stop it if (HoldKeybindStartTime > 0.f) { StopHoldProgress(BoundAction, false); } } void UCommonPlayerInputKey::UpdateHoldProgress() { if (HoldKeybindStartTime != 0.f && HoldKeybindDuration > 0.f) { const float CurrentTime = GetWorld()->GetRealTimeSeconds(); const float ElapsedTime = FMath::Min(CurrentTime - HoldKeybindStartTime, HoldKeybindDuration); const float RemainingTime = FMath::Max(0.0f, HoldKeybindDuration - ElapsedTime); if (ElapsedTime < HoldKeybindDuration && ensure(ProgressPercentageMID)) { const float HoldKeybindPercentage = ElapsedTime / HoldKeybindDuration; ProgressPercentageMID->SetScalarParameterValue(PercentageMaterialParameterName, HoldKeybindPercentage); // Schedule a callback for next tick to update the hold progress again. GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::UpdateHoldProgress); } if (bShowTimeCountDown) { FNumberFormattingOptions Options; Options.MinimumFractionalDigits = 1; Options.MaximumFractionalDigits = 1; CountdownText.SetText(FText::AsNumber(RemainingTime, &Options)); bDrawCountdownText = true; Invalidate(EInvalidateWidget::Paint); RecalculateDesiredSize(); } } } void UCommonPlayerInputKey::UpdateKeybindWidget() { if (!GetOwningPlayer()) { bWaitingForPlayerController = true; return; } UCommonInputSubsystem* CommonInputSubsystem = GetInputSubsystem(); if (CommonInputSubsystem && !CommonInputSubsystem->ShouldShowInputKeys()) { SetVisibility(ESlateVisibility::Collapsed); return; } const bool bIsUsingGamepad = (InputTypeOverride == ECommonInputType::Gamepad) || ((CommonInputSubsystem != nullptr) && (CommonInputSubsystem->GetCurrentInputType() == ECommonInputType::Gamepad)) ; if (!BoundKey.IsValid()) { BoundKey = BoundKeyFallback; } UE_LOG(LogCommonPlayerInput, Verbose, TEXT("UCommonKeybindWidget::UpdateKeybindWidget: Action: %s Key: %s"), *(BoundAction.ToString()), *(BoundKey.ToString())); // Must be called before Update, due to the creation of ProgressPercentageMID which will be used in Update SetupHoldKeybind(); bool NewDrawBrushForKey = false; bool NeedToRecalcSize = false; if (BoundKey.IsValid()) { SetVisibility(ESlateVisibility::HitTestInvisible); ShowHoldBackPlate(); NeedToRecalcSize = true; } else { if (bShowUnboundStatus) { SetVisibility(ESlateVisibility::HitTestInvisible); NewDrawBrushForKey = false; KeybindText.SetText(LOCTEXT("Unbound", "Unbound")); NeedToRecalcSize = true; } else { SetVisibility(ESlateVisibility::Collapsed); } } if (bDrawBrushForKey != NewDrawBrushForKey) { bDrawBrushForKey = NewDrawBrushForKey; Invalidate(EInvalidateWidget::Paint); } // As RecalculateDesiredSize relies on the bDrawBrushForKey // we shouldn't call it until that value has been finalized // for the update if (NeedToRecalcSize) { RecalculateDesiredSize(); } } void UCommonPlayerInputKey::SetBoundKey(FKey NewKey) { if (NewKey != BoundKey) { BoundKeyFallback = NewKey; BoundAction = NAME_None; UpdateKeybindWidget(); } } void UCommonPlayerInputKey::SetBoundAction(FName NewBoundAction) { bool bUpdateWidget = true; if (BoundAction != NewBoundAction) { BoundAction = NewBoundAction; } if (bUpdateWidget) { UpdateKeybindWidget(); } } void UCommonPlayerInputKey::NativeOnInitialized() { Super::NativeOnInitialized(); if (UCommonLocalPlayer* CommonLocalPlayer = GetOwningLocalPlayer()) { CommonLocalPlayer->OnPlayerControllerSet.AddUObject(this, &ThisClass::HandlePlayerControllerSet); } } void UCommonPlayerInputKey::SetForcedHoldKeybind(bool InForcedHoldKeybind) { if (InForcedHoldKeybind) { SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus::ForcedHold); } else { SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus::NoForcedHold); } } void UCommonPlayerInputKey::SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus InForcedHoldKeybindStatus) { ForcedHoldKeybindStatus = InForcedHoldKeybindStatus; UpdateKeybindWidget(); } void UCommonPlayerInputKey::SetShowProgressCountDown(bool bShow) { bShowTimeCountDown = bShow; } void UCommonPlayerInputKey::SetupHoldKeybind() { ACommonPlayerController* OwningCommonPlayer = Cast(GetOwningPlayer()); // Setup the hold if (ForcedHoldKeybindStatus == ECommonKeybindForcedHoldStatus::ForcedHold) { bIsHoldKeybind = true; } else if (ForcedHoldKeybindStatus == ECommonKeybindForcedHoldStatus::NeverShowHold) { bIsHoldKeybind = false; } if (ensure(OwningCommonPlayer)) { if (bIsHoldKeybind) { // Setup the ProgressPercentageMID if (ProgressPercentageMID == nullptr) { if (UMaterialInterface* Material = Cast(HoldProgressBrush.GetResourceObject())) { ProgressPercentageMID = UMaterialInstanceDynamic::Create(Material, this); HoldProgressBrush.SetResourceObject(ProgressPercentageMID); } } SyncHoldProgress(); } } } void UCommonPlayerInputKey::ShowHoldBackPlate() { bool bDirty = false; if (IsHoldKeybind()) { float BrushSizeAsValue = 32.0f; float DesiredBoxSize = BrushSizeAsValue + 10.0f; if (!bDrawBrushForKey) { DesiredBoxSize += 14.0f; } const FVector2D NewDesiredBrushSize(DesiredBoxSize, DesiredBoxSize); if (HoldProgressBrush.GetImageSize() != NewDesiredBrushSize) { HoldProgressBrush.SetImageSize(NewDesiredBrushSize); bDirty = true; } if (!bDrawProgress) { bDrawProgress = true; bDirty = true; } static const FName BackAlphaName = TEXT("BackAlpha"); static const FName OutlineAlphaName = TEXT("OutlineAlpha"); if (ProgressPercentageMID) { ProgressPercentageMID->SetScalarParameterValue(BackAlphaName, 0.2f); ProgressPercentageMID->SetScalarParameterValue(OutlineAlphaName, 0.4f); } } else { if (bDrawProgress) { bDrawProgress = false; bDirty = true; } } if (bDirty) { Invalidate(EInvalidateWidget::Paint); } } void UCommonPlayerInputKey::HandlePlayerControllerSet(UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController) { if (bWaitingForPlayerController && GetOwningPlayer()) { UpdateKeybindWidget(); bWaitingForPlayerController = false; } } void UCommonPlayerInputKey::RecalculateDesiredSize() { FVector2D MaximumDesiredSize(0, 0); float LayoutScale = 1; if (bDrawProgress) { MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, HoldProgressBrush.GetImageSize()); } if (bDrawCountdownText) { MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, CountdownText.UpdateTextSize(CountdownTextFont, LayoutScale)); } else if (bDrawBrushForKey) { MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, CachedKeyBrush.GetImageSize()); } else { const FVector2D KeybindTextSize = KeybindText.UpdateTextSize(KeyBindTextFont, LayoutScale); FrameSize = FVector2D::Max(KeybindTextSize, KeybindFrameMinimumSize) + KeybindTextPadding.GetDesiredSize(); MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, FrameSize); } SetMinimumDesiredSize(MaximumDesiredSize); } #undef LOCTEXT_NAMESPACE