Bubbling NativeOnMouseButtonDown from custom widget to PlayerController

Hi to all of you

If you create a custom widget for drawing selection rectangle and try to process mouse input (LMB for example) with NativeOnMouseButtonDown event the system will not pass information about mouse being pressed to player controller because of NativeOnMouseButtonDown returns FReply::Handled() (it’s needed for processing NativeOnMouseButtonUp event).

If NativeOnMouseButtonDown returns FReply::Unhandled() then PlayerController will get information about LMB were pressed but NativeOnMouseButtonUp won’t fire

Steps to reproduce

  1. Create class UMyUserWidget : public UUserWidget.
  2. Fill MyUserWidget.h and MyUserWidget.cpp with provided code.
  3. In UE editor add new blueprint which parent is UMyUserWidget calling it NewBlueprint.
  4. Add new basic WidgetBlueprint calling it NewWidgetBlueprint.
  5. In NewWidgetBlueprint delete CanvasPanel and add NewBlueprint as a child. Set visibility of NewWidgetBlueprint to Not Hit-Testable (Self Only) on right side of UMG Designer
  6. In GameMode blueprint add nodes as in Fig.1.
  7. Start simulation and press and hold LMB after 1 second you will be able to draw blue rectangle and as you could see the time from PlayerController->GetInputKeyTimeDown(EKeys::LeftMouseButton) will be zero. If you press RMB then you will see that PlayerController->GetInputKeyTimeDown(EKeys::RightMouseButton) will raise from zero to some number, thus PlayerController knowing that RMB is pressed and LMB isn’t. If you double click with LMB then PlayerController->GetInputKeyTimeDown(EKeys::LeftMouseButton) will show time sinse first click and won’t stop.

What should I do to be able to get time of key pressed in PlayerController? In other words what should I do to pass handled event from widget to PlayerController (quick remind, that NativeOnMouseButtonUp should necessarily fire)?


MyUserWidget.h

    #pragma once
    
    #include "CoreMinimal.h"
    #include "Blueprint/UserWidget.h"
    #include "MyUserWidget.generated.h"
    
    class ARTSPlayerController;
    
    UCLASS()
    class RTSPROJECT_API UMyUserWidget : public UUserWidget
    {
    	GENERATED_BODY()
    
    public:
    
    	UPROPERTY(BlueprintReadOnly)
    	ARTSPlayerController* PlayerController = nullptr;
    
    private:
    
    	FVector2D StartClick;
    	FVector2D HoldingLocation;
    	bool bIsDrawingSelectionRectangle = false;
    	bool bIsLMBPressed = false;
    	float StartClickTime = 0;
    	float HoldTime = 1;
    
    	virtual void NativeConstruct() override;
    	virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
    	virtual int32 NativePaint(const FPaintArgs& Args,
    		const FGeometry& AllottedGeometry,
    		const FSlateRect& MyCullingRect,
    		FSlateWindowElementList& OutDrawElements,
    		int32 LayerId,
    		const FWidgetStyle& InWidgetStyle,
    		bool bParentEnabled) const override;
    
    	virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
    	virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
    	virtual FReply NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
    	
    };

MyUserWidget.cpp

#include "UI/MyUserWidget.h"

#include "Core/RTSPlayerController.h"

void UMyUserWidget::NativeConstruct()
{
	Super::NativeConstruct();
	ARTSPlayerController* TestPlayerController = Cast<ARTSPlayerController>(GetOwningPlayer());
	if (TestPlayerController)
	{
		PlayerController = TestPlayerController;
	}
	// For NativeOnKeyDown event to work properly the flag IsFocusable should be true in Widget->Designer->Interaction
	bIsFocusable = true;

	Visibility = ESlateVisibility::Visible;
}

