To clarify, we’re not trying to support simultaneous mouse and keyboard navigation and understand that this would cause design issues, as mentioned in the documentation.
Here are some images showing visuals when navigating with mouse, gamepad and keyboard respectively:
[Image Removed]
[Image Removed]
[Image Removed]
In terms of specification, we’re looking for a solution that:
Works with Common UI
Works with Enhanced Input
Supports cardinal navigation for gamepad, mouse and keyboard (including selection visuals)
Supports the use of CommonActionWidget for displaying actions, for gamepad, mouse and keyboard
Removes the blue outline for gamepad and keyboard navigation
This is a bit of a tricky subject because of the considerations mentioned in that docs page you linked, but basically what you’re running into is inconsistency between two separate systems- CommonUI with it’s custom navigation handling and default actions, and Slate “native” navigation (i.e. the out-of-the-box keyboard navigation features). There are a few things you can do to bring these in line.
First, the blue outline is driven by Render Focus Rule in your project settings. The default is to show it when navigating (with Slate native navigation), but you can turn that off if it interferes with other visuals. CommonUI instead moves around an invisible cursor to trigger the hover visual of the button, you may be able to manually move that cursor over the focused widget to replicate similar behavior with keyboard navigation.
Second, the space key is the default accept key for Slate native navigation, but you can customize that by applying your own FNativationConfig (see FSlateApplication::SetNavigationConfig).
Finally, keyboard navigation is going to use our native hittestgrid-based navigation rules, unless you explicitly define navigation rules on each widget. Generally it should navigate to the thing that makes sense (as long as the only focusable widgets are something that should be focusable), though after focusing the viewport you’ll need to do something to manually bring focus back to a widget. Usually when you summon a menu, you might focus the first button- CommonUI already has pretty robust handling for that via DesiredFocusWidget to set focus when activating things, and that focused widget would be the same in both the CommonUI and native Slate domains, so you may just need to dig into where focus is moving when it gets stuck. The console slate debugger can help with that.
Hopefully that can get you pointed in the direction, it’s not super trivial to make keyboard and gamepad navigation behave similarly but a few changes should bring them more in line. With keyboard controls we assume a mouse is also present so there isn’t the same risk of softlocking that would be present with a gamepad player, but manually setting focus or customizing navigation rules can usually help keep things inbounds.
I’m also trying to support both Mouse and Keyboard navigation at the same time, along with Gamepad.
I’ve mostly managed to do it, by indeed adding calls to re-focus buttons each time the mouse clicks on an “empty space”.
Currently:
No issue with Gamepad
No issue with Mouse
Only one issue with Keyboard => when re-focusing a Widget (after a mouse click or after adding a Modal menu), the Widget is correctly selected and focused, visually all is ok, but the “Confirm” action does not work.
Navigation with the arrows (Up/Right/Bottom/Left) works fine, specific Actions defined in the CommonUI DataTable work fine, but when trying to use Space to “click” the button, it fails.
The ProcessKeyUpEvent / ProcessKeyDown Events within SlateApplication are called, but the Event is not bubbled up to SButton.
Slate Debugger indicates that the Event is fired on SViewport instead of the Widget.
I’ve tried to SetFocus, SetUserFocus, SetKeyboardFocus, but nothing’s worked so far.
I’ve tested in Editor and in a Packaged game, just to be sure, but the behavior is the same.
Actually, I’ve noticed a difference between a successful click and a non-validated click when using the space bar with SlateDebugger.Event.InputRoutingModeEnabled
- In the first case, the first entry of the Route is MyButtonClass [InternalRootButtonBase(CommonButtonBase.cpp(255))]
- In the second case, the first entry of the Route is MyButtonClass [MyButtonName]
So it seems the Spacebar Event is not handled because the InternalRootButtonBase is not selected.
Which is, I think, confirmed by the following log (in my main Menu, I need to navigate with Left/Right arrows to make the Spacebar event work): Focus Changing(0:Navigation) - MyButtonClass [MyButtonName] → MyButtonClass [InternalRootButtonBase(CommonButtonBase.cpp(255))]
Thus, I think I just need to find a way to select the InternalRootButtonBase when MyButton is re-focused.
I’ve spent the night on it, trying many many things to focus the InternalRootButtonBase, and the only working way I’ve found involves adding some code to UCommonButtonBase and UCommonButtonInternalBase to get access to their private RootButton and MyCommonButton variables.
If it may help some other users, here is my FocusInternalButtonHack function.
// Check to avoid infinite loop
if (WidgetTree->RootWidget && !WidgetTree->RootWidget->HasUserFocus(GetOwningPlayer()))
{
/* In order to successfully focus our UCommonButtonBase Button to respond to Keyboard Input, we need
to set the focus to the Root Button of the UCommonButtonBase, named "InternalRootButtonBase". */
TWeakObjectPtr<class UCommonButtonInternalBase> RootBtn = GetRootButton();
/* But to do that, we have to focus the corresponding Slate SWidget, otherwise Slate will try
to focus our UMyButtonBase, which is already focused, so it will fail.
So we need to get the Slate SButton from our Root Button. */
TSharedRef<SCommonButton> MyCommonButtonRef = RootBtn->GetMyCommonButton().ToSharedRef();
// Then cast it to SWidget.
TSharedRef<SWidget> MyCommonWidgetRef = StaticCastSharedRef<SWidget>(MyCommonButtonRef);
/* So we can finally call our Set User Focus function.
Note: replace the "0" with the valid UserIndex in Split Screen */
FSlateApplication::Get().SetUserFocus(0, MyCommonWidgetRef, EFocusCause::Navigation);
}