This C++ solution worked for me and does not require additional styles.
It hides the mouse cursor when you start using the navigation keys on the keyboard and acts the same as a gamepad. So if you want the mouse cursor to remain visible while you navigate the menu with your keyboard, this is not for you.
MyCommonUIActionRouter.h
#pragma once
#include "CoreMinimal.h"
#include "Input/CommonUIActionRouterBase.h"
#include "MyCommonUIActionRouter.generated.h"
UCLASS(MinimalAPI)
class UMyCommonUIActionRouter : public UCommonUIActionRouterBase
{
GENERATED_BODY()
protected:
MYGAME_API virtual TSharedRef<FCommonAnalogCursor> MakeAnalogCursor() const override;
};
MyCommonUIActionRouter.cpp
#include "MyCommonUIActionRouter.h"
#include "Input/CommonAnalogCursor.h"
#include "CommonInputTypeEnum.h"
// Tested with Unreal 5.7.3
class FMyCommonAnalogCursor : public FCommonAnalogCursor
{
public:
bool bKeyboardNavigationOn = false;
bool bIgnoreNextMouseMoveEvent = false;
FMyCommonAnalogCursor(const UCommonUIActionRouterBase& InActionRouter)
: FCommonAnalogCursor(InActionRouter)
{
}
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override
{
// When FCommonAnalogCursor::Tick updates the cursor position, this also triggers a HandleMouseMoveEvent, which we want to ignore.
// So we check if the Tick has changed the position.
FVector2D PreviousPosition = Cursor->GetPosition();
FCommonAnalogCursor::Tick(DeltaTime, SlateApp, Cursor);
if (Cursor->GetPosition() != PreviousPosition)
{
bIgnoreNextMouseMoveEvent = true;
}
}
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override
{
// So we don't need to hard-code all the navigation keys.
if (InKeyEvent.GetKey().GetMenuCategory() == EKeys::NAME_KeyboardCategory)
{
EUINavigation NavDir = SlateApp.GetNavigationDirectionFromKey(InKeyEvent);
if (NavDir == EUINavigation::Left || NavDir == EUINavigation::Right || NavDir == EUINavigation::Down || NavDir == EUINavigation::Up)
{
if (!bKeyboardNavigationOn)
{
bKeyboardNavigationOn = true;
// We don't just set ActiveInputMethod directly, so the Super::HandleInputMethodChanged can still do its thing.
HandleInputMethodChanged(ECommonInputType::Gamepad);
}
}
}
return FCommonAnalogCursor::HandleKeyDownEvent(SlateApp, InKeyEvent);
}
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
{
// This is to ignore the MouseMoveEvent generated when the hidden cursor is moved.
if (bIgnoreNextMouseMoveEvent)
{
bIgnoreNextMouseMoveEvent = false;
}
else
{
// When we detect a mouse move by the user, we switch keyboard navigation off, so the mouse cursor becomes visible again.
if (bKeyboardNavigationOn)
{
bKeyboardNavigationOn = false;
// We don't just set ActiveInputMethod directly, so the Super::HandleInputMethodChanged can still do its thing.
HandleInputMethodChanged(ECommonInputType::MouseAndKeyboard);
}
}
return FCommonAnalogCursor::HandleMouseMoveEvent(SlateApp, MouseEvent);
}
// We override this method for the use case where the player presses any other key on the keyboard besides the navigation keys.
virtual void HandleInputMethodChanged(ECommonInputType NewInputMethod) override
{
// We still want to call the super method, because it does Reset() on LastCursorTarget, which is a private member we cannot access.
FCommonAnalogCursor::HandleInputMethodChanged(NewInputMethod);
// When keyboard navigation is on, we fake being a Gamepad, but only in the eyes of FCommonAnalogCursor.
// This will cause the Tick function to still hide the mouse cursor and do the proper navigation.
if (bKeyboardNavigationOn)
{
ActiveInputMethod = ECommonInputType::Gamepad;
}
}
};
TSharedRef<FCommonAnalogCursor> UMyCommonUIActionRouter::MakeAnalogCursor() const
{
return FCommonAnalogCursor::CreateAnalogCursor<FMyCommonAnalogCursor>(*this);
}
This solution is like what they mention here in the official docs.
And if you to allow WASD as well, you can do this:
// Somewhere in your code, for example in the cpp of your custom GameInstance
#include "Framework/Application/NavigationConfig.h"
class FMyNavigationConfig : public FNavigationConfig
{
public:
FMyNavigationConfig()
{
KeyEventRules.Emplace(EKeys::W, EUINavigation::Up);
KeyEventRules.Emplace(EKeys::S, EUINavigation::Down);
KeyEventRules.Emplace(EKeys::A, EUINavigation::Left);
KeyEventRules.Emplace(EKeys::D, EUINavigation::Right);
}
};
// Then, in the Init of your GameInstance for example:
void UMyGameInstance::Init()
{
Super::Init();
if (FSlateApplication::IsInitialized())
{
TSharedRef<FMyNavigationConfig> NavConfig = MakeShared<FMyNavigationConfig>();
FSlateApplication::Get().SetNavigationConfig(NavConfig);
}
}
I also had to add Slate to my PublicDependencyModuleNames in my .Build.cs file.