Community Tutorial: Common UI Plugin - Keyboard Navigation

Hello all. After spending a week researching solutions to this problem, including this thread, I have put together a combination of some of the best ideas (this tutorial included) as well as my own solutions to get a fully functioning Keyboard+Mouse+Gamepad common button.

The only downside is it requires not just one but two additional ButtonStyles (unlike @Patterson’s solution which required one additional). I may tackle this in the future, but unfortunately CommonUI has a lot of this functionality locked down tight in private code sections, so I have simply labeled this as todo.

This solution requires C++ and is a C++ adaptation of Patterson’s implementation, so I apologize in advance to you blueprint only users. Unfortunately there is no way to replicate some of this in blueprint only.

Hopefully this well help some people, and maybe some keen eyed engineers may be able to even improve upon it.

You’ll notice in some places where I am checking the player controller against index 0. This is in case you want to avoid local multiplayer widgets belonging to other players interfering with the mouse and keyboard. If you don’t need those checks you can leave them out. I haven’t fully tested local multiplayer either so if anyone does this please report back.

With this Solution you will probably need to include Slate, SlateCore, CommonUI, and CommonInput in your additional dependencies. One improvement may be to use an enum for Normal, Hovered, and Pressed states and expose it to blueprint, in case you want to use these styles for text or something like that. Or you could simply get the current style which should be up to date by the time blueprints are called anyway. Just one thing to be aware of (and that I did not test around).

Edit: I will continue to make some edits as I make improvements to or simplify this code

Edit 2: Well I guess this still has a few problems. But it does seem better than solutions so far.

MyCommonButtonBase.h:

#pragma once

#include "CoreMinimal.h"
#include "CommonButtonBase.h"
#include "MyCommonButtonBase.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMyCommonButtonDelegate, UMyCommonButtonBase*, MyCommonButton);

/**
 * Class to get around Common UI Buttons being Difficult at Keyboard events
 */
UCLASS()
class My_API UMyCommonButtonBase : public UCommonButtonBase
{
	GENERATED_BODY()

public:

	UMyCommonButtonBase();
    // Use this to determine which widget to pick in UCommonActivatableWidget::GetDesiredFocusTarget	
	UPROPERTY(BlueprintAssignable)
	FMyCommonButtonDelegate OnFocused;
	
protected:
	
	// Begin UUserWidget Overrides
	virtual void NativeOnInitialized() override;
	virtual void NativeOnAddedToFocusPath(const FFocusEvent& InFocusEvent) override;
	virtual void NativeOnRemovedFromFocusPath(const FFocusEvent& InFocusEvent) override;
	virtual void NativeOnHovered() override;
	virtual void NativeOnUnhovered() override;
	virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
	virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
	virtual FReply NativeOnKeyUp(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
	// End UUserWidget Overrides

	// This should be identical to the Set Style, except with the normal state equal to 
	// the hovered state
	UPROPERTY(EditAnywhere, Category = Style)
	TSubclassOf<UCommonButtonStyle> HoveredStyle;

	// This should be identical to the Set Style, except with the normal AND hovered state 
	// equal to the pressed state
	UPROPERTY(EditAnywhere, Category = Style)
	TSubclassOf<UCommonButtonStyle> PressedStyle;

	UPROPERTY(Transient)
	TSubclassOf<UCommonButtonStyle> DefaultStyle;
	
	UPROPERTY(Transient)
	FDataTableRowHandle DefaultTriggeringInputAction;
	
	UPROPERTY(Transient)
	FDataTableRowHandle NullTriggeringInputAction;
};

MyCommonButtonBase.cpp:

#include "MyCommonButtonBase.h"
#include "Kismet/GameplayStatics.h"

DECLARE_LOG_CATEGORY_EXTERN(LogMyCommonButtonBase, Log, All);
DEFINE_LOG_CATEGORY(LogMyCommonButtonBase);

UMyCommonButtonBase::UMyCommonButtonBase()
{
    // Default to focusable because a non-focusable button doesn't make any sense
    SetIsFocusable(true);
}

void UMyCommonButtonBase::NativeOnInitialized()
{
    // Store the TriggeringInputAction and then disable it for now
    DefaultTriggeringInputAction = TriggeringInputAction;
    TriggeringInputAction = NullTriggeringInputAction;
    
    // Store the Default Style
    DefaultStyle = Style;

    // TODO: Is there a way we can generate these styles so the user doesn't have to
    // create a bunch of duplicate styles? Most of this functionality is set to private
    // in CommonUI so it seems difficult short of modifying CommonUI directly, which is
    // not portable.
    if (!IsValid(HoveredStyle))
    {
        UE_LOG(LogMyCommonButtonBase, Warning, TEXT("%s: HoveredStyle is null. Did you forget to set it?"), *GetName());
        
        // This is a fallback in case the user forgot to set a Hovered Style
        HoveredStyle = DefaultStyle;
    }

    if (!IsValid(PressedStyle))
    {
        UE_LOG(LogMyCommonButtonBase, Warning, TEXT("%s: PressedStyle is null. Did you forget to set it?"), *GetName());

        // This is a fallback in case the user forgot to set a Pressed Style
        PressedStyle = DefaultStyle;
    }
}

void UMyCommonButtonBase::NativeOnAddedToFocusPath(const FFocusEvent& InFocusEvent)
{
    if (InFocusEvent.GetCause() == EFocusCause::SetDirectly)
    {
        // Prevent infinitely recurring focus path events from manually setting focus
        return;
    }
    Super::NativeOnAddedToFocusPath(InFocusEvent);

    // This will activate our input action (can press)
    TriggeringInputAction = DefaultTriggeringInputAction;

    // Simulate hovered style
    SetStyle(HoveredStyle);

    OnFocused.Broadcast(this);
}

void UMyCommonButtonBase::NativeOnRemovedFromFocusPath(const FFocusEvent& InFocusEvent)
{
    Super::NativeOnRemovedFromFocusPath(InFocusEvent);

    // This will deactivate our input action (cannot press)
    TriggeringInputAction = NullTriggeringInputAction;

    // Restore style to the default state
    SetStyle(DefaultStyle);
}

void UMyCommonButtonBase::NativeOnHovered()
{
    Super::NativeOnHovered();

    // Set the keyboard focus as well
    if (GetOwningLocalPlayer()->GetPlatformUserIndex() == 0)
    {
        OnFocused.Broadcast(this);
        SetKeyboardFocus();
    }
}

void UMyCommonButtonBase::NativeOnUnhovered()
{
    if (HasKeyboardFocus())
    {
        // There is no handy Clear Keyboard or User focus event so we need to go through Slate
        FSlateApplication::Get().ClearKeyboardFocus();
    }
    Super::NativeOnHovered();
}

FReply UMyCommonButtonBase::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
    FReply Reply = Super::NativeOnMouseMove(InGeometry, InMouseEvent);
    
    // OnMouseMove triggers even if the delta is zero, so just double check that we actually
    // moved the mouse. It may also trigger if we aren't hovering over this widget so use our
    // IsHovered flag to check for that
    if (IsHovered() && InMouseEvent.GetCursorDelta().SizeSquared() > 0.0f && GetOwningLocalPlayer()->GetPlatformUserIndex() == 0)
    {
        // Gamepad or Keyboard may have changed the focus, so when we actually move the mouse set focus back
        SetKeyboardFocus();
    }

    return Reply;
}

FReply UMyCommonButtonBase::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
    FReply Reply = Super::NativeOnKeyDown(InGeometry, InKeyEvent);

    if (FSlateApplication::Get().GetNavigationActionFromKey(InKeyEvent) == EUINavigationAction::Accept)
    {
        if (PressMethod == EButtonPressMethod::ButtonRelease || (PressMethod == EButtonPressMethod::DownAndUp))
        {
            SetStyle(PressedStyle);
            HandleButtonPressed();
        }
    }

    return Reply;
}
FReply UMyCommonButtonBase::NativeOnKeyUp(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
    FReply Reply = Super::NativeOnKeyUp(InGeometry, InKeyEvent);
    if (FSlateApplication::Get().GetNavigationActionFromKey(InKeyEvent) == EUINavigationAction::Accept)
    {
        if (PressMethod == EButtonPressMethod::ButtonRelease || (PressMethod == EButtonPressMethod::DownAndUp))
        {
            SetStyle(HoveredStyle);
            if (IsPressed())
            {
                HandleButtonReleased();
                HandleButtonClicked();
            }
        }
    }
    return Reply;
}
3 Likes