void UMyUserWidget::NativeTick(const FGeometry& MovieSceneBlends, float InDeltaTime)
{
	GEngine->AddOnScreenDebugMessage(-1, 0.01, FColor::White, FString::Printf(TEXT("PlayerController->GetInputKeyTimeDown(EKeys::LeftMouseButton) is %s"), *FString::SanitizeFloat(PlayerController->GetInputKeyTimeDown(EKeys::LeftMouseButton))));
GEngine->AddOnScreenDebugMessage(-1, 0.01, FColor::White, FString::Printf(TEXT("PlayerController->GetInputKeyTimeDown(EKeys::RightMouseButton) is %s"), *FString::SanitizeFloat(PlayerController->GetInputKeyTimeDown(EKeys::RightMouseButton))));
GEngine->AddOnScreenDebugMessage(-1, 0.01, FColor::White, FString::Printf(TEXT("bIsLMBPressed = %s"), bIsLMBPressed ? *FString("true") : *FString("false")));
GEngine->AddOnScreenDebugMessage(-1, 0.01, FColor::White, FString::Printf(TEXT("bIsDrawingSelectionRectangle = %s"), bIsDrawingSelectionRectangle ? *FString("true") : *FString("false")));


	if (bIsLMBPressed)
	{
		const float CurrentTime = GetWorld()->GetTimeSeconds() - StartClickTime;
		if (CurrentTime < HoldTime)
		{
			GEngine->AddOnScreenDebugMessage(-1, 0.01, FColor::White, TEXT("Just click"));
		}
		else
		{
			GEngine->AddOnScreenDebugMessage(-1, 0.01, FColor::White, TEXT("Just drawing"));
			bIsDrawingSelectionRectangle = true;
		}
	}
	Super::NativeTick(MovieSceneBlends, InDeltaTime);
}

int32 UMyUserWidget::NativePaint(const FPaintArgs& MovieSceneBlends, const FGeometry& AllottedGeometry,
	const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId,
	const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
	LayerId = Super::NativePaint(MovieSceneBlends, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
	FPaintContext Context(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
	Context.MaxLayer++;

	if (bIsDrawingSelectionRectangle)
	{
		TArray<FVector2D> Points;
		Points.Add(FVector2D(StartClick.X, StartClick.Y));
		Points.Add(FVector2D(HoldingLocation.X, StartClick.Y));
		Points.Add(FVector2D(HoldingLocation.X, HoldingLocation.Y));
		Points.Add(FVector2D(StartClick.X, HoldingLocation.Y));
		Points.Add(FVector2D(StartClick.X, StartClick.Y));

		constexpr float Thickness = 1;

		FSlateDrawElement::MakeLines(
			Context.OutDrawElements,
			Context.MaxLayer,
			Context.AllottedGeometry.ToPaintGeometry(),
			Points,
			ESlateDrawEffect::None,
			FLinearColor::Blue,
			true,
			Thickness);
	}

	return FMath::Max(LayerId, Context.MaxLayer);
}

FReply UMyUserWidget::NativeOnMouseButtonDown(const FGeometry& MovieSceneBlends, const FPointerEvent& InMouseEvent)
{
	GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Blue, TEXT("UMyUserWidget::NativeOnMouseButtonDown"));
	auto Reply = Super::NativeOnMouseButtonDown(MovieSceneBlends, InMouseEvent);
	if (Reply.IsEventHandled())
	{
		return Reply;
	}
	if (InMouseEvent.GetPressedButtons().Num() == 1 && InMouseEvent.GetPressedButtons().Contains(EKeys::LeftMouseButton))
	{
		StartClickTime = GetWorld()->GetTimeSeconds();
		StartClick = MovieSceneBlends.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
		HoldingLocation = StartClick;
		bIsLMBPressed = true;
		return FReply::Handled();
	}
	return FReply::Unhandled();
}

FReply UMyUserWidget::NativeOnMouseMove(const FGeometry& MovieSceneBlends, const FPointerEvent& InMouseEvent)
{
	auto Reply = Super::NativeOnMouseMove(MovieSceneBlends, InMouseEvent);
	if (Reply.IsEventHandled())
	{
		return Reply;
	}
	if (bIsDrawingSelectionRectangle)
	{
		HoldingLocation = MovieSceneBlends.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition());
	}
	return FReply::Unhandled();
}

FReply UMyUserWidget::NativeOnMouseButtonUp(const FGeometry& MovieSceneBlends, const FPointerEvent& InMouseEvent)
{
	GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Blue, TEXT("UMyUserWidget::NativeOnMouseButtonUp"));
	auto Reply = Super::NativeOnMouseButtonUp(MovieSceneBlends, InMouseEvent);
	if (Reply.IsEventHandled())
	{
		return Reply;
	}
	bIsDrawingSelectionRectangle = false;
	bIsLMBPressed = false;
	StartClickTime = 0;
	return FReply::Handled();
}

Fig.